├── .python-version ├── .github ├── FUNDING.yml └── workflows │ ├── publish.yml │ └── checks.yml ├── src └── dj_toml_settings │ ├── value_parsers │ ├── __init__.py │ ├── str_parsers.py │ └── dict_parsers.py │ ├── exceptions.py │ ├── __init__.py │ ├── config.py │ └── toml_parser.py ├── justfile ├── tests ├── test_value_parsers │ └── test_dict_parsers │ │ ├── test_dict_parser.py │ │ └── test_type_parser.py ├── test_config │ ├── test_configure_toml_settings.py │ └── test_get_toml_settings.py └── test_toml_parser │ └── test_parse_file.py ├── LICENSE ├── CHANGELOG.md ├── pyproject.toml ├── README.md └── uv.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: adamghill 2 | 3 | -------------------------------------------------------------------------------- /src/dj_toml_settings/value_parsers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/dj_toml_settings/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidActionError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /src/dj_toml_settings/__init__.py: -------------------------------------------------------------------------------- 1 | from dj_toml_settings.config import configure_toml_settings, get_toml_settings 2 | from dj_toml_settings.toml_parser import Parser 3 | 4 | __all__ = [ 5 | "Parser", 6 | "configure_toml_settings", 7 | "get_toml_settings", 8 | ] 9 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | import? 'adamghill.justfile' 2 | import? '../dotfiles/just/justfile' 3 | 4 | src := "src/dj_toml_settings" 5 | 6 | # List commands 7 | _default: 8 | just --list --unsorted --justfile {{ justfile() }} --list-heading $'Available commands:\n' 9 | 10 | # Grab default `adamghill.justfile` from GitHub 11 | fetch: 12 | curl https://raw.githubusercontent.com/adamghill/dotfiles/master/just/justfile > adamghill.justfile 13 | -------------------------------------------------------------------------------- /tests/test_value_parsers/test_dict_parsers/test_dict_parser.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from dj_toml_settings.value_parsers.dict_parsers import DictParser 4 | 5 | 6 | class FakeParser(DictParser): 7 | pass 8 | 9 | 10 | class FakeParserWithKey(DictParser): 11 | key = "test" 12 | 13 | 14 | def test_missing_key(): 15 | with pytest.raises(NotImplementedError) as e: 16 | FakeParser({}, {}) 17 | 18 | assert "Missing key attribute" in e.exconly() 19 | 20 | 21 | def test_missing_parse(): 22 | with pytest.raises(NotImplementedError) as e: 23 | FakeParserWithKey({}, {}).parse() 24 | 25 | assert "parse() not implemented" in e.exconly() 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: ["published"] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | pypi-publish: 13 | name: Upload release to PyPI 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: pypi 18 | url: https://pypi.org/project/dj-toml-settings/ 19 | 20 | permissions: 21 | id-token: write 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Install uv 27 | uses: astral-sh/setup-uv@v3 28 | with: 29 | enable-cache: true 30 | 31 | - name: Set up Python 32 | run: uv python install 3.10 33 | 34 | - name: Build 35 | run: uv build 36 | 37 | # - name: Publish package distributions to Test PyPI 38 | # uses: pypa/gh-action-pypi-publish@release/v1 39 | # with: 40 | # repository-url: https://test.pypi.org/legacy/ 41 | # skip-existing: true 42 | 43 | - name: Publish package distributions to PyPI 44 | uses: pypa/gh-action-pypi-publish@release/v1 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Ceterai 4 | Copyright (c) 2025 adamghill 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | pytest: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: 13 | - "3.10" 14 | - "3.11" 15 | - "3.12" 16 | - "3.13" 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Install uv 22 | uses: astral-sh/setup-uv@v3 23 | with: 24 | enable-cache: true 25 | 26 | - name: Set up Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | 31 | - name: Install the package 32 | run: uv sync --all-extras --dev 33 | 34 | - name: Run pytest 35 | run: uv run pytest 36 | 37 | ruff: 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: astral-sh/ruff-action@v1 43 | 44 | mypy: 45 | runs-on: ubuntu-latest 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | 50 | - name: Install uv 51 | uses: astral-sh/setup-uv@v3 52 | with: 53 | enable-cache: true 54 | 55 | - name: Set up Python 56 | uses: actions/setup-python@v5 57 | 58 | - name: Install the package 59 | run: uv sync --all-extras --dev 60 | 61 | - name: Run mypy 62 | run: uv run mypy src/dj_toml_settings 63 | -------------------------------------------------------------------------------- /src/dj_toml_settings/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from typeguard import typechecked 4 | 5 | from dj_toml_settings.toml_parser import Parser 6 | 7 | TOML_SETTINGS_FILES = ["pyproject.toml", "django.toml"] 8 | 9 | 10 | @typechecked 11 | def get_toml_settings(base_dir: Path, data: dict | None = None, toml_settings_files: list[str] | None = None) -> dict: 12 | """Gets the Django settings from the TOML files. 13 | 14 | TOML files to look in for settings: 15 | - pyproject.toml 16 | - django.toml 17 | """ 18 | 19 | toml_settings_files = toml_settings_files or TOML_SETTINGS_FILES 20 | data = data or {} 21 | 22 | for settings_file_name in toml_settings_files: 23 | settings_path = base_dir / settings_file_name 24 | 25 | if settings_path.exists(): 26 | file_data = Parser(settings_path, data=data.copy()).parse_file() 27 | data.update(file_data) 28 | 29 | return data 30 | 31 | 32 | @typechecked 33 | def configure_toml_settings(base_dir: Path, data: dict) -> None: 34 | """Configure Django settings from TOML files. 35 | 36 | Args: 37 | base_dir: Base directory to look for TOML files 38 | data: Dictionary to update with settings from TOML files 39 | 40 | Returns: 41 | The updated dictionary with settings from TOML files 42 | """ 43 | 44 | toml_settings = get_toml_settings(base_dir, data) 45 | data.update(toml_settings) 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.6.0-dev 4 | 5 | - Support for "path" casting for `$type`. 6 | 7 | ## 0.5.0 8 | 9 | - Move from `toml` to `tomli` for TOML parsing to allow using `tomllib` in standard library for Python > 3.11. 10 | 11 | ### Breaking changes 12 | 13 | - Remove custom prefix or suffixes for special operators. Everything should start with "$" to reduce code and unnecessary complications. 14 | 15 | ## 0.4.0 16 | 17 | - Add `$value` operator. 18 | - Add `$type` operator with support for `bool`, `int`, `str`, `float`, `decimal`, `datetime`, `date`, `time`, `timedelta`, `url`. 19 | 20 | ## 0.3.1 21 | 22 | - Fix `$index`. 23 | 24 | ## 0.3.0 25 | 26 | - Handle `datetime`, `int`, `float`, `list`, `dict`, `datetime`, and `Callable` as variables. 27 | - Handle appending to a variable for `Path`. 28 | - Add `$none` special operator. 29 | 30 | ### Breaking changes 31 | 32 | - Start special operations with "$" to reduce key conflicts. Can be configured as "" to replicate 0.2.0. 33 | 34 | ## 0.2.0 35 | 36 | - Better support for using variables which are a `Path`. 37 | 38 | ## 0.1.0 39 | 40 | - Load `pyproject.toml` and `django.toml` files and parse them. 41 | - Load settings from `tool.django.apps.*` sections. 42 | - Load settings from `tool.django.envs.*` sections if it matches `ENVIRONMENT` environment variable. 43 | - Retrieve value from environment variable, e.g. `{ env = "ENVIRONMENT_VARIABLE_NAME" }`. 44 | - Retrieve value from previous settings, e.g. `VARIABLE_NAME = ${PREVIOUS_VARIABLE_NAME}`. 45 | - Parse strings into a `Path` object, e.g. `{ path = "." }`. 46 | - Insert value into arrays, e.g. `ALLOWED_HOSTS = { insert = "127.0.0.1" }`. 47 | - Integrations for Django, `nanodjango`, `coltrane`. 48 | -------------------------------------------------------------------------------- /tests/test_config/test_configure_toml_settings.py: -------------------------------------------------------------------------------- 1 | from dj_toml_settings.config import configure_toml_settings 2 | 3 | 4 | def test(tmp_path, monkeypatch): 5 | monkeypatch.setattr( 6 | "dj_toml_settings.config.get_toml_settings", 7 | lambda *_: {}, 8 | ) 9 | 10 | expected = {} 11 | 12 | actual = {} 13 | configure_toml_settings(base_dir=tmp_path, data=actual) 14 | 15 | assert expected == actual 16 | 17 | 18 | def test_toml_settings(tmp_path, monkeypatch): 19 | monkeypatch.setattr( 20 | "dj_toml_settings.config.get_toml_settings", 21 | lambda *_: {"ALLOWED_HOSTS": ["127.0.0.1"]}, 22 | ) 23 | 24 | expected = {"ALLOWED_HOSTS": ["127.0.0.1"]} 25 | 26 | actual = {} 27 | configure_toml_settings(base_dir=tmp_path, data=actual) 28 | 29 | assert expected == actual 30 | 31 | 32 | def test_existing_settings(tmp_path): 33 | expected = {"DEBUG": True, "ALLOWED_HOSTS": ["127.0.0.1"]} 34 | 35 | (tmp_path / "pyproject.toml").write_text(""" 36 | [tool.django] 37 | DEBUG = true 38 | """) 39 | 40 | actual = {"ALLOWED_HOSTS": ["127.0.0.1"]} 41 | configure_toml_settings(base_dir=tmp_path, data=actual) 42 | 43 | assert expected == actual 44 | 45 | 46 | def test_empty_existing_settings_empty(tmp_path): 47 | expected = {"DEBUG": True} 48 | 49 | (tmp_path / "pyproject.toml").write_text(""" 50 | [tool.django] 51 | DEBUG = true 52 | """) 53 | 54 | actual = {} 55 | configure_toml_settings(base_dir=tmp_path, data=actual) 56 | 57 | assert expected == actual 58 | 59 | 60 | def test_override_existing_settings(tmp_path): 61 | expected = {"DEBUG": True} 62 | 63 | (tmp_path / "pyproject.toml").write_text(""" 64 | [tool.django] 65 | DEBUG = true 66 | """) 67 | 68 | actual = {"DEBUG": False} 69 | configure_toml_settings(base_dir=tmp_path, data=actual) 70 | 71 | assert expected == actual 72 | 73 | 74 | def test_override_toml_settings(tmp_path): 75 | expected = {"ALLOWED_HOSTS": ["127.0.0.3"]} 76 | 77 | (tmp_path / "pyproject.toml").write_text(""" 78 | [tool.django] 79 | ALLOWED_HOSTS = ["127.0.0.2"] 80 | """) 81 | 82 | (tmp_path / "django.toml").write_text(""" 83 | [tool.django] 84 | ALLOWED_HOSTS = ["127.0.0.3"] 85 | """) 86 | 87 | actual = {"ALLOWED_HOSTS": ["127.0.0.1"]} 88 | configure_toml_settings(base_dir=tmp_path, data=actual) 89 | 90 | assert expected == actual 91 | -------------------------------------------------------------------------------- /src/dj_toml_settings/value_parsers/str_parsers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from datetime import datetime 4 | from pathlib import Path 5 | from typing import Any 6 | 7 | from dateutil import parser as dateparser 8 | from typeguard import typechecked 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class VariableParser: 14 | data: dict 15 | value: str 16 | 17 | def __init__(self, data: dict, value: str): 18 | self.data = data 19 | self.value = value 20 | 21 | def parse(self) -> Any: 22 | value: Any = self.value 23 | 24 | for match in re.finditer(r"\$\{\w+\}", value): 25 | data_key = value[match.start() : match.end()][2:-1] 26 | 27 | if variable := self.data.get(data_key): 28 | if isinstance(variable, Path): 29 | path_str = combine_bookends(value, match, variable) 30 | 31 | value = Path(path_str) 32 | elif callable(variable): 33 | value = variable 34 | elif isinstance(variable, int): 35 | value = combine_bookends(value, match, variable) 36 | 37 | try: 38 | value = int(value) 39 | except Exception: # noqa: S110 40 | pass 41 | elif isinstance(variable, float): 42 | value = combine_bookends(value, match, variable) 43 | 44 | try: 45 | value = float(value) 46 | except Exception: # noqa: S110 47 | pass 48 | elif isinstance(variable, list): 49 | value = variable 50 | elif isinstance(variable, dict): 51 | value = variable 52 | elif isinstance(variable, datetime): 53 | value = dateparser.isoparse(str(variable)) 54 | else: 55 | value = value.replace(match.string, str(variable)) 56 | else: 57 | logger.warning(f"Missing variable substitution {value}") 58 | 59 | return value 60 | 61 | 62 | @typechecked 63 | def combine_bookends(original: str, match: re.Match, middle: Any) -> str: 64 | """Get the beginning of the original string before the match, and the 65 | end of the string after the match and smush the replaced value in between 66 | them to generate a new string. 67 | """ 68 | 69 | start_idx = match.start() 70 | start = original[:start_idx] 71 | 72 | end_idx = match.end() 73 | ending = original[end_idx:] 74 | 75 | return start + str(middle) + ending 76 | -------------------------------------------------------------------------------- /tests/test_config/test_get_toml_settings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from dj_toml_settings.config import get_toml_settings 4 | 5 | 6 | def test(tmp_path): 7 | expected = {} 8 | 9 | actual = get_toml_settings(base_dir=tmp_path) 10 | 11 | assert expected == actual 12 | 13 | 14 | def test_pyproject_only(tmp_path): 15 | expected = {"DEBUG": True, "SECRET_KEY": "test-secret"} 16 | 17 | (tmp_path / "pyproject.toml").write_text(""" 18 | [tool.django] 19 | DEBUG = true 20 | SECRET_KEY = "test-secret" 21 | """) 22 | 23 | actual = get_toml_settings(base_dir=tmp_path) 24 | 25 | assert expected == actual 26 | 27 | 28 | def test_django_only(tmp_path): 29 | expected = { 30 | "DATABASES": {"default": {"ENGINE": "django.db.backends.sqlite3"}}, 31 | "ALLOWED_HOSTS": ["example.com"], 32 | } 33 | 34 | (tmp_path / "django.toml").write_text(""" 35 | [tool.django] 36 | DATABASES = { default = { ENGINE = "django.db.backends.sqlite3" } } 37 | ALLOWED_HOSTS = ["example.com"] 38 | """) 39 | 40 | actual = get_toml_settings(base_dir=tmp_path) 41 | 42 | assert expected == actual 43 | 44 | 45 | def test_both_files(tmp_path): 46 | expected = { 47 | "DEBUG": False, # Should be overridden by django.toml 48 | "SECRET_KEY": "test-secret", 49 | "DATABASES": {"default": {"ENGINE": "django.db.backends.sqlite3"}}, 50 | } 51 | 52 | (tmp_path / "pyproject.toml").write_text(""" 53 | [tool.django] 54 | DEBUG = true 55 | SECRET_KEY = "test-secret" 56 | """) 57 | 58 | (tmp_path / "django.toml").write_text(""" 59 | [tool.django] 60 | DATABASES = {default = {ENGINE = "django.db.backends.sqlite3"}} 61 | DEBUG = false 62 | """) 63 | 64 | actual = get_toml_settings(base_dir=tmp_path) 65 | 66 | assert expected == actual 67 | 68 | 69 | def test_invalid_toml(tmp_path, caplog): 70 | expected = {} 71 | 72 | (tmp_path / "pyproject.toml").write_text("this is not valid TOML") 73 | 74 | with caplog.at_level(logging.ERROR): 75 | actual = get_toml_settings(base_dir=tmp_path) 76 | 77 | assert expected == actual 78 | assert "Cannot parse TOML at: " in caplog.text 79 | 80 | 81 | def test_empty_file(tmp_path): 82 | expected = {} 83 | 84 | (tmp_path / "pyproject.toml").write_text("") 85 | 86 | actual = get_toml_settings(base_dir=tmp_path) 87 | 88 | assert expected == actual 89 | 90 | 91 | def test_no_files(tmp_path): 92 | expected = {} 93 | 94 | actual = get_toml_settings(base_dir=tmp_path) 95 | 96 | assert expected == actual 97 | 98 | 99 | def test_data(tmp_path): 100 | expected = {"DEBUG": False} 101 | 102 | actual = get_toml_settings(base_dir=tmp_path, data={"DEBUG": False}) 103 | 104 | assert expected == actual 105 | 106 | 107 | def test_data_is_none(tmp_path): 108 | expected = {} 109 | 110 | actual = get_toml_settings(base_dir=tmp_path, data=None) 111 | 112 | assert expected == actual 113 | 114 | 115 | def test_specified_file(tmp_path): 116 | expected = { 117 | "DEBUG": True, 118 | } 119 | 120 | (tmp_path / "blob.toml").write_text(""" 121 | [tool.django] 122 | DEBUG = true 123 | """) 124 | 125 | actual = get_toml_settings(base_dir=tmp_path, toml_settings_files=["blob.toml"]) 126 | 127 | assert expected == actual 128 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "dj-toml-settings" 3 | version = "0.5.0" 4 | description = "Load Django settings from a TOML file" 5 | authors = [ 6 | { name = "Adam Hill", email = "adam@adamghill.com" }, 7 | ] 8 | readme = "README.md" 9 | requires-python = ">=3.10" 10 | license = {file = "LICENSE"} 11 | keywords = [ 12 | "django", 13 | "web", 14 | "toml", 15 | ] 16 | classifiers = [ 17 | "Development Status :: 3 - Alpha", 18 | "Framework :: Django", 19 | "Framework :: Django :: 3", 20 | "Framework :: Django :: 4", 21 | "Framework :: Django :: 5", 22 | "Intended Audience :: Developers", 23 | "License :: OSI Approved :: MIT License", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Topic :: Software Development :: Libraries :: Python Modules", 27 | ] 28 | dependencies = [ 29 | "python-dateutil>=2.9.0.post0", 30 | "tomli>=1.1.0; python_version < '3.11'", 31 | "typeguard>=2", 32 | ] 33 | 34 | [tool.uv] 35 | dev-dependencies = [ 36 | "pytest < 9", 37 | "pytest-django >= 0", 38 | "pytest-cov >= 0", 39 | "mypy >= 0", 40 | "types-toml >= 0", 41 | "types-python-dateutil >= 0", 42 | ] 43 | 44 | [project.urls] 45 | homepage = "https://github.com/adamghill/dj-toml-settings/" 46 | repository = "https://github.com/adamghill/dj-toml-settings.git" 47 | 48 | [tool.ruff] 49 | src = ["src"] 50 | exclude = [] 51 | target-version = "py310" 52 | line-length = 120 53 | lint.select = [ 54 | "A", 55 | "ARG", 56 | "B", 57 | "C", 58 | "DTZ", 59 | "E", 60 | "EM", 61 | "F", 62 | "FBT", 63 | "I", 64 | "ICN", 65 | "ISC", 66 | "N", 67 | "PLC", 68 | "PLE", 69 | "PLR", 70 | "PLW", 71 | "Q", 72 | "RUF", 73 | "S", 74 | "T", 75 | "TID", 76 | "UP", 77 | "W", 78 | "YTT", 79 | ] 80 | lint.ignore = [ 81 | # Allow non-abstract empty methods in abstract base classes 82 | "B027", 83 | # Allow boolean positional values in function calls, like `dict.get(... True)` 84 | "FBT003", 85 | # Ignore checks for possible passwords 86 | "S105", "S106", "S107", 87 | # Ignore complexity 88 | "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", 89 | # Ignore unused variables 90 | "F841", 91 | # Ignore exception strings 92 | "EM101", "EM102", 93 | ] 94 | lint.unfixable = [ 95 | # Don't touch unused imports 96 | "F401", 97 | ] 98 | 99 | [tool.ruff.lint.pydocstyle] 100 | convention = "google" 101 | 102 | [tool.ruff.lint.isort] 103 | known-first-party = ["dj_toml_settings"] 104 | 105 | [tool.ruff.lint.flake8-tidy-imports] 106 | ban-relative-imports = "all" 107 | 108 | [tool.ruff.lint.per-file-ignores] 109 | # Tests can use magic values, assertions, and relative imports 110 | "tests/**/*" = ["PLR2004", "S101", "TID252", "ARG001"] 111 | 112 | [tool.pytest.ini_options] 113 | addopts = "--quiet --failed-first -p no:warnings --no-migrations -m \"not slow\"" 114 | testpaths = [ 115 | "tests" 116 | ] 117 | markers = [ 118 | "slow: marks tests as slow", 119 | ] 120 | 121 | [tool.coverage.run] 122 | branch = true 123 | parallel = true 124 | 125 | [tool.coverage.report] 126 | show_missing = true 127 | skip_covered = true 128 | skip_empty = true 129 | sort = "cover" 130 | 131 | [tool.mypy] 132 | python_version = "3.10" 133 | warn_return_any = true 134 | warn_unused_configs = true 135 | ignore_missing_imports = true 136 | 137 | [build-system] 138 | requires = ["uv_build>=0.8.12,<0.9.0"] 139 | build-backend = "uv_build" 140 | -------------------------------------------------------------------------------- /src/dj_toml_settings/toml_parser.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from datetime import datetime 5 | from pathlib import Path 6 | from typing import Any 7 | 8 | from dateutil import parser as dateparser 9 | from typeguard import typechecked 10 | 11 | if sys.version_info >= (3, 11): 12 | import tomllib 13 | else: 14 | import tomli as tomllib 15 | 16 | from dj_toml_settings.value_parsers.dict_parsers import ( 17 | EnvParser, 18 | InsertParser, 19 | NoneParser, 20 | PathParser, 21 | TypeParser, 22 | ValueParser, 23 | ) 24 | from dj_toml_settings.value_parsers.str_parsers import VariableParser 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | class Parser: 30 | path: Path 31 | data: dict 32 | 33 | def __init__(self, path: Path, data: dict | None = None): 34 | self.path = path 35 | self.data = data or {} 36 | 37 | @typechecked 38 | def parse_file(self): 39 | """Parse data from the specified TOML file to use for Django settings. 40 | 41 | The sections get parsed in the following order with the later sections overriding the earlier: 42 | 1. `[tool.django]` 43 | 2. `[tool.django.apps.*]` 44 | 3. `[tool.django.envs.{ENVIRONMENT}]` where {ENVIRONMENT} is defined in the `ENVIRONMENT` env variable 45 | """ 46 | 47 | toml_data = self.get_data() 48 | 49 | # Get potential settings from `tool.django.apps` and `tool.django.envs` 50 | apps_data = toml_data.pop("apps", {}) 51 | envs_data = toml_data.pop("envs", {}) 52 | 53 | # Add default settings from `tool.django` 54 | for key, value in toml_data.items(): 55 | logger.debug(f"tool.django: Update '{key}' with '{value}'") 56 | 57 | self.data.update({key: self.parse_value(key, value)}) 58 | 59 | # Add settings from `tool.django.apps.*` 60 | for apps_name, apps_value in apps_data.items(): 61 | for app_key, app_value in apps_value.items(): 62 | logger.debug(f"tool.django.apps.{apps_name}: Update '{app_key}' with '{app_value}'") 63 | 64 | self.data.update({app_key: self.parse_value(app_key, app_value)}) 65 | 66 | # Add settings from `tool.django.envs.*` if it matches the `ENVIRONMENT` env variable 67 | if environment_env_variable := os.getenv("ENVIRONMENT"): 68 | for envs_name, envs_value in envs_data.items(): 69 | if environment_env_variable == envs_name: 70 | for env_key, env_value in envs_value.items(): 71 | logger.debug(f"tool.django.envs.{envs_name}: Update '{env_key}' with '{env_value}'") 72 | 73 | self.data.update({env_key: self.parse_value(env_key, env_value)}) 74 | 75 | return self.data 76 | 77 | @typechecked 78 | def get_data(self) -> dict: 79 | """Gets the data from the passed-in TOML file.""" 80 | 81 | data = {} 82 | 83 | try: 84 | with open(self.path, "rb") as f: 85 | data = tomllib.load(f) 86 | except FileNotFoundError: 87 | logger.warning(f"Cannot find file at: {self.path}") 88 | except tomllib.TOMLDecodeError: 89 | logger.error(f"Cannot parse TOML at: {self.path}") 90 | 91 | return data.get("tool", {}).get("django", {}) or {} 92 | 93 | @typechecked 94 | def parse_value(self, key: Any, value: Any) -> Any: 95 | """Handle special cases for `value`. 96 | 97 | Special cases: 98 | - `dict` keys 99 | - `$env`: retrieves an environment variable; optional `default` argument 100 | - `$path`: converts string to a `Path`; handles relative path 101 | - `$insert`: inserts the value to an array; optional `index` argument 102 | - `$none`: inserts the `None` value 103 | - `$value`: literal value 104 | - `$type`: casts the value to a particular type 105 | - variables in `str` 106 | - `datetime` 107 | """ 108 | 109 | if isinstance(value, list): 110 | # Process each item in the list 111 | processed_list = [] 112 | 113 | for item in value: 114 | processed_item = self.parse_value(key, item) 115 | processed_list.append(processed_item) 116 | 117 | value = processed_list 118 | elif isinstance(value, dict): 119 | # Process nested dictionaries 120 | processed_dict = {} 121 | 122 | for k, v in value.items(): 123 | if isinstance(v, dict): 124 | processed_dict.update({k: self.parse_value(key, v)}) 125 | else: 126 | processed_dict[k] = v 127 | 128 | value = processed_dict 129 | 130 | type_parser = TypeParser(data=self.data, value=value) 131 | env_parser = EnvParser(data=self.data, value=value) 132 | path_parser = PathParser(data=self.data, value=value, path=self.path) 133 | value_parser = ValueParser(data=self.data, value=value) 134 | none_parser = NoneParser(data=self.data, value=value) 135 | insert_parser = InsertParser(data=self.data, value=value, data_key=key) 136 | 137 | # Check for a match for all operators (except $type) 138 | for parser in [env_parser, path_parser, value_parser, insert_parser, none_parser]: 139 | if parser.match(): 140 | value = parser.parse() 141 | break 142 | 143 | # Parse $type last because it can operate on the resolved value from the other parsers 144 | if type_parser.match(): 145 | value = type_parser.parse(value) 146 | elif isinstance(value, str): 147 | value = VariableParser(data=self.data, value=value).parse() 148 | elif isinstance(value, datetime): 149 | value = dateparser.isoparse(str(value)) 150 | 151 | return value 152 | -------------------------------------------------------------------------------- /tests/test_value_parsers/test_dict_parsers/test_type_parser.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime, time, timedelta 2 | from decimal import Decimal 3 | from os import getcwd 4 | from pathlib import Path 5 | from urllib.parse import ParseResult 6 | 7 | import pytest 8 | 9 | from dj_toml_settings.value_parsers.dict_parsers import TypeParser 10 | 11 | 12 | def test_bool(): 13 | parser = TypeParser(data={}, value={"$type": "bool"}) 14 | 15 | # Test string conversions 16 | assert parser.parse("true") is True 17 | assert parser.parse("True") is True 18 | assert parser.parse("false") is False 19 | assert parser.parse("False") is False 20 | 21 | # Test integer conversions 22 | assert parser.parse(1) is True 23 | assert parser.parse(0) is False 24 | 25 | # Test already boolean 26 | assert parser.parse(True) is True 27 | assert parser.parse(False) is False 28 | 29 | with pytest.raises(ValueError) as e: 30 | assert parser.parse(1.1) 31 | 32 | assert "ValueError: Failed to convert 1.1 to bool: Type must be a string or int, got float" in e.exconly() 33 | 34 | 35 | def test_int(): 36 | parser = TypeParser(data={}, value={"$type": "int"}) 37 | 38 | # Test string to int 39 | assert parser.parse("42") == 42 40 | assert parser.parse("-10") == -10 41 | 42 | # Test float to int (should truncate) 43 | assert parser.parse(3.14) == 3 44 | 45 | # Test already int 46 | assert parser.parse(100) == 100 47 | 48 | 49 | def test_str(): 50 | parser = TypeParser(data={}, value={"$type": "str"}) 51 | 52 | # Test various types to string 53 | assert parser.parse(42) == "42" 54 | assert parser.parse(3.14) == "3.14" 55 | assert parser.parse(True) == "True" 56 | assert parser.parse(None) == "None" 57 | assert parser.parse("hello") == "hello" 58 | 59 | 60 | def test_float(): 61 | parser = TypeParser(data={}, value={"$type": "float"}) 62 | 63 | # Test string to float 64 | assert parser.parse("3.14") == 3.14 65 | assert parser.parse("-1.5") == -1.5 66 | 67 | # Test int to float 68 | assert parser.parse(42) == 42.0 69 | 70 | # Test already float 71 | assert parser.parse(3.14) == 3.14 72 | 73 | 74 | def test_decimal(): 75 | parser = TypeParser(data={}, value={"$type": "decimal"}) 76 | 77 | # Test string to Decimal 78 | assert parser.parse("3.14") == Decimal("3.14") 79 | assert parser.parse("-1.5") == Decimal("-1.5") 80 | 81 | # Test int to Decimal 82 | assert parser.parse(42) == Decimal(42) 83 | 84 | # Test float to Decimal (note: float imprecision might occur) 85 | assert float(parser.parse(3.14)) == 3.14 86 | 87 | 88 | def test_path(): 89 | expected = getcwd() 90 | 91 | parser = TypeParser(data={}, value={"$type": "path"}) 92 | actual = parser.parse(".") 93 | 94 | assert isinstance(actual, Path) 95 | assert expected == str(actual) 96 | 97 | with pytest.raises(ValueError) as e: 98 | parser.parse(1) 99 | 100 | assert "ValueError: Failed to convert 1 to path: expected str, bytes or os.PathLike object, not int" in e.exconly() 101 | 102 | 103 | def test_datetime(): 104 | parser = TypeParser(data={}, value={"$type": "datetime"}) 105 | 106 | actual = parser.parse("2023-01-01 12:00:00") 107 | assert isinstance(actual, datetime) 108 | assert actual.year == 2023 109 | assert actual.month == 1 110 | assert actual.day == 1 111 | assert actual.hour == 12 112 | 113 | 114 | def test_datetime_invalid(): 115 | parser = TypeParser(data={}, value={"$type": "datetime"}) 116 | 117 | with pytest.raises(ValueError) as e: 118 | parser.parse("abcd") 119 | 120 | assert "Failed to convert 'abcd' to datetime" in e.exconly() 121 | 122 | 123 | def test_date(): 124 | parser = TypeParser(data={}, value={"$type": "date"}) 125 | 126 | actual = parser.parse("2023-01-01") 127 | 128 | assert isinstance(actual, date) 129 | assert actual.year == 2023 130 | assert actual.month == 1 131 | assert actual.day == 1 132 | 133 | 134 | def test_time(): 135 | parser = TypeParser(data={}, value={"$type": "time"}) 136 | 137 | actual = parser.parse("12:30:45") 138 | 139 | assert isinstance(actual, time) 140 | assert actual.hour == 12 141 | assert actual.minute == 30 142 | assert actual.second == 45 143 | 144 | 145 | def test_timedelta(): 146 | parser = TypeParser(data={}, value={"$type": "timedelta"}) 147 | 148 | # Test numeric values (treated as seconds) 149 | assert parser.parse(60) == timedelta(seconds=60) 150 | assert parser.parse(90.5) == timedelta(seconds=90.5) 151 | 152 | # Test string formats 153 | assert parser.parse("30s") == timedelta(seconds=30) 154 | assert parser.parse("2m") == timedelta(minutes=2) 155 | assert parser.parse("1.5h") == timedelta(hours=1.5) 156 | assert parser.parse("2d") == timedelta(days=2) 157 | assert parser.parse("1w") == timedelta(weeks=1) 158 | 159 | assert parser.parse("1w 2d 3h 4m 5s 6ms 7u") == timedelta( 160 | weeks=1, days=2, hours=3, minutes=4, seconds=5, milliseconds=6, microseconds=7 161 | ) 162 | 163 | assert parser.parse("1w2d") == timedelta(weeks=1, days=2) 164 | 165 | with pytest.raises(ValueError) as e: 166 | parser.parse({}) 167 | 168 | assert "ValueError: Failed to convert {} to timedelta: Unsupported type for timedelta: dict" in e.exconly() 169 | 170 | with pytest.raises(ValueError) as e: 171 | parser.parse("abcd") 172 | 173 | assert "ValueError: Failed to convert 'abcd' to timedelta: Invalid timedelta format: abcd" in e.exconly() 174 | 175 | with pytest.raises(ValueError) as e: 176 | parser.parse("4z") 177 | 178 | assert "alueError: Failed to convert '4z' to timedelta: Invalid timedelta format: 4z" in e.exconly() 179 | 180 | 181 | def test_url(): 182 | parser = TypeParser(data={}, value={"$type": "url"}) 183 | 184 | actual = parser.parse("https://example.com/path?query=1") 185 | 186 | assert isinstance(actual, ParseResult) 187 | assert actual.scheme == "https" 188 | assert actual.netloc == "example.com" 189 | assert actual.path == "/path" 190 | assert actual.query == "query=1" 191 | 192 | 193 | def test_invalid_type(): 194 | parser = TypeParser(data={}, value={"$type": "invalid_type"}) 195 | 196 | with pytest.raises(ValueError) as e: 197 | parser.parse("some value") 198 | 199 | assert "Unsupported type: invalid_type" in e.exconly() 200 | 201 | 202 | def test_invalid_type_type(): 203 | parser = TypeParser(data={}, value={"$type": 1}) 204 | 205 | with pytest.raises(ValueError) as e: 206 | parser.parse("some value") 207 | 208 | assert "Type must be a string, got int" in e.exconly() 209 | -------------------------------------------------------------------------------- /src/dj_toml_settings/value_parsers/dict_parsers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | from datetime import timedelta 5 | from decimal import Decimal 6 | from pathlib import Path 7 | from typing import Any 8 | from urllib.parse import urlparse 9 | 10 | from dateutil import parser as dateparser 11 | from typeguard import typechecked 12 | 13 | from dj_toml_settings.exceptions import InvalidActionError 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class DictParser: 19 | data: dict 20 | value: dict 21 | key: str 22 | 23 | def __init__(self, data: dict, value: dict): 24 | self.data = data 25 | self.value = value 26 | 27 | if not hasattr(self, "key"): 28 | raise NotImplementedError("Missing key attribute") 29 | 30 | self.key = self.add_prefix_to_key(self.key) 31 | 32 | def match(self) -> bool: 33 | return self.key in self.value 34 | 35 | @typechecked 36 | def add_prefix_to_key(self, key: str) -> str: 37 | """Gets the key for the special operator.""" 38 | 39 | return f"${key}" 40 | 41 | def parse(self, *args, **kwargs): 42 | raise NotImplementedError("parse() not implemented") 43 | 44 | 45 | class EnvParser(DictParser): 46 | key: str = "env" 47 | 48 | def parse(self) -> Any: 49 | default_special_key = self.add_prefix_to_key("default") 50 | default_value = self.value.get(default_special_key) 51 | 52 | env_value = self.value[self.key] 53 | value = os.getenv(env_value, default_value) 54 | 55 | return value 56 | 57 | 58 | class PathParser(DictParser): 59 | key: str = "path" 60 | 61 | def __init__(self, data: dict, value: dict, path: Path): 62 | super().__init__(data, value) 63 | self.path = path 64 | 65 | def parse(self) -> Any: 66 | self.file_name = self.value[self.key] 67 | value = self.resolve_file_name() 68 | 69 | return value 70 | 71 | @typechecked 72 | def resolve_file_name(self) -> Path: 73 | """Parse a path string relative to a base path. 74 | 75 | Args: 76 | file_name: Relative or absolute file name. 77 | path: Base path to resolve file_name against. 78 | """ 79 | 80 | current_path = Path(self.path).parent if self.path.is_file() else self.path 81 | 82 | return Path((current_path / self.file_name).resolve()) 83 | 84 | 85 | class ValueParser(DictParser): 86 | key = "value" 87 | 88 | def parse(self) -> Any: 89 | return self.value[self.key] 90 | 91 | 92 | class InsertParser(DictParser): 93 | key = "insert" 94 | 95 | def __init__(self, data: dict, value: dict, data_key: str): 96 | super().__init__(data, value) 97 | self.data_key = data_key 98 | 99 | def parse(self) -> Any: 100 | insert_data = self.data.get(self.data_key, []) 101 | 102 | # Check the existing value is an array 103 | if not isinstance(insert_data, list): 104 | raise InvalidActionError(f"`insert` cannot be used for value of type: {type(self.data[self.data_key])}") 105 | 106 | # Insert the data 107 | index_key = self.add_prefix_to_key("index") 108 | index = self.value.get(index_key, len(insert_data)) 109 | 110 | insert_data.insert(index, self.value[self.key]) 111 | 112 | return insert_data 113 | 114 | 115 | class NoneParser(DictParser): 116 | key = "none" 117 | 118 | def match(self) -> bool: 119 | return super().match() and self.value.get(self.key) is not None 120 | 121 | def parse(self) -> Any: 122 | return None 123 | 124 | 125 | class TypeParser(DictParser): 126 | key = "type" 127 | 128 | def parse(self, resolved_value: Any) -> Any: 129 | value_type = self.value[self.key] 130 | 131 | if not isinstance(value_type, str): 132 | raise ValueError(f"Type must be a string, got {type(value_type).__name__}") 133 | 134 | try: 135 | if value_type == "bool": 136 | if isinstance(resolved_value, str): 137 | resolved_value = resolved_value.lower() == "true" 138 | elif isinstance(resolved_value, int): 139 | resolved_value = bool(resolved_value) 140 | else: 141 | raise ValueError(f"Type must be a string or int, got {type(resolved_value).__name__}") 142 | 143 | return bool(resolved_value) 144 | elif value_type == "int": 145 | return int(resolved_value) 146 | elif value_type == "str": 147 | return str(resolved_value) 148 | elif value_type == "float": 149 | return float(resolved_value) 150 | elif value_type == "decimal": 151 | return Decimal(str(resolved_value)) 152 | elif value_type == "datetime": 153 | return dateparser.parse(resolved_value) 154 | elif value_type == "date": 155 | result = dateparser.parse(resolved_value) 156 | 157 | return result.date() 158 | elif value_type == "time": 159 | result = dateparser.parse(resolved_value) 160 | 161 | return result.time() 162 | elif value_type == "timedelta": 163 | return parse_timedelta(resolved_value) 164 | elif value_type == "url": 165 | return urlparse(str(resolved_value)) 166 | elif value_type == "path": 167 | return Path(resolved_value).resolve() 168 | else: 169 | raise ValueError(f"Unsupported type: {value_type}") 170 | except (ValueError, TypeError, AttributeError) as e: 171 | logger.debug(f"Failed to convert {resolved_value!r} to {value_type}: {e}") 172 | 173 | raise ValueError(f"Failed to convert {resolved_value!r} to {value_type}: {e}") from e 174 | 175 | 176 | def parse_timedelta(value): 177 | if isinstance(value, int | float): 178 | return timedelta(seconds=value) 179 | elif not isinstance(value, str): 180 | raise ValueError(f"Unsupported type for timedelta: {type(value).__name__}") 181 | 182 | # Pattern to match both space-separated and combined formats like '7w2d' 183 | pattern = r"(?:\s*(\d+\.?\d*)([u|ms|s|m|h|d|w]+))" 184 | matches = re.findall(pattern, value, re.IGNORECASE) 185 | 186 | if not matches and value.strip(): 187 | raise ValueError(f"Invalid timedelta format: {value}") 188 | 189 | unit_map = { 190 | "u": "microseconds", 191 | "ms": "milliseconds", 192 | "s": "seconds", 193 | "m": "minutes", 194 | "h": "hours", 195 | "d": "days", 196 | "w": "weeks", 197 | } 198 | kwargs = {} 199 | 200 | for num_str, unit in matches: 201 | try: 202 | num = float(num_str) 203 | except ValueError as e: 204 | raise ValueError(f"Invalid number in timedelta: {num_str}") from e 205 | 206 | if unit not in unit_map: 207 | raise ValueError(f"Invalid time unit: {unit}") 208 | 209 | key = unit_map[unit] 210 | kwargs[key] = kwargs.get(key, 0) + num 211 | 212 | return timedelta(**kwargs) 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dj-toml-settings ⚙️ 2 | 3 | > Load Django settings from a TOML file 4 | 5 | `dj-toml-settings` reads settings from a TOML file. By default, both `pyproject.toml` and `django.toml` files are parsed for settings in the `[tool.django]` namespace. 6 | 7 | ```toml 8 | [tool.django] 9 | 10 | # Paths are relative to the TOML file (unless they are absolute) 11 | BASE_DIR = { "$path" = "." } 12 | STATIC_ROOT = { "$path" = "staticfiles" } 13 | 14 | # This sets the key based on the environment variable 15 | SECRET_KEY = { "$env" = "SECRET_KEY" } 16 | 17 | # This sets the key based on the environment variable, but has a fallback 18 | ADMIN_URL_PATH = { "$env" = "ADMIN_URL_PATH", "$default"="admin" } 19 | 20 | # Booleans, arrays, tables (dictionaries), integers, strings, floats, dates are all supported in TOML 21 | DEBUG = true 22 | ALLOWED_HOSTS = [ 23 | "127.0.0.1", 24 | ] 25 | 26 | # Values can be casted to a bool, int, str, float, decimal, datetime, date, time, timedelta, url, Path 27 | SITE_ID = { "$value" = "1", "$type" = "int" } 28 | 29 | # This is an implicit dictionary and equivalent to `COLTRANE = { TITLE = "Example blog" }` 30 | [tool.django.COLTRANE] 31 | TITLE = "Example blog" 32 | 33 | # Any name can be used under `apps` for organizational purposes 34 | [tool.django.apps.tailwind-cli] 35 | TAILWIND_CLI_USE_DAISY_UI = true 36 | TAILWIND_CLI_SRC_CSS = ".django_tailwind_cli/source.css" 37 | 38 | # These settings are included when the `ENVIRONMENT` environment variable is "development" 39 | [tool.django.envs.development] 40 | ALLOWED_HOSTS = { "$insert" = "example.localhost" } 41 | 42 | # These settings are included when the `ENVIRONMENT` environment variable is "production" 43 | [tool.django.envs.production] 44 | DEBUG = false 45 | ALLOWED_HOSTS = { "$insert" = "example.com" } 46 | ``` 47 | 48 | ## Features 🤩 49 | 50 | ### Variables 51 | 52 | Use `${SOME_VARIABLE_NAME}` to use an existing setting as a value. 53 | 54 | ```toml 55 | [tool.django] 56 | GOOD_IPS = ["127.0.0.1"] 57 | ALLOWED_HOSTS = "${GOOD_IPS}" # this needs to be quoted to be valid TOML, but will be converted into a `list` 58 | ``` 59 | 60 | ### Apps 61 | 62 | `[tool.django.apps.{ANY_NAME_HERE}]` sections of the TOML file can be used to group settings together. They can be named anything. They will override any settings in `[tool.django]`. 63 | 64 | ```toml 65 | [tool.django.apps.tailwind-cli] 66 | TAILWIND_CLI_USE_DAISY_UI = true 67 | TAILWIND_CLI_SRC_CSS = ".django_tailwind_cli/source.css" 68 | ``` 69 | 70 | ### Environments 71 | 72 | The `[tool.django.envs.{ENVIRONMENT_NAME}]` section of the TOML file will be used when `{ENVIRONMENT_NAME}` is set to the `ENVIRONMENT` environment variable. For example, `ENVIRONMENT=production python manage.py runserver` will load all settings in the `[tool.django.envs.production]` section. There settings will override any settings in `[tool.django.apps.*]` or `[tool.django]`. 73 | 74 | ```toml 75 | [tool.django] 76 | ALLOWED_HOSTS = ["127.0.0.1"] 77 | 78 | [tool.django.envs.development] 79 | ALLOWED_HOSTS = ["example.localhost"] 80 | 81 | [tool.django.envs.production] 82 | ALLOWED_HOSTS = ["example.com"] 83 | ``` 84 | 85 | ## Special operations 😎 86 | 87 | By default, special operations are denoted by an [`inline table`](https://toml.io/en/v1.0.0#inline-table), (aka a `dictionary`) with a key that starts with a `$`, e.g. `{ "$value" = "1" }`. 88 | 89 | ### Path 90 | 91 | Converts a string to a `Path` object by using a `$path` key. Handles relative paths based on the location of the parsed TOML file. 92 | 93 | ```toml 94 | [tool.django] 95 | BASE_DIR = { "$path" = "." } 96 | PROJECT_DIR = { "$path" = "./your_project_folder" } 97 | REPOSITORY_DIR = { "$path" = "./.." } 98 | ``` 99 | 100 | ### Environment Variable 101 | 102 | Retrieve variables from the environment by using an `$env` key. Specify an optional `$default` key for a fallback value. 103 | 104 | ```toml 105 | [tool.django] 106 | EMAIL_HOST_PASSWORD = { "$env" = "SECRET_PASSWORD" } 107 | SECRET_KEY = { "$env" = "SECRET_KEY", "$default" = "this-is-a-secret" } 108 | ``` 109 | 110 | ### Arrays 111 | 112 | Add items to an array by using the `$insert` key. 113 | 114 | ```toml 115 | [tool.django] 116 | ALLOWED_HOSTS = { "$insert" = "127.0.0.1" } 117 | ``` 118 | 119 | Specify the index of the new item with the `$index` key. 120 | 121 | ```toml 122 | [tool.django] 123 | ALLOWED_HOSTS = { "$insert" = "127.0.0.1", "$index" = 0 } 124 | ``` 125 | 126 | ### None 127 | 128 | Specify `None` for a variable with a `$none` key. The value must be truthy, i.e. `true` or 1 (even though the value won't get used). 129 | 130 | ```toml 131 | [tool.django] 132 | EMAIL_HOST_PASSWORD = { "$none" = 1 } 133 | ``` 134 | 135 | ### Value 136 | 137 | Specifies a value for a variable. 138 | 139 | ```toml 140 | [tool.django] 141 | SITE_ID = { "$value" = 1 } 142 | ``` 143 | 144 | ### Type 145 | 146 | Casts the value to a particular type. Supported types: `bool`, `int`, `str`, `float`, `decimal`, `datetime`, `date`, `time`, `timedelta`, `url`, and `Path`. Especially helpful for values that come from environment variables which are usually read in as strings. 147 | 148 | `$type` can be used as an additional operator with any other operator. 149 | 150 | ```toml 151 | [tool.django] 152 | SITE_ID = { "$env" = "SITE_ID", $type = "int" } 153 | ``` 154 | 155 | ```toml 156 | [tool.django] 157 | SITE_ID = { "$value" = "1", $type = "int" } 158 | ``` 159 | 160 | ## Example Integrations 💚 161 | 162 | ### Django 163 | 164 | This will override any variables defined in `settings.py` with settings from the TOML files. 165 | 166 | ```python 167 | # settings.py 168 | from pathlib import Path 169 | from dj_toml_settings import configure_toml_settings 170 | 171 | BASE_DIR = Path(__file__).resolve().parent.parent 172 | ... 173 | 174 | configure_toml_settings(base_dir=BASE_DIR, data=globals()) 175 | ``` 176 | 177 | ### [nanodjango](https://nanodjango.readthedocs.io) 178 | 179 | ```python 180 | # app.py 181 | from pathlib import Path 182 | from dj_toml_settings import get_toml_settings 183 | 184 | base_dir = Path(__file__).resolve().parent 185 | app = Django(**get_toml_settings(base_dir=base_dir)) 186 | 187 | ... 188 | ``` 189 | 190 | ### [coltrane](https://coltrane.adamghill.com) 191 | 192 | ```python 193 | # app.py 194 | from pathlib import Path 195 | from django.core.management import execute_from_command_line 196 | from dj_toml_settings import get_toml_settings 197 | from coltrane import initialize 198 | 199 | base_dir = Path(__file__).resolve().parent.parent 200 | wsgi = initialize(**get_toml_settings(base_dir=base_dir)) 201 | 202 | if __name__ == "__main__": 203 | execute_from_command_line() 204 | 205 | ... 206 | ``` 207 | 208 | ## Precedence 🔻 209 | 210 | This is the order that files and sections are parsed (by default). The later sections override the previous settings. 211 | 212 | 1. `pyproject.toml` -> `[tool.django]` 213 | 2. `pyproject.toml` -> `[tool.django.apps.*]` 214 | 3. `pyproject.toml` -> `[tool.django.envs.*]` that match `ENVIRONMENT` environment variable 215 | 4. `django.toml` -> `[tool.django]` 216 | 5. `django.toml` -> `[tool.django.apps.*]` 217 | 6. `django.toml` -> `[tool.django.envs.*]` that match `ENVIRONMENT` environment variable 218 | 219 | ## Specify a TOML file 🤓 220 | 221 | ```python 222 | from pathlib import Path 223 | from dj_toml_settings import get_toml_settings 224 | 225 | base_dir = Path(__file__).resolve().parent 226 | toml_settings = get_toml_settings(base_dir=base_dir, toml_settings_files=["custom-settings.toml"]) 227 | ... 228 | ``` 229 | 230 | ## Test 🧪 231 | 232 | - `uv install pip install -e .[dev]` 233 | - `just test` 234 | 235 | ## Inspiration 😍 236 | 237 | - [django-pyproject](https://github.com/Ceterai/django-pyproject) 238 | - [django-settings-toml](https://github.com/maxking/django-settings-toml) 239 | -------------------------------------------------------------------------------- /tests/test_toml_parser/test_parse_file.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime, timezone 3 | from pathlib import Path 4 | 5 | import pytest 6 | from dateutil import parser as dateparser 7 | 8 | from dj_toml_settings.exceptions import InvalidActionError 9 | from dj_toml_settings.toml_parser import Parser 10 | 11 | 12 | def test(tmp_path): 13 | expected = {"ALLOWED_HOSTS": ["127.0.0.1"]} 14 | 15 | path = tmp_path / "pyproject.toml" 16 | path.write_text(""" 17 | [tool.django] 18 | ALLOWED_HOSTS = [ 19 | "127.0.0.1", 20 | ] 21 | """) 22 | 23 | actual = Parser(path).parse_file() 24 | 25 | assert expected == actual 26 | 27 | 28 | def test_type_bool_true(tmp_path): 29 | expected = {"DEBUG": True} 30 | 31 | path = tmp_path / "pyproject.toml" 32 | path.write_text(""" 33 | [tool.django] 34 | DEBUG = { "$value" = "True", "$type" = "bool" } 35 | """) 36 | 37 | actual = Parser(path).parse_file() 38 | 39 | assert expected == actual 40 | 41 | 42 | def test_type_float(tmp_path): 43 | expected = {"FLOAT": float("1.5")} 44 | 45 | path = tmp_path / "pyproject.toml" 46 | path.write_text(""" 47 | [tool.django] 48 | FLOAT = { "$value" = "1.5", "$type" = "float" } 49 | """) 50 | 51 | actual = Parser(path).parse_file() 52 | 53 | assert expected == actual 54 | 55 | 56 | def test_data(tmp_path): 57 | expected = {"DEBUG": False, "ALLOWED_HOSTS": ["127.0.0.1"]} 58 | 59 | path = tmp_path / "pyproject.toml" 60 | path.write_text(""" 61 | [tool.django] 62 | ALLOWED_HOSTS = [ 63 | "127.0.0.1", 64 | ] 65 | """) 66 | data = {"DEBUG": False} 67 | 68 | actual = Parser(path, data=data).parse_file() 69 | 70 | assert expected == actual 71 | 72 | 73 | def test_data_updated(tmp_path): 74 | expected = {"DEBUG": True} 75 | 76 | path = tmp_path / "pyproject.toml" 77 | path.write_text(""" 78 | [tool.django] 79 | DEBUG = true 80 | """) 81 | data = {"DEBUG": False} 82 | 83 | actual = Parser(path, data=data).parse_file() 84 | 85 | assert expected == actual 86 | 87 | 88 | def test_apps(tmp_path): 89 | expected = {"ALLOWED_HOSTS": ["127.0.0.2"]} 90 | 91 | path = tmp_path / "pyproject.toml" 92 | path.write_text(""" 93 | [tool.django] 94 | ALLOWED_HOSTS = [ 95 | "127.0.0.1", 96 | ] 97 | 98 | [tool.django.apps.whatever] 99 | ALLOWED_HOSTS = [ 100 | "127.0.0.2", 101 | ] 102 | """) 103 | 104 | actual = Parser(path).parse_file() 105 | 106 | assert expected == actual 107 | 108 | 109 | def test_environment(tmp_path, monkeypatch): 110 | monkeypatch.setenv("ENVIRONMENT", "production") 111 | 112 | expected = {"ALLOWED_HOSTS": ["127.0.0.2"]} 113 | 114 | path = tmp_path / "pyproject.toml" 115 | path.write_text(""" 116 | [tool.django] 117 | ALLOWED_HOSTS = [ 118 | "127.0.0.1", 119 | ] 120 | 121 | [tool.django.envs.production] 122 | ALLOWED_HOSTS = [ 123 | "127.0.0.2", 124 | ] 125 | """) 126 | 127 | actual = Parser(path).parse_file() 128 | 129 | assert expected == actual 130 | 131 | 132 | def test_production_missing_env(tmp_path, monkeypatch): 133 | monkeypatch.setenv("ENVIRONMENT", "production") 134 | 135 | expected = {"ALLOWED_HOSTS": ["127.0.0.1"]} 136 | 137 | path = tmp_path / "pyproject.toml" 138 | path.write_text(""" 139 | [tool.django] 140 | ALLOWED_HOSTS = [ 141 | "127.0.0.1", 142 | ] 143 | 144 | [tool.django.envs.development] 145 | ALLOWED_HOSTS = [ 146 | "127.0.0.2", 147 | ] 148 | """) 149 | 150 | actual = Parser(path).parse_file() 151 | 152 | assert expected == actual 153 | 154 | 155 | def test_precedence(tmp_path, monkeypatch): 156 | monkeypatch.setenv("ENVIRONMENT", "development") 157 | 158 | expected = {"ALLOWED_HOSTS": ["127.0.0.3"]} 159 | 160 | path = tmp_path / "pyproject.toml" 161 | path.write_text(""" 162 | [tool.django] 163 | ALLOWED_HOSTS = [ 164 | "127.0.0.1", 165 | ] 166 | 167 | [tool.django.apps.whatever] 168 | ALLOWED_HOSTS = [ 169 | "127.0.0.2", 170 | ] 171 | 172 | [tool.django.envs.development] 173 | ALLOWED_HOSTS = [ 174 | "127.0.0.3", 175 | ] 176 | """) 177 | 178 | actual = Parser(path).parse_file() 179 | 180 | assert expected == actual 181 | 182 | 183 | def test_env(tmp_path, monkeypatch): 184 | monkeypatch.setenv("SOME_VAR", "blob") 185 | 186 | expected = {"SOMETHING": "blob"} 187 | 188 | path = tmp_path / "pyproject.toml" 189 | path.write_text(""" 190 | [tool.django] 191 | SOMETHING = { "$env" = "SOME_VAR" } 192 | """) 193 | 194 | actual = Parser(path).parse_file() 195 | 196 | assert expected == actual 197 | 198 | 199 | def test_env_in_nested_dict(tmp_path, monkeypatch): 200 | monkeypatch.setenv("DATABASE_ENGINE", "django.db.backends.sqlite3") 201 | 202 | expected = {"DATABASES": {"default": {"ENGINE": "django.db.backends.sqlite3"}}} 203 | 204 | path = tmp_path / "pyproject.toml" 205 | path.write_text(""" 206 | [tool.django.DATABASES] 207 | default = { ENGINE = { "$env" = "DATABASE_ENGINE", "$default" = "django.db.backends.postgresql" } } 208 | """) 209 | 210 | actual = Parser(path).parse_file() 211 | 212 | assert expected == actual 213 | 214 | 215 | def test_env_in_list(tmp_path, monkeypatch): 216 | monkeypatch.setenv("SOME_VAR", "blob") 217 | 218 | expected = {"SOMETHING": ["blob"]} 219 | 220 | path = tmp_path / "pyproject.toml" 221 | path.write_text(""" 222 | [tool.django] 223 | SOMETHING = [{ "$env" = "SOME_VAR"}] 224 | """) 225 | 226 | actual = Parser(path).parse_file() 227 | 228 | assert expected == actual 229 | 230 | 231 | def test_variable_in_list(tmp_path): 232 | expected = {"SOMETHING": ["blob"], "SOME_VAR": "blob"} 233 | 234 | path = tmp_path / "pyproject.toml" 235 | path.write_text(""" 236 | [tool.django] 237 | SOME_VAR = "blob" 238 | SOMETHING = ["${SOME_VAR}"] 239 | """) 240 | 241 | actual = Parser(path).parse_file() 242 | 243 | assert expected == actual 244 | 245 | 246 | def test_env_quoted_key(tmp_path, monkeypatch): 247 | monkeypatch.setenv("SOME_VAR", "blob") 248 | 249 | expected = {"SOMETHING": "blob"} 250 | 251 | path = tmp_path / "pyproject.toml" 252 | path.write_text(""" 253 | [tool.django] 254 | SOMETHING = { "$env" = "SOME_VAR" } 255 | """) 256 | 257 | actual = Parser(path).parse_file() 258 | 259 | assert expected == actual 260 | 261 | 262 | def test_env_missing(tmp_path): 263 | expected = {"SOMETHING": None} 264 | 265 | path = tmp_path / "pyproject.toml" 266 | path.write_text(""" 267 | [tool.django] 268 | SOMETHING = { "$env" = "SOME_VAR" } 269 | """) 270 | 271 | actual = Parser(path).parse_file() 272 | 273 | assert expected == actual 274 | 275 | 276 | def test_env_default(tmp_path): 277 | expected = {"SOMETHING": "default"} 278 | 279 | path = tmp_path / "pyproject.toml" 280 | path.write_text(""" 281 | [tool.django] 282 | SOMETHING = { "$env" = "SOME_VAR", "$default" = "default" } 283 | """) 284 | 285 | actual = Parser(path).parse_file() 286 | 287 | assert expected == actual 288 | 289 | 290 | def test_path(tmp_path): 291 | expected = {"SOMETHING": tmp_path / "test-file"} 292 | 293 | path = tmp_path / "pyproject.toml" 294 | path.write_text(""" 295 | [tool.django] 296 | SOMETHING = { "$path" = "test-file" } 297 | """) 298 | 299 | actual = Parser(path).parse_file() 300 | 301 | assert expected == actual 302 | 303 | 304 | def test_relative_path(tmp_path): 305 | expected = {"SOMETHING": tmp_path / "test-file"} 306 | 307 | path = tmp_path / "pyproject.toml" 308 | path.write_text(""" 309 | [tool.django] 310 | SOMETHING = { "$path" = "./test-file" } 311 | """) 312 | 313 | actual = Parser(path).parse_file() 314 | 315 | assert expected == actual 316 | 317 | 318 | def test_parent_path(tmp_path): 319 | expected = {"SOMETHING": tmp_path.parent / "test-file"} 320 | 321 | path = tmp_path / "pyproject.toml" 322 | path.write_text(""" 323 | [tool.django] 324 | SOMETHING = { "$path" = "../test-file" } 325 | """) 326 | 327 | actual = Parser(path).parse_file() 328 | 329 | assert expected == actual 330 | 331 | 332 | def test_parent_path_2(tmp_path): 333 | expected = {"SOMETHING": tmp_path.parent / "test-file"} 334 | 335 | path = tmp_path / "pyproject.toml" 336 | path.write_text(""" 337 | [tool.django] 338 | SOMETHING = { "$path" = "./../test-file" } 339 | """) 340 | 341 | actual = Parser(path).parse_file() 342 | 343 | assert expected == actual 344 | 345 | 346 | def test_insert(tmp_path): 347 | expected = {"SOMETHING": [1, 2]} 348 | 349 | path = tmp_path / "pyproject.toml" 350 | path.write_text(""" 351 | [tool.django] 352 | SOMETHING = [1] 353 | 354 | [tool.django.apps.something] 355 | SOMETHING = { "$insert" = 2 } 356 | """) 357 | 358 | actual = Parser(path).parse_file() 359 | 360 | assert expected == actual 361 | 362 | 363 | def test_insert_missing(tmp_path): 364 | expected = {"SOMETHING": [1]} 365 | 366 | path = tmp_path / "pyproject.toml" 367 | path.write_text(""" 368 | [tool.django] 369 | SOMETHING = { "$insert" = 1 } 370 | """) 371 | 372 | actual = Parser(path).parse_file() 373 | 374 | assert expected == actual 375 | 376 | 377 | def test_insert_invalid(tmp_path): 378 | expected = {"SOMETHING": "blob"} 379 | 380 | path = tmp_path / "pyproject.toml" 381 | path.write_text(""" 382 | [tool.django] 383 | SOMETHING = "hello" 384 | 385 | [tool.django.apps.something] 386 | SOMETHING = { "$insert" = 1 } 387 | """) 388 | 389 | with pytest.raises(InvalidActionError) as e: 390 | actual = Parser(path).parse_file() 391 | 392 | assert expected == actual 393 | 394 | assert ( 395 | "dj_toml_settings.exceptions.InvalidActionError: `insert` cannot be used for value of type: " 396 | == e.exconly() 397 | ) 398 | 399 | 400 | def test_insert_index(tmp_path): 401 | expected = {"SOMETHING": [2, 1]} 402 | 403 | path = tmp_path / "pyproject.toml" 404 | path.write_text(""" 405 | [tool.django] 406 | SOMETHING = [1] 407 | 408 | [tool.django.apps.something] 409 | SOMETHING = { "$insert" = 2, "$index" = 0 } 410 | """) 411 | 412 | actual = Parser(path).parse_file() 413 | 414 | assert expected == actual 415 | 416 | 417 | def test_inline_table(tmp_path): 418 | expected = {"SOMETHING": {"blob": "hello"}} 419 | 420 | path = tmp_path / "pyproject.toml" 421 | path.write_text(""" 422 | [tool.django] 423 | SOMETHING = [1] 424 | 425 | [tool.django.apps.something] 426 | SOMETHING = { blob = "hello" } 427 | """) 428 | 429 | actual = Parser(path).parse_file() 430 | 431 | assert expected == actual 432 | 433 | 434 | def test_table(tmp_path): 435 | expected = {"SOMETHING": {"blob": "hello"}} 436 | 437 | path = tmp_path / "pyproject.toml" 438 | path.write_text(""" 439 | [tool.django.SOMETHING] 440 | blob = "hello" 441 | """) 442 | 443 | actual = Parser(path).parse_file() 444 | 445 | assert expected == actual 446 | 447 | 448 | def test_all_dictionaries(tmp_path): 449 | path = tmp_path / "pyproject.toml" 450 | 451 | expected = {"DATABASES": {"default": {"ENGINE": "django.db.backends.postgresql"}}} 452 | 453 | # inline table 454 | path.write_text(""" 455 | [tool.django] 456 | DATABASES = { default = { ENGINE = "django.db.backends.postgresql" } } 457 | """) 458 | 459 | actual = Parser(path).parse_file() 460 | assert expected == actual 461 | 462 | # table for DATABASES 463 | path.write_text(""" 464 | [tool.django.DATABASES] 465 | default = { ENGINE = "django.db.backends.postgresql" } 466 | """) 467 | 468 | actual = Parser(path).parse_file() 469 | assert expected == actual 470 | 471 | # table for DATABASES.default 472 | path.write_text(""" 473 | [tool.django.DATABASES.default] 474 | ENGINE = "django.db.backends.postgresql" 475 | """) 476 | 477 | actual = Parser(path).parse_file() 478 | assert expected == actual 479 | 480 | 481 | def test_variable(tmp_path): 482 | expected = {"SOMETHING": "hello", "SOMETHING2": "hello"} 483 | 484 | path = tmp_path / "pyproject.toml" 485 | path.write_text(""" 486 | [tool.django] 487 | SOMETHING = "hello" 488 | 489 | [tool.django.apps.something] 490 | SOMETHING2 = "${SOMETHING}" 491 | """) 492 | 493 | actual = Parser(path).parse_file() 494 | 495 | assert expected == actual 496 | 497 | 498 | def test_variable_callable(tmp_path): 499 | def some_function(): 500 | pass 501 | 502 | expected = {"SOMETHING": some_function} 503 | 504 | path = tmp_path / "pyproject.toml" 505 | path.write_text(""" 506 | [tool.django] 507 | SOMETHING = "${some_function}" 508 | """) 509 | 510 | actual = Parser(path, data={"some_function": some_function}).parse_file() 511 | 512 | assert id(expected["SOMETHING"]) == id(actual["SOMETHING"]) 513 | 514 | 515 | def test_variable_int(tmp_path): 516 | expected = {"INT": 123, "TEST": 1234} 517 | 518 | path = tmp_path / "pyproject.toml" 519 | path.write_text(""" 520 | [tool.django] 521 | INT = 123 522 | TEST = "${INT}4" 523 | """) 524 | 525 | actual = Parser(path).parse_file() 526 | 527 | assert expected == actual 528 | 529 | 530 | def test_variable_int_with_string(tmp_path): 531 | expected = {"INT": 123, "TEST": "a123"} 532 | 533 | path = tmp_path / "pyproject.toml" 534 | path.write_text(""" 535 | [tool.django] 536 | INT = 123 537 | TEST = "a${INT}" 538 | """) 539 | 540 | actual = Parser(path).parse_file() 541 | 542 | assert expected == actual 543 | 544 | 545 | def test_variable_float(tmp_path): 546 | expected = {"FLOAT": 123.1, "TEST": 123.14} 547 | 548 | path = tmp_path / "pyproject.toml" 549 | path.write_text(""" 550 | [tool.django] 551 | FLOAT = 123.1 552 | TEST = "${FLOAT}4" 553 | """) 554 | 555 | actual = Parser(path).parse_file() 556 | 557 | assert expected == actual 558 | 559 | 560 | def test_variable_float_with_string(tmp_path): 561 | expected = {"FLOAT": 123.1, "TEST": "a123.1"} 562 | 563 | path = tmp_path / "pyproject.toml" 564 | path.write_text(""" 565 | [tool.django] 566 | FLOAT = 123.1 567 | TEST = "a${FLOAT}" 568 | """) 569 | 570 | actual = Parser(path).parse_file() 571 | 572 | assert expected == actual 573 | 574 | 575 | def test_variable_array(tmp_path): 576 | expected = {"ARRAY": [1, 2, 3], "TEST": [1, 2, 3]} 577 | 578 | path = tmp_path / "pyproject.toml" 579 | path.write_text(""" 580 | [tool.django] 581 | ARRAY = [1, 2, 3] 582 | TEST = "${ARRAY}" 583 | """) 584 | 585 | actual = Parser(path).parse_file() 586 | 587 | assert expected == actual 588 | 589 | 590 | def test_variable_dictionary(tmp_path): 591 | expected = {"HASH": {"a": 1}, "TEST": {"a": 1}} 592 | 593 | path = tmp_path / "pyproject.toml" 594 | path.write_text(""" 595 | [tool.django] 596 | HASH = { a = 1 } 597 | TEST = "${HASH}" 598 | """) 599 | 600 | actual = Parser(path).parse_file() 601 | 602 | assert expected == actual 603 | 604 | 605 | def test_variable_inline_table(tmp_path): 606 | expected = {"HASH": {"a": 1}, "TEST": {"a": 1}} 607 | 608 | path = tmp_path / "pyproject.toml" 609 | path.write_text(""" 610 | [tool.django.HASH] 611 | a = 1 612 | 613 | [tool.django.apps.blob] 614 | TEST = "${HASH}" 615 | """) 616 | 617 | actual = Parser(path).parse_file() 618 | 619 | assert expected == actual 620 | 621 | 622 | def test_variable_datetime_utc(tmp_path): 623 | expected = { 624 | "DATETIME": datetime(2025, 8, 30, 7, 32, tzinfo=timezone.utc), 625 | "TEST": datetime(2025, 8, 30, 7, 32, tzinfo=timezone.utc), 626 | } 627 | 628 | path = tmp_path / "pyproject.toml" 629 | path.write_text(""" 630 | [tool.django] 631 | DATETIME = 2025-08-30T07:32:00Z 632 | 633 | [tool.django.apps.blob] 634 | TEST = "${DATETIME}" 635 | """) 636 | 637 | actual = Parser(path).parse_file() 638 | 639 | assert expected == actual 640 | 641 | 642 | def test_variable_datetime_tz(tmp_path): 643 | expected = { 644 | "DATETIME": dateparser.parse("2025-08-30T00:32:00-07:00"), 645 | "TEST": dateparser.parse("2025-08-30T00:32:00-07:00"), 646 | } 647 | 648 | path = tmp_path / "pyproject.toml" 649 | path.write_text(""" 650 | [tool.django] 651 | DATETIME = 2025-08-30T00:32:00-07:00 652 | 653 | [tool.django.apps.blob] 654 | TEST = "${DATETIME}" 655 | """) 656 | 657 | actual = Parser(path).parse_file() 658 | 659 | assert expected == actual 660 | 661 | 662 | def test_none(tmp_path): 663 | expected = {"TEST": None} 664 | 665 | path = tmp_path / "pyproject.toml" 666 | path.write_text(""" 667 | [tool.django] 668 | TEST = { "$none" = 1 } 669 | """) 670 | 671 | actual = Parser(path).parse_file() 672 | 673 | assert expected == actual 674 | 675 | 676 | def test_variable_invalid(tmp_path, caplog): 677 | expected = {"SOMETHING": "${SOMETHING2}", "SOMETHING2": "hello"} 678 | 679 | path = tmp_path / "pyproject.toml" 680 | path.write_text(""" 681 | [tool.django] 682 | SOMETHING = "${SOMETHING2}" 683 | 684 | [tool.django.apps.something] 685 | SOMETHING2 = "hello" 686 | """) 687 | 688 | with caplog.at_level(logging.WARNING): 689 | actual = Parser(path).parse_file() 690 | 691 | assert expected == actual 692 | 693 | # Check that an error was logged 694 | assert len(caplog.records) == 1 695 | actual = caplog.records[0] 696 | 697 | assert "Missing variable substitution ${SOMETHING2}" == str(actual.msg) 698 | 699 | 700 | def test_variable_missing(tmp_path, caplog): 701 | expected = {"SOMETHING": "hello", "SOMETHING2": "${SOMETHING1}"} 702 | 703 | path = tmp_path / "pyproject.toml" 704 | path.write_text(""" 705 | [tool.django] 706 | SOMETHING = "hello" 707 | 708 | [tool.django.apps.something] 709 | SOMETHING2 = "${SOMETHING1}" 710 | """) 711 | 712 | with caplog.at_level(logging.WARNING): 713 | actual = Parser(path).parse_file() 714 | 715 | assert expected == actual 716 | 717 | # Check that an error was logged 718 | assert len(caplog.records) == 1 719 | actual = caplog.records[0] 720 | 721 | assert "Missing variable substitution ${SOMETHING1}" == str(actual.msg) 722 | 723 | 724 | def test_variable_start_path(tmp_path): 725 | expected = {"BASE_DIR": tmp_path, "STATIC_ROOT": tmp_path / "staticfiles"} 726 | 727 | path = tmp_path / "pyproject.toml" 728 | path.write_text(""" 729 | [tool.django] 730 | BASE_DIR = { "$path" = "." } 731 | STATIC_ROOT = "${BASE_DIR}/staticfiles" 732 | """) 733 | 734 | actual = Parser(path).parse_file() 735 | 736 | assert expected == actual 737 | 738 | 739 | def test_variable_end_path(tmp_path): 740 | expected = {"BASE_DIR": Path("/something"), "STATIC_ROOT": Path("/blob/something")} 741 | 742 | path = tmp_path / "pyproject.toml" 743 | path.write_text(""" 744 | [tool.django] 745 | BASE_DIR = { "$path" = "/something" } 746 | STATIC_ROOT = "/blob${BASE_DIR}" 747 | """) 748 | 749 | actual = Parser(path).parse_file() 750 | 751 | assert expected == actual 752 | 753 | 754 | def test_invalid_toml(tmp_path, caplog): 755 | path = tmp_path / "pyproject.toml" 756 | path.write_text("[") 757 | 758 | expected = "Cannot parse TOML at: " 759 | 760 | with caplog.at_level(logging.ERROR): 761 | Parser(path).parse_file() 762 | 763 | # Check that an error was logged 764 | assert len(caplog.records) == 1 765 | actual = caplog.records[0] 766 | 767 | assert expected in str(actual.msg) 768 | 769 | 770 | def test_missing_file(caplog): 771 | expected = "Cannot find file at: missing-file" 772 | 773 | with caplog.at_level(logging.WARNING): 774 | Parser(Path("missing-file")).parse_file() 775 | 776 | # Check that an error was logged 777 | assert len(caplog.records) == 1 778 | actual = caplog.records[0] 779 | 780 | assert expected == str(actual.msg) 781 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.10" 4 | 5 | [[package]] 6 | name = "colorama" 7 | version = "0.4.6" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 12 | ] 13 | 14 | [[package]] 15 | name = "coverage" 16 | version = "7.10.5" 17 | source = { registry = "https://pypi.org/simple" } 18 | sdist = { url = "https://files.pythonhosted.org/packages/61/83/153f54356c7c200013a752ce1ed5448573dca546ce125801afca9e1ac1a4/coverage-7.10.5.tar.gz", hash = "sha256:f2e57716a78bc3ae80b2207be0709a3b2b63b9f2dcf9740ee6ac03588a2015b6", size = 821662 } 19 | wheels = [ 20 | { url = "https://files.pythonhosted.org/packages/af/70/e77b0061a6c7157bfce645c6b9a715a08d4c86b3360a7b3252818080b817/coverage-7.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c6a5c3414bfc7451b879141ce772c546985163cf553f08e0f135f0699a911801", size = 216774 }, 21 | { url = "https://files.pythonhosted.org/packages/91/08/2a79de5ecf37ee40f2d898012306f11c161548753391cec763f92647837b/coverage-7.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bc8e4d99ce82f1710cc3c125adc30fd1487d3cf6c2cd4994d78d68a47b16989a", size = 217175 }, 22 | { url = "https://files.pythonhosted.org/packages/64/57/0171d69a699690149a6ba6a4eb702814448c8d617cf62dbafa7ce6bfdf63/coverage-7.10.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:02252dc1216e512a9311f596b3169fad54abcb13827a8d76d5630c798a50a754", size = 243931 }, 23 | { url = "https://files.pythonhosted.org/packages/15/06/3a67662c55656702bd398a727a7f35df598eb11104fcb34f1ecbb070291a/coverage-7.10.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73269df37883e02d460bee0cc16be90509faea1e3bd105d77360b512d5bb9c33", size = 245740 }, 24 | { url = "https://files.pythonhosted.org/packages/00/f4/f8763aabf4dc30ef0d0012522d312f0b7f9fede6246a1f27dbcc4a1e523c/coverage-7.10.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8a81b0614642f91c9effd53eec284f965577591f51f547a1cbeb32035b4c2f", size = 247600 }, 25 | { url = "https://files.pythonhosted.org/packages/9c/31/6632219a9065e1b83f77eda116fed4c76fb64908a6a9feae41816dab8237/coverage-7.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6a29f8e0adb7f8c2b95fa2d4566a1d6e6722e0a637634c6563cb1ab844427dd9", size = 245640 }, 26 | { url = "https://files.pythonhosted.org/packages/6e/e2/3dba9b86037b81649b11d192bb1df11dde9a81013e434af3520222707bc8/coverage-7.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fcf6ab569436b4a647d4e91accba12509ad9f2554bc93d3aee23cc596e7f99c3", size = 243659 }, 27 | { url = "https://files.pythonhosted.org/packages/02/b9/57170bd9f3e333837fc24ecc88bc70fbc2eb7ccfd0876854b0c0407078c3/coverage-7.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:90dc3d6fb222b194a5de60af8d190bedeeddcbc7add317e4a3cd333ee6b7c879", size = 244537 }, 28 | { url = "https://files.pythonhosted.org/packages/b3/1c/93ac36ef1e8b06b8d5777393a3a40cb356f9f3dab980be40a6941e443588/coverage-7.10.5-cp310-cp310-win32.whl", hash = "sha256:414a568cd545f9dc75f0686a0049393de8098414b58ea071e03395505b73d7a8", size = 219285 }, 29 | { url = "https://files.pythonhosted.org/packages/30/95/23252277e6e5fe649d6cd3ed3f35d2307e5166de4e75e66aa7f432abc46d/coverage-7.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:e551f9d03347196271935fd3c0c165f0e8c049220280c1120de0084d65e9c7ff", size = 220185 }, 30 | { url = "https://files.pythonhosted.org/packages/cb/f2/336d34d2fc1291ca7c18eeb46f64985e6cef5a1a7ef6d9c23720c6527289/coverage-7.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c177e6ffe2ebc7c410785307758ee21258aa8e8092b44d09a2da767834f075f2", size = 216890 }, 31 | { url = "https://files.pythonhosted.org/packages/39/ea/92448b07cc1cf2b429d0ce635f59cf0c626a5d8de21358f11e92174ff2a6/coverage-7.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14d6071c51ad0f703d6440827eaa46386169b5fdced42631d5a5ac419616046f", size = 217287 }, 32 | { url = "https://files.pythonhosted.org/packages/96/ba/ad5b36537c5179c808d0ecdf6e4aa7630b311b3c12747ad624dcd43a9b6b/coverage-7.10.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:61f78c7c3bc272a410c5ae3fde7792b4ffb4acc03d35a7df73ca8978826bb7ab", size = 247683 }, 33 | { url = "https://files.pythonhosted.org/packages/28/e5/fe3bbc8d097029d284b5fb305b38bb3404895da48495f05bff025df62770/coverage-7.10.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f39071caa126f69d63f99b324fb08c7b1da2ec28cbb1fe7b5b1799926492f65c", size = 249614 }, 34 | { url = "https://files.pythonhosted.org/packages/69/9c/a1c89a8c8712799efccb32cd0a1ee88e452f0c13a006b65bb2271f1ac767/coverage-7.10.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343a023193f04d46edc46b2616cdbee68c94dd10208ecd3adc56fcc54ef2baa1", size = 251719 }, 35 | { url = "https://files.pythonhosted.org/packages/e9/be/5576b5625865aa95b5633315f8f4142b003a70c3d96e76f04487c3b5cc95/coverage-7.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:585ffe93ae5894d1ebdee69fc0b0d4b7c75d8007983692fb300ac98eed146f78", size = 249411 }, 36 | { url = "https://files.pythonhosted.org/packages/94/0a/e39a113d4209da0dbbc9385608cdb1b0726a4d25f78672dc51c97cfea80f/coverage-7.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0ef4e66f006ed181df29b59921bd8fc7ed7cd6a9289295cd8b2824b49b570df", size = 247466 }, 37 | { url = "https://files.pythonhosted.org/packages/40/cb/aebb2d8c9e3533ee340bea19b71c5b76605a0268aa49808e26fe96ec0a07/coverage-7.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb7b0bbf7cc1d0453b843eca7b5fa017874735bef9bfdfa4121373d2cc885ed6", size = 248104 }, 38 | { url = "https://files.pythonhosted.org/packages/08/e6/26570d6ccce8ff5de912cbfd268e7f475f00597cb58da9991fa919c5e539/coverage-7.10.5-cp311-cp311-win32.whl", hash = "sha256:1d043a8a06987cc0c98516e57c4d3fc2c1591364831e9deb59c9e1b4937e8caf", size = 219327 }, 39 | { url = "https://files.pythonhosted.org/packages/79/79/5f48525e366e518b36e66167e3b6e5db6fd54f63982500c6a5abb9d3dfbd/coverage-7.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:fefafcca09c3ac56372ef64a40f5fe17c5592fab906e0fdffd09543f3012ba50", size = 220213 }, 40 | { url = "https://files.pythonhosted.org/packages/40/3c/9058128b7b0bf333130c320b1eb1ae485623014a21ee196d68f7737f8610/coverage-7.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:7e78b767da8b5fc5b2faa69bb001edafcd6f3995b42a331c53ef9572c55ceb82", size = 218893 }, 41 | { url = "https://files.pythonhosted.org/packages/27/8e/40d75c7128f871ea0fd829d3e7e4a14460cad7c3826e3b472e6471ad05bd/coverage-7.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2d05c7e73c60a4cecc7d9b60dbfd603b4ebc0adafaef371445b47d0f805c8a9", size = 217077 }, 42 | { url = "https://files.pythonhosted.org/packages/18/a8/f333f4cf3fb5477a7f727b4d603a2eb5c3c5611c7fe01329c2e13b23b678/coverage-7.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32ddaa3b2c509778ed5373b177eb2bf5662405493baeff52278a0b4f9415188b", size = 217310 }, 43 | { url = "https://files.pythonhosted.org/packages/ec/2c/fbecd8381e0a07d1547922be819b4543a901402f63930313a519b937c668/coverage-7.10.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd382410039fe062097aa0292ab6335a3f1e7af7bba2ef8d27dcda484918f20c", size = 248802 }, 44 | { url = "https://files.pythonhosted.org/packages/3f/bc/1011da599b414fb6c9c0f34086736126f9ff71f841755786a6b87601b088/coverage-7.10.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7fa22800f3908df31cea6fb230f20ac49e343515d968cc3a42b30d5c3ebf9b5a", size = 251550 }, 45 | { url = "https://files.pythonhosted.org/packages/4c/6f/b5c03c0c721c067d21bc697accc3642f3cef9f087dac429c918c37a37437/coverage-7.10.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f366a57ac81f5e12797136552f5b7502fa053c861a009b91b80ed51f2ce651c6", size = 252684 }, 46 | { url = "https://files.pythonhosted.org/packages/f9/50/d474bc300ebcb6a38a1047d5c465a227605d6473e49b4e0d793102312bc5/coverage-7.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1dc8f1980a272ad4a6c84cba7981792344dad33bf5869361576b7aef42733a", size = 250602 }, 47 | { url = "https://files.pythonhosted.org/packages/4a/2d/548c8e04249cbba3aba6bd799efdd11eee3941b70253733f5d355d689559/coverage-7.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2285c04ee8676f7938b02b4936d9b9b672064daab3187c20f73a55f3d70e6b4a", size = 248724 }, 48 | { url = "https://files.pythonhosted.org/packages/e2/96/a7c3c0562266ac39dcad271d0eec8fc20ab576e3e2f64130a845ad2a557b/coverage-7.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2492e4dd9daab63f5f56286f8a04c51323d237631eb98505d87e4c4ff19ec34", size = 250158 }, 49 | { url = "https://files.pythonhosted.org/packages/f3/75/74d4be58c70c42ef0b352d597b022baf12dbe2b43e7cb1525f56a0fb1d4b/coverage-7.10.5-cp312-cp312-win32.whl", hash = "sha256:38a9109c4ee8135d5df5505384fc2f20287a47ccbe0b3f04c53c9a1989c2bbaf", size = 219493 }, 50 | { url = "https://files.pythonhosted.org/packages/4f/08/364e6012d1d4d09d1e27437382967efed971d7613f94bca9add25f0c1f2b/coverage-7.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:6b87f1ad60b30bc3c43c66afa7db6b22a3109902e28c5094957626a0143a001f", size = 220302 }, 51 | { url = "https://files.pythonhosted.org/packages/db/d5/7c8a365e1f7355c58af4fe5faf3f90cc8e587590f5854808d17ccb4e7077/coverage-7.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:672a6c1da5aea6c629819a0e1461e89d244f78d7b60c424ecf4f1f2556c041d8", size = 218936 }, 52 | { url = "https://files.pythonhosted.org/packages/9f/08/4166ecfb60ba011444f38a5a6107814b80c34c717bc7a23be0d22e92ca09/coverage-7.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef3b83594d933020f54cf65ea1f4405d1f4e41a009c46df629dd964fcb6e907c", size = 217106 }, 53 | { url = "https://files.pythonhosted.org/packages/25/d7/b71022408adbf040a680b8c64bf6ead3be37b553e5844f7465643979f7ca/coverage-7.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b96bfdf7c0ea9faebce088a3ecb2382819da4fbc05c7b80040dbc428df6af44", size = 217353 }, 54 | { url = "https://files.pythonhosted.org/packages/74/68/21e0d254dbf8972bb8dd95e3fe7038f4be037ff04ba47d6d1b12b37510ba/coverage-7.10.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63df1fdaffa42d914d5c4d293e838937638bf75c794cf20bee12978fc8c4e3bc", size = 248350 }, 55 | { url = "https://files.pythonhosted.org/packages/90/65/28752c3a896566ec93e0219fc4f47ff71bd2b745f51554c93e8dcb659796/coverage-7.10.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8002dc6a049aac0e81ecec97abfb08c01ef0c1fbf962d0c98da3950ace89b869", size = 250955 }, 56 | { url = "https://files.pythonhosted.org/packages/a5/eb/ca6b7967f57f6fef31da8749ea20417790bb6723593c8cd98a987be20423/coverage-7.10.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63d4bb2966d6f5f705a6b0c6784c8969c468dbc4bcf9d9ded8bff1c7e092451f", size = 252230 }, 57 | { url = "https://files.pythonhosted.org/packages/bc/29/17a411b2a2a18f8b8c952aa01c00f9284a1fbc677c68a0003b772ea89104/coverage-7.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1f672efc0731a6846b157389b6e6d5d5e9e59d1d1a23a5c66a99fd58339914d5", size = 250387 }, 58 | { url = "https://files.pythonhosted.org/packages/c7/89/97a9e271188c2fbb3db82235c33980bcbc733da7da6065afbaa1d685a169/coverage-7.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3f39cef43d08049e8afc1fde4a5da8510fc6be843f8dea350ee46e2a26b2f54c", size = 248280 }, 59 | { url = "https://files.pythonhosted.org/packages/d1/c6/0ad7d0137257553eb4706b4ad6180bec0a1b6a648b092c5bbda48d0e5b2c/coverage-7.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2968647e3ed5a6c019a419264386b013979ff1fb67dd11f5c9886c43d6a31fc2", size = 249894 }, 60 | { url = "https://files.pythonhosted.org/packages/84/56/fb3aba936addb4c9e5ea14f5979393f1c2466b4c89d10591fd05f2d6b2aa/coverage-7.10.5-cp313-cp313-win32.whl", hash = "sha256:0d511dda38595b2b6934c2b730a1fd57a3635c6aa2a04cb74714cdfdd53846f4", size = 219536 }, 61 | { url = "https://files.pythonhosted.org/packages/fc/54/baacb8f2f74431e3b175a9a2881feaa8feb6e2f187a0e7e3046f3c7742b2/coverage-7.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:9a86281794a393513cf117177fd39c796b3f8e3759bb2764259a2abba5cce54b", size = 220330 }, 62 | { url = "https://files.pythonhosted.org/packages/64/8a/82a3788f8e31dee51d350835b23d480548ea8621f3effd7c3ba3f7e5c006/coverage-7.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:cebd8e906eb98bb09c10d1feed16096700b1198d482267f8bf0474e63a7b8d84", size = 218961 }, 63 | { url = "https://files.pythonhosted.org/packages/d8/a1/590154e6eae07beee3b111cc1f907c30da6fc8ce0a83ef756c72f3c7c748/coverage-7.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0520dff502da5e09d0d20781df74d8189ab334a1e40d5bafe2efaa4158e2d9e7", size = 217819 }, 64 | { url = "https://files.pythonhosted.org/packages/0d/ff/436ffa3cfc7741f0973c5c89405307fe39b78dcf201565b934e6616fc4ad/coverage-7.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d9cd64aca68f503ed3f1f18c7c9174cbb797baba02ca8ab5112f9d1c0328cd4b", size = 218040 }, 65 | { url = "https://files.pythonhosted.org/packages/a0/ca/5787fb3d7820e66273913affe8209c534ca11241eb34ee8c4fd2aaa9dd87/coverage-7.10.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0913dd1613a33b13c4f84aa6e3f4198c1a21ee28ccb4f674985c1f22109f0aae", size = 259374 }, 66 | { url = "https://files.pythonhosted.org/packages/b5/89/21af956843896adc2e64fc075eae3c1cadb97ee0a6960733e65e696f32dd/coverage-7.10.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b7181c0feeb06ed8a02da02792f42f829a7b29990fef52eff257fef0885d760", size = 261551 }, 67 | { url = "https://files.pythonhosted.org/packages/e1/96/390a69244ab837e0ac137989277879a084c786cf036c3c4a3b9637d43a89/coverage-7.10.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d42b7396b605f774d4372dd9c49bed71cbabce4ae1ccd074d155709dd8f235", size = 263776 }, 68 | { url = "https://files.pythonhosted.org/packages/00/32/cfd6ae1da0a521723349f3129b2455832fc27d3f8882c07e5b6fefdd0da2/coverage-7.10.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b4fdc777e05c4940b297bf47bf7eedd56a39a61dc23ba798e4b830d585486ca5", size = 261326 }, 69 | { url = "https://files.pythonhosted.org/packages/4c/c4/bf8d459fb4ce2201e9243ce6c015936ad283a668774430a3755f467b39d1/coverage-7.10.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:42144e8e346de44a6f1dbd0a56575dd8ab8dfa7e9007da02ea5b1c30ab33a7db", size = 259090 }, 70 | { url = "https://files.pythonhosted.org/packages/f4/5d/a234f7409896468e5539d42234016045e4015e857488b0b5b5f3f3fa5f2b/coverage-7.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:66c644cbd7aed8fe266d5917e2c9f65458a51cfe5eeff9c05f15b335f697066e", size = 260217 }, 71 | { url = "https://files.pythonhosted.org/packages/f3/ad/87560f036099f46c2ddd235be6476dd5c1d6be6bb57569a9348d43eeecea/coverage-7.10.5-cp313-cp313t-win32.whl", hash = "sha256:2d1b73023854068c44b0c554578a4e1ef1b050ed07cf8b431549e624a29a66ee", size = 220194 }, 72 | { url = "https://files.pythonhosted.org/packages/36/a8/04a482594fdd83dc677d4a6c7e2d62135fff5a1573059806b8383fad9071/coverage-7.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:54a1532c8a642d8cc0bd5a9a51f5a9dcc440294fd06e9dda55e743c5ec1a8f14", size = 221258 }, 73 | { url = "https://files.pythonhosted.org/packages/eb/ad/7da28594ab66fe2bc720f1bc9b131e62e9b4c6e39f044d9a48d18429cc21/coverage-7.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:74d5b63fe3f5f5d372253a4ef92492c11a4305f3550631beaa432fc9df16fcff", size = 219521 }, 74 | { url = "https://files.pythonhosted.org/packages/d3/7f/c8b6e4e664b8a95254c35a6c8dd0bf4db201ec681c169aae2f1256e05c85/coverage-7.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:68c5e0bc5f44f68053369fa0d94459c84548a77660a5f2561c5e5f1e3bed7031", size = 217090 }, 75 | { url = "https://files.pythonhosted.org/packages/44/74/3ee14ede30a6e10a94a104d1d0522d5fb909a7c7cac2643d2a79891ff3b9/coverage-7.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cf33134ffae93865e32e1e37df043bef15a5e857d8caebc0099d225c579b0fa3", size = 217365 }, 76 | { url = "https://files.pythonhosted.org/packages/41/5f/06ac21bf87dfb7620d1f870dfa3c2cae1186ccbcdc50b8b36e27a0d52f50/coverage-7.10.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad8fa9d5193bafcf668231294241302b5e683a0518bf1e33a9a0dfb142ec3031", size = 248413 }, 77 | { url = "https://files.pythonhosted.org/packages/21/bc/cc5bed6e985d3a14228539631573f3863be6a2587381e8bc5fdf786377a1/coverage-7.10.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:146fa1531973d38ab4b689bc764592fe6c2f913e7e80a39e7eeafd11f0ef6db2", size = 250943 }, 78 | { url = "https://files.pythonhosted.org/packages/8d/43/6a9fc323c2c75cd80b18d58db4a25dc8487f86dd9070f9592e43e3967363/coverage-7.10.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6013a37b8a4854c478d3219ee8bc2392dea51602dd0803a12d6f6182a0061762", size = 252301 }, 79 | { url = "https://files.pythonhosted.org/packages/69/7c/3e791b8845f4cd515275743e3775adb86273576596dc9f02dca37357b4f2/coverage-7.10.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eb90fe20db9c3d930fa2ad7a308207ab5b86bf6a76f54ab6a40be4012d88fcae", size = 250302 }, 80 | { url = "https://files.pythonhosted.org/packages/5c/bc/5099c1e1cb0c9ac6491b281babea6ebbf999d949bf4aa8cdf4f2b53505e8/coverage-7.10.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:384b34482272e960c438703cafe63316dfbea124ac62006a455c8410bf2a2262", size = 248237 }, 81 | { url = "https://files.pythonhosted.org/packages/7e/51/d346eb750a0b2f1e77f391498b753ea906fde69cc11e4b38dca28c10c88c/coverage-7.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:467dc74bd0a1a7de2bedf8deaf6811f43602cb532bd34d81ffd6038d6d8abe99", size = 249726 }, 82 | { url = "https://files.pythonhosted.org/packages/a3/85/eebcaa0edafe427e93286b94f56ea7e1280f2c49da0a776a6f37e04481f9/coverage-7.10.5-cp314-cp314-win32.whl", hash = "sha256:556d23d4e6393ca898b2e63a5bca91e9ac2d5fb13299ec286cd69a09a7187fde", size = 219825 }, 83 | { url = "https://files.pythonhosted.org/packages/3c/f7/6d43e037820742603f1e855feb23463979bf40bd27d0cde1f761dcc66a3e/coverage-7.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:f4446a9547681533c8fa3e3c6cf62121eeee616e6a92bd9201c6edd91beffe13", size = 220618 }, 84 | { url = "https://files.pythonhosted.org/packages/4a/b0/ed9432e41424c51509d1da603b0393404b828906236fb87e2c8482a93468/coverage-7.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e78bd9cf65da4c303bf663de0d73bf69f81e878bf72a94e9af67137c69b9fe9", size = 219199 }, 85 | { url = "https://files.pythonhosted.org/packages/2f/54/5a7ecfa77910f22b659c820f67c16fc1e149ed132ad7117f0364679a8fa9/coverage-7.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5661bf987d91ec756a47c7e5df4fbcb949f39e32f9334ccd3f43233bbb65e508", size = 217833 }, 86 | { url = "https://files.pythonhosted.org/packages/4e/0e/25672d917cc57857d40edf38f0b867fb9627115294e4f92c8fcbbc18598d/coverage-7.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a46473129244db42a720439a26984f8c6f834762fc4573616c1f37f13994b357", size = 218048 }, 87 | { url = "https://files.pythonhosted.org/packages/cb/7c/0b2b4f1c6f71885d4d4b2b8608dcfc79057adb7da4143eb17d6260389e42/coverage-7.10.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1f64b8d3415d60f24b058b58d859e9512624bdfa57a2d1f8aff93c1ec45c429b", size = 259549 }, 88 | { url = "https://files.pythonhosted.org/packages/94/73/abb8dab1609abec7308d83c6aec547944070526578ee6c833d2da9a0ad42/coverage-7.10.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:44d43de99a9d90b20e0163f9770542357f58860a26e24dc1d924643bd6aa7cb4", size = 261715 }, 89 | { url = "https://files.pythonhosted.org/packages/0b/d1/abf31de21ec92731445606b8d5e6fa5144653c2788758fcf1f47adb7159a/coverage-7.10.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a931a87e5ddb6b6404e65443b742cb1c14959622777f2a4efd81fba84f5d91ba", size = 263969 }, 90 | { url = "https://files.pythonhosted.org/packages/9c/b3/ef274927f4ebede96056173b620db649cc9cb746c61ffc467946b9d0bc67/coverage-7.10.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9559b906a100029274448f4c8b8b0a127daa4dade5661dfd821b8c188058842", size = 261408 }, 91 | { url = "https://files.pythonhosted.org/packages/20/fc/83ca2812be616d69b4cdd4e0c62a7bc526d56875e68fd0f79d47c7923584/coverage-7.10.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b08801e25e3b4526ef9ced1aa29344131a8f5213c60c03c18fe4c6170ffa2874", size = 259168 }, 92 | { url = "https://files.pythonhosted.org/packages/fc/4f/e0779e5716f72d5c9962e709d09815d02b3b54724e38567308304c3fc9df/coverage-7.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed9749bb8eda35f8b636fb7632f1c62f735a236a5d4edadd8bbcc5ea0542e732", size = 260317 }, 93 | { url = "https://files.pythonhosted.org/packages/2b/fe/4247e732f2234bb5eb9984a0888a70980d681f03cbf433ba7b48f08ca5d5/coverage-7.10.5-cp314-cp314t-win32.whl", hash = "sha256:609b60d123fc2cc63ccee6d17e4676699075db72d14ac3c107cc4976d516f2df", size = 220600 }, 94 | { url = "https://files.pythonhosted.org/packages/a7/a0/f294cff6d1034b87839987e5b6ac7385bec599c44d08e0857ac7f164ad0c/coverage-7.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:0666cf3d2c1626b5a3463fd5b05f5e21f99e6aec40a3192eee4d07a15970b07f", size = 221714 }, 95 | { url = "https://files.pythonhosted.org/packages/23/18/fa1afdc60b5528d17416df440bcbd8fd12da12bfea9da5b6ae0f7a37d0f7/coverage-7.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:bc85eb2d35e760120540afddd3044a5bf69118a91a296a8b3940dfc4fdcfe1e2", size = 219735 }, 96 | { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736 }, 97 | ] 98 | 99 | [package.optional-dependencies] 100 | toml = [ 101 | { name = "tomli", marker = "python_full_version <= '3.11'" }, 102 | ] 103 | 104 | [[package]] 105 | name = "dj-toml-settings" 106 | version = "0.5.0" 107 | source = { editable = "." } 108 | dependencies = [ 109 | { name = "python-dateutil" }, 110 | { name = "tomli", marker = "python_full_version < '3.11'" }, 111 | { name = "typeguard" }, 112 | ] 113 | 114 | [package.dev-dependencies] 115 | dev = [ 116 | { name = "mypy" }, 117 | { name = "pytest" }, 118 | { name = "pytest-cov" }, 119 | { name = "pytest-django" }, 120 | { name = "types-python-dateutil" }, 121 | { name = "types-toml" }, 122 | ] 123 | 124 | [package.metadata] 125 | requires-dist = [ 126 | { name = "python-dateutil", specifier = ">=2.9.0.post0" }, 127 | { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1.1.0" }, 128 | { name = "typeguard", specifier = ">=2" }, 129 | ] 130 | 131 | [package.metadata.requires-dev] 132 | dev = [ 133 | { name = "mypy", specifier = ">=0" }, 134 | { name = "pytest", specifier = "<9" }, 135 | { name = "pytest-cov", specifier = ">=0" }, 136 | { name = "pytest-django", specifier = ">=0" }, 137 | { name = "types-python-dateutil", specifier = ">=0" }, 138 | { name = "types-toml", specifier = ">=0" }, 139 | ] 140 | 141 | [[package]] 142 | name = "exceptiongroup" 143 | version = "1.3.0" 144 | source = { registry = "https://pypi.org/simple" } 145 | dependencies = [ 146 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 147 | ] 148 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } 149 | wheels = [ 150 | { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, 151 | ] 152 | 153 | [[package]] 154 | name = "iniconfig" 155 | version = "2.1.0" 156 | source = { registry = "https://pypi.org/simple" } 157 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } 158 | wheels = [ 159 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, 160 | ] 161 | 162 | [[package]] 163 | name = "mypy" 164 | version = "1.17.1" 165 | source = { registry = "https://pypi.org/simple" } 166 | dependencies = [ 167 | { name = "mypy-extensions" }, 168 | { name = "pathspec" }, 169 | { name = "tomli", marker = "python_full_version < '3.11'" }, 170 | { name = "typing-extensions" }, 171 | ] 172 | sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570 } 173 | wheels = [ 174 | { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299 }, 175 | { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451 }, 176 | { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211 }, 177 | { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687 }, 178 | { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322 }, 179 | { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962 }, 180 | { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009 }, 181 | { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482 }, 182 | { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883 }, 183 | { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215 }, 184 | { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956 }, 185 | { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307 }, 186 | { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295 }, 187 | { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355 }, 188 | { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285 }, 189 | { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895 }, 190 | { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025 }, 191 | { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664 }, 192 | { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338 }, 193 | { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066 }, 194 | { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473 }, 195 | { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296 }, 196 | { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657 }, 197 | { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320 }, 198 | { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037 }, 199 | { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550 }, 200 | { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963 }, 201 | { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189 }, 202 | { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322 }, 203 | { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879 }, 204 | { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411 }, 205 | ] 206 | 207 | [[package]] 208 | name = "mypy-extensions" 209 | version = "1.1.0" 210 | source = { registry = "https://pypi.org/simple" } 211 | sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } 212 | wheels = [ 213 | { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, 214 | ] 215 | 216 | [[package]] 217 | name = "packaging" 218 | version = "25.0" 219 | source = { registry = "https://pypi.org/simple" } 220 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } 221 | wheels = [ 222 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, 223 | ] 224 | 225 | [[package]] 226 | name = "pathspec" 227 | version = "0.12.1" 228 | source = { registry = "https://pypi.org/simple" } 229 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } 230 | wheels = [ 231 | { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, 232 | ] 233 | 234 | [[package]] 235 | name = "pluggy" 236 | version = "1.6.0" 237 | source = { registry = "https://pypi.org/simple" } 238 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } 239 | wheels = [ 240 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, 241 | ] 242 | 243 | [[package]] 244 | name = "pygments" 245 | version = "2.19.2" 246 | source = { registry = "https://pypi.org/simple" } 247 | sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } 248 | wheels = [ 249 | { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, 250 | ] 251 | 252 | [[package]] 253 | name = "pytest" 254 | version = "8.4.1" 255 | source = { registry = "https://pypi.org/simple" } 256 | dependencies = [ 257 | { name = "colorama", marker = "sys_platform == 'win32'" }, 258 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 259 | { name = "iniconfig" }, 260 | { name = "packaging" }, 261 | { name = "pluggy" }, 262 | { name = "pygments" }, 263 | { name = "tomli", marker = "python_full_version < '3.11'" }, 264 | ] 265 | sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714 } 266 | wheels = [ 267 | { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474 }, 268 | ] 269 | 270 | [[package]] 271 | name = "pytest-cov" 272 | version = "6.2.1" 273 | source = { registry = "https://pypi.org/simple" } 274 | dependencies = [ 275 | { name = "coverage", extra = ["toml"] }, 276 | { name = "pluggy" }, 277 | { name = "pytest" }, 278 | ] 279 | sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432 } 280 | wheels = [ 281 | { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644 }, 282 | ] 283 | 284 | [[package]] 285 | name = "pytest-django" 286 | version = "4.11.1" 287 | source = { registry = "https://pypi.org/simple" } 288 | dependencies = [ 289 | { name = "pytest" }, 290 | ] 291 | sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202 } 292 | wheels = [ 293 | { url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281 }, 294 | ] 295 | 296 | [[package]] 297 | name = "python-dateutil" 298 | version = "2.9.0.post0" 299 | source = { registry = "https://pypi.org/simple" } 300 | dependencies = [ 301 | { name = "six" }, 302 | ] 303 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } 304 | wheels = [ 305 | { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, 306 | ] 307 | 308 | [[package]] 309 | name = "six" 310 | version = "1.17.0" 311 | source = { registry = "https://pypi.org/simple" } 312 | sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } 313 | wheels = [ 314 | { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, 315 | ] 316 | 317 | [[package]] 318 | name = "tomli" 319 | version = "2.2.1" 320 | source = { registry = "https://pypi.org/simple" } 321 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 322 | wheels = [ 323 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, 324 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, 325 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, 326 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, 327 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, 328 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, 329 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, 330 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, 331 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 332 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 333 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, 334 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, 335 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, 336 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, 337 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, 338 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, 339 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, 340 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, 341 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 342 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 343 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, 344 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, 345 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, 346 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, 347 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, 348 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, 349 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, 350 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, 351 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 352 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 353 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 354 | ] 355 | 356 | [[package]] 357 | name = "typeguard" 358 | version = "4.4.4" 359 | source = { registry = "https://pypi.org/simple" } 360 | dependencies = [ 361 | { name = "typing-extensions" }, 362 | ] 363 | sdist = { url = "https://files.pythonhosted.org/packages/c7/68/71c1a15b5f65f40e91b65da23b8224dad41349894535a97f63a52e462196/typeguard-4.4.4.tar.gz", hash = "sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74", size = 75203 } 364 | wheels = [ 365 | { url = "https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl", hash = "sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e", size = 34874 }, 366 | ] 367 | 368 | [[package]] 369 | name = "types-python-dateutil" 370 | version = "2.9.0.20250822" 371 | source = { registry = "https://pypi.org/simple" } 372 | sdist = { url = "https://files.pythonhosted.org/packages/0c/0a/775f8551665992204c756be326f3575abba58c4a3a52eef9909ef4536428/types_python_dateutil-2.9.0.20250822.tar.gz", hash = "sha256:84c92c34bd8e68b117bff742bc00b692a1e8531262d4507b33afcc9f7716cd53", size = 16084 } 373 | wheels = [ 374 | { url = "https://files.pythonhosted.org/packages/ab/d9/a29dfa84363e88b053bf85a8b7f212a04f0d7343a4d24933baa45c06e08b/types_python_dateutil-2.9.0.20250822-py3-none-any.whl", hash = "sha256:849d52b737e10a6dc6621d2bd7940ec7c65fcb69e6aa2882acf4e56b2b508ddc", size = 17892 }, 375 | ] 376 | 377 | [[package]] 378 | name = "types-toml" 379 | version = "0.10.8.20240310" 380 | source = { registry = "https://pypi.org/simple" } 381 | sdist = { url = "https://files.pythonhosted.org/packages/86/47/3e4c75042792bff8e90d7991aa5c51812cc668828cc6cce711e97f63a607/types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331", size = 4392 } 382 | wheels = [ 383 | { url = "https://files.pythonhosted.org/packages/da/a2/d32ab58c0b216912638b140ab2170ee4b8644067c293b170e19fba340ccc/types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d", size = 4777 }, 384 | ] 385 | 386 | [[package]] 387 | name = "typing-extensions" 388 | version = "4.14.1" 389 | source = { registry = "https://pypi.org/simple" } 390 | sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } 391 | wheels = [ 392 | { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, 393 | ] 394 | --------------------------------------------------------------------------------