├── tests ├── __init__.py ├── fixtures │ ├── with-from-file │ │ ├── version.txt │ │ └── pyproject.toml │ ├── with-bump │ │ └── pyproject.toml │ ├── with-dirty │ │ └── pyproject.toml │ ├── with-pep440 │ │ └── pyproject.toml │ ├── with-semver │ │ └── pyproject.toml │ ├── with-pattern │ │ └── pyproject.toml │ ├── with-format │ │ └── pyproject.toml │ └── with-jinja-format │ │ └── pyproject.toml ├── utils.py ├── conftest.py ├── test_schemas.py ├── test_main.py ├── test_version_source.py ├── test_metadata_hook.py └── test_template.py ├── requirements.txt ├── examples ├── metadata-hook │ ├── metadata_hook │ │ └── __init__.py │ ├── .gitignore │ ├── child-project │ │ ├── child_project │ │ │ └── __init__.py │ │ ├── README.md │ │ ├── .gitignore │ │ └── pyproject.toml │ ├── README.md │ └── pyproject.toml └── version-source │ ├── .gitignore │ ├── version_source │ ├── _version.pyi │ └── __init__.py │ ├── README.md │ └── pyproject.toml ├── src └── uv_dynamic_versioning │ ├── __init__.py │ ├── cli.py │ ├── hooks.py │ ├── version_source.py │ ├── base.py │ ├── template.py │ ├── metadata_hook.py │ ├── main.py │ └── schemas.py ├── .pre-commit-config.yaml ├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── docs ├── tips.md ├── metadata_hook.md └── version_source.md ├── LICENSE ├── pyproject.toml ├── README.md ├── .gitignore └── uv.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | uv==0.8.17 2 | -------------------------------------------------------------------------------- /examples/metadata-hook/metadata_hook/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/with-from-file/version.txt: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | -------------------------------------------------------------------------------- /examples/metadata-hook/.gitignore: -------------------------------------------------------------------------------- 1 | src/example/_version.py 2 | -------------------------------------------------------------------------------- /examples/metadata-hook/child-project/child_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/version-source/.gitignore: -------------------------------------------------------------------------------- 1 | src/example/_version.py 2 | -------------------------------------------------------------------------------- /examples/metadata-hook/child-project/README.md: -------------------------------------------------------------------------------- 1 | # Child Project 2 | -------------------------------------------------------------------------------- /examples/version-source/version_source/_version.pyi: -------------------------------------------------------------------------------- 1 | version: str 2 | -------------------------------------------------------------------------------- /src/uv_dynamic_versioning/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.12.0" 2 | -------------------------------------------------------------------------------- /examples/metadata-hook/child-project/.gitignore: -------------------------------------------------------------------------------- 1 | src/example/_version.py 2 | -------------------------------------------------------------------------------- /examples/metadata-hook/README.md: -------------------------------------------------------------------------------- 1 | # Metadata Hook Example 2 | 3 | An example project of `uv-dynamic-versioning`'s metadata hook. 4 | 5 | ```bash 6 | pip install uv 7 | uv build 8 | ``` 9 | -------------------------------------------------------------------------------- /examples/version-source/README.md: -------------------------------------------------------------------------------- 1 | # Version Source Example 2 | 3 | An example project of `uv-dynamic-versioning`'s version source. 4 | 5 | ```bash 6 | pip install uv 7 | uv build 8 | ``` 9 | -------------------------------------------------------------------------------- /src/uv_dynamic_versioning/cli.py: -------------------------------------------------------------------------------- 1 | from .version_source import DynamicVersionSource 2 | 3 | 4 | def main() -> None: 5 | source = DynamicVersionSource(root=".", config={}) 6 | print(source.get_version_data()["version"]) # noqa: T201 7 | -------------------------------------------------------------------------------- /examples/version-source/version_source/__init__.py: -------------------------------------------------------------------------------- 1 | # using importlib.metadata 2 | import importlib.metadata 3 | 4 | __version__ = importlib.metadata.version(__name__) 5 | 6 | # alternatively, you can use a version file generated by version build hook 7 | from ._version import version 8 | 9 | __version__ = version 10 | -------------------------------------------------------------------------------- /tests/fixtures/with-bump/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "dummy" 3 | dynamic = ["version"] 4 | description = "" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [] 8 | 9 | [build-system] 10 | requires = ["hatchling", "uv-dynamic-versioning"] 11 | build-backend = "hatchling.build" 12 | 13 | [tool.uv-dynamic-versioning] 14 | bump = true 15 | -------------------------------------------------------------------------------- /tests/fixtures/with-dirty/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "dummy" 3 | dynamic = ["version"] 4 | description = "" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [] 8 | 9 | [build-system] 10 | requires = ["hatchling", "uv-dynamic-versioning"] 11 | build-backend = "hatchling.build" 12 | 13 | [tool.uv-dynamic-versioning] 14 | dirty = true 15 | -------------------------------------------------------------------------------- /tests/fixtures/with-pep440/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "dummy" 3 | dynamic = ["version"] 4 | description = "" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [] 8 | 9 | [build-system] 10 | requires = ["hatchling", "uv-dynamic-versioning"] 11 | build-backend = "hatchling.build" 12 | 13 | [tool.uv-dynamic-versioning] 14 | style = "pep440" 15 | -------------------------------------------------------------------------------- /tests/fixtures/with-semver/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "dummy" 3 | dynamic = ["version"] 4 | description = "" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [] 8 | 9 | [build-system] 10 | requires = ["hatchling", "uv-dynamic-versioning"] 11 | build-backend = "hatchling.build" 12 | 13 | [tool.uv-dynamic-versioning] 14 | style = "semver" 15 | -------------------------------------------------------------------------------- /tests/fixtures/with-pattern/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "dummy" 3 | dynamic = ["version"] 4 | description = "" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [] 8 | 9 | [build-system] 10 | requires = ["hatchling", "uv-dynamic-versioning"] 11 | build-backend = "hatchling.build" 12 | 13 | [tool.uv-dynamic-versioning] 14 | pattern = '(?P\d+)' 15 | -------------------------------------------------------------------------------- /examples/metadata-hook/child-project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "child-project" 3 | dynamic = ["version"] 4 | description = "A child project" 5 | readme = "README.md" 6 | requires-python = ">=3.9" 7 | 8 | [build-system] 9 | requires = ["hatchling", "uv-dynamic-versioning"] 10 | build-backend = "hatchling.build" 11 | 12 | [tool.hatch.version] 13 | source = "uv-dynamic-versioning" 14 | -------------------------------------------------------------------------------- /tests/fixtures/with-format/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "dummy" 3 | dynamic = ["version"] 4 | description = "" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [] 8 | 9 | [build-system] 10 | requires = ["hatchling", "uv-dynamic-versioning"] 11 | build-backend = "hatchling.build" 12 | 13 | [tool.uv-dynamic-versioning] 14 | format = "v{base}+{distance}.{commit}" 15 | -------------------------------------------------------------------------------- /tests/fixtures/with-from-file/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "dummy" 3 | dynamic = ["version"] 4 | description = "" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [] 8 | 9 | [build-system] 10 | requires = ["hatchling", "uv-dynamic-versioning"] 11 | build-backend = "hatchling.build" 12 | 13 | [tool.uv-dynamic-versioning.from-file] 14 | source = "tests/fixtures/with-from-file/version.txt" 15 | -------------------------------------------------------------------------------- /tests/fixtures/with-jinja-format/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "dummy" 3 | dynamic = ["version"] 4 | description = "" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [] 8 | 9 | [build-system] 10 | requires = ["hatchling", "uv-dynamic-versioning"] 11 | build-backend = "hatchling.build" 12 | 13 | [tool.uv-dynamic-versioning] 14 | format-jinja = """{{- base }}.dev{{ distance }}+g{{commit}}""" 15 | -------------------------------------------------------------------------------- /src/uv_dynamic_versioning/hooks.py: -------------------------------------------------------------------------------- 1 | from hatchling.plugin import hookimpl 2 | 3 | from .metadata_hook import DependenciesMetadataHook 4 | from .version_source import DynamicVersionSource 5 | 6 | 7 | @hookimpl 8 | def hatch_register_version_source(): 9 | return DynamicVersionSource 10 | 11 | 12 | @hookimpl 13 | def hatch_register_metadata_hook() -> type[DependenciesMetadataHook]: 14 | return DependenciesMetadataHook 15 | -------------------------------------------------------------------------------- /src/uv_dynamic_versioning/version_source.py: -------------------------------------------------------------------------------- 1 | from hatchling.version.source.plugin.interface import VersionSourceInterface 2 | 3 | from .base import BasePlugin 4 | from .main import get_version 5 | 6 | 7 | class DynamicVersionSource(BasePlugin, VersionSourceInterface): 8 | PLUGIN_NAME = "uv-dynamic-versioning" 9 | 10 | def get_version_data(self) -> dict[str, str]: 11 | version, _ = get_version(self.project_config) 12 | return {"version": version} 13 | -------------------------------------------------------------------------------- /examples/metadata-hook/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "metadata-hook" 3 | dynamic = ["version", "dependencies"] 4 | description = "An example project for uv-dynamic-versioning (version source + metadata hook)" 5 | readme = "README.md" 6 | requires-python = ">=3.9" 7 | 8 | [build-system] 9 | requires = ["hatchling", "uv-dynamic-versioning"] 10 | build-backend = "hatchling.build" 11 | 12 | [tool.hatch.version] 13 | source = "uv-dynamic-versioning" 14 | 15 | [tool.hatch.metadata.hooks.uv-dynamic-versioning] 16 | dependencies = ["child-project=={{ version }}"] 17 | -------------------------------------------------------------------------------- /examples/version-source/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "version-source" 3 | dynamic = ["version"] 4 | description = "An example project for uv-dynamic-versioning (version source)" 5 | readme = "README.md" 6 | requires-python = ">=3.9" 7 | dependencies = [] 8 | 9 | [build-system] 10 | requires = ["hatchling", "uv-dynamic-versioning"] 11 | build-backend = "hatchling.build" 12 | 13 | [tool.hatch.version] 14 | source = "uv-dynamic-versioning" 15 | 16 | [tool.hatch.build.hooks.version] 17 | path = "src/example/_version.py" 18 | template = ''' 19 | version = "{version}" 20 | ''' 21 | -------------------------------------------------------------------------------- /src/uv_dynamic_versioning/base.py: -------------------------------------------------------------------------------- 1 | from functools import cached_property 2 | from typing import cast 3 | 4 | from . import schemas 5 | from .main import parse, read, validate 6 | 7 | 8 | class BasePlugin: 9 | @cached_property 10 | def project(self) -> schemas.Project: 11 | text = read(cast(str, self.root)) # type: ignore 12 | parsed = parse(text) 13 | return validate(parsed) 14 | 15 | @property 16 | def project_config(self) -> schemas.UvDynamicVersioning: 17 | return self.project.tool.uv_dynamic_versioning or schemas.UvDynamicVersioning() 18 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | from pathlib import Path 3 | 4 | from git import Repo 5 | 6 | 7 | @contextlib.contextmanager 8 | def empty_commit(repo: Repo): 9 | repo.git.execute(["git", "commit", "--allow-empty", "-m", "dirty commit"]) 10 | try: 11 | yield 12 | finally: 13 | repo.git.execute(["git", "reset", "--soft", "HEAD~1"]) 14 | 15 | 16 | @contextlib.contextmanager 17 | def dirty(repo: Repo): 18 | assert repo.working_dir 19 | 20 | path = Path(repo.working_dir) / "dirty-file" 21 | path.write_text("dirty") 22 | try: 23 | yield 24 | finally: 25 | path.unlink(missing_ok=True) 26 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v6.0.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-toml 7 | - id: check-yaml 8 | args: 9 | - --unsafe 10 | - id: end-of-file-fixer 11 | - id: trailing-whitespace 12 | 13 | - repo: https://github.com/astral-sh/uv-pre-commit 14 | rev: 0.8.17 15 | hooks: 16 | - id: uv-lock 17 | 18 | - repo: https://github.com/ninoseki/uv-sort 19 | rev: v0.6.1 20 | hooks: 21 | - id: uv-sort 22 | 23 | - repo: https://github.com/charliermarsh/ruff-pre-commit 24 | rev: v0.13.0 25 | hooks: 26 | - id: ruff 27 | args: 28 | - --fix 29 | - id: ruff-format 30 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package 2 | 3 | on: 4 | release: 5 | types: ["created"] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 12 | steps: 13 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 14 | - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 15 | with: 16 | python-version: 3.11 17 | cache: "pip" 18 | - run: pip install -r requirements.txt 19 | - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 20 | with: 21 | path: ~/.cache/uv 22 | key: uv-3.11-${{ hashFiles('uv.lock') }} 23 | - run: uv sync --frozen 24 | - name: build 25 | run: uv build 26 | - name: publish 27 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: ["pull_request", "push"] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.9, "3.10", 3.11, 3.12, 3.13, 3.14] 11 | steps: 12 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 13 | - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 14 | with: 15 | python-version: ${{ matrix.python-version }} 16 | cache: "pip" 17 | - run: pip install -r requirements.txt 18 | - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 19 | with: 20 | path: ~/.cache/uv 21 | key: uv-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }} 22 | - run: uv sync --frozen 23 | - run: | 24 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 25 | git config --global user.name "github-actions[bot]" 26 | - run: uv run pytest 27 | -------------------------------------------------------------------------------- /docs/tips.md: -------------------------------------------------------------------------------- 1 | # Tips 2 | 3 | ## Dependabot 4 | 5 | Dependabot may fail if your project uses Depandabot and `uv-dynamic-versioning` together. 6 | 7 | ```yml 8 | version: 2 9 | updates: 10 | - package-ecosystem: uv 11 | ``` 12 | 13 | This is because Dependabot does `uv lock --upgrade-package {package_name}` and it invokes a build. The build may fail with the following RuntimeError: 14 | 15 | ```text 16 | RuntimeError: Error getting the version from source 17 | `uv-dynamic-versioning`: This does not appear to be a Git project 18 | ``` 19 | 20 | A workaround is setting `fallback-version` in the configuration: 21 | 22 | ```toml 23 | [tool.uv-dynamic-versioning] 24 | fallback-version = "0.0.0" 25 | ``` 26 | 27 | ## Build caching and editable installs 28 | 29 | The `uv` cache is aggressive, and needs to be made aware that the project's metadata is dynamic, for example like this: 30 | 31 | ```toml 32 | [tool.uv] 33 | cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true }}] 34 | ``` 35 | 36 | See [`uv`'s docs on dynamic metadata](https://docs.astral.sh/uv/concepts/cache/#dynamic-metadata) for more information. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Manabu Niseki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "uv-dynamic-versioning" 3 | dynamic = ["version"] 4 | description = "Dynamic versioning based on VCS tags for uv/hatch project" 5 | readme = "README.md" 6 | requires-python = ">=3.9,<4.0" 7 | license = { file = "LICENSE" } 8 | keywords = ["uv", "hatch"] 9 | classifiers = [ 10 | "License :: OSI Approved :: MIT License", 11 | "Programming Language :: Python", 12 | "Programming Language :: Python :: 3.9", 13 | "Programming Language :: Python :: 3.10", 14 | "Programming Language :: Python :: 3.11", 15 | "Programming Language :: Python :: 3.12", 16 | "Framework :: Hatch", 17 | ] 18 | dependencies = [ 19 | "dunamai~=1.25", 20 | "hatchling~=1.26", 21 | "jinja2~=3.0", 22 | "tomlkit~=0.13", 23 | ] 24 | 25 | [project.urls] 26 | Repository = "https://github.com/ninoseki/uv-dynamic-versioning/" 27 | Homepage = "https://github.com/ninoseki/uv-dynamic-versioning/" 28 | 29 | [project.scripts] 30 | uv-dynamic-versioning = "uv_dynamic_versioning.cli:main" 31 | 32 | [project.entry-points.hatch] 33 | uv-dynamic-versioning = "uv_dynamic_versioning.hooks" 34 | 35 | [build-system] 36 | requires = ["hatchling"] 37 | build-backend = "hatchling.build" 38 | 39 | [tool.hatch.version] 40 | path = "src/uv_dynamic_versioning/__init__.py" 41 | 42 | [dependency-groups] 43 | dev = [ 44 | "gitpython~=3.1.45", 45 | "pre-commit~=4.3.0", 46 | "pytest-pretty~=1.3.0", 47 | "pytest-randomly~=4.0.1", 48 | "pytest~=8.4.2", 49 | ] 50 | 51 | [tool.ruff.lint] 52 | select = [ 53 | "B", # flake8-bugbear 54 | "C", # flake8-comprehensions 55 | "E", # pycodestyle errors 56 | "F", # pyflakes 57 | "FA", # flake8-future-annotations 58 | "I", # isort 59 | "N", # pep8-naming 60 | "RET", # flake8-return 61 | "RUF", # Ruff-specific rules 62 | "SIM", # flake8-simplify 63 | "T20", # flake8-print 64 | "UP", # pyupgrade 65 | "W", # pycodestyle warnings 66 | ] 67 | ignore = [ 68 | "E501", # line too long 69 | ] 70 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | import pytest 5 | from git import Repo, TagReference 6 | 7 | PROJECT_ROOT = Path().resolve() 8 | 9 | 10 | @pytest.fixture(scope="session") 11 | def repo(): 12 | return Repo.init() 13 | 14 | 15 | @pytest.fixture(scope="session", autouse=True) 16 | def clear_tags(repo: Repo): 17 | saved_tags = {tag.name: tag.commit.hexsha for tag in repo.tags} 18 | 19 | for tag in repo.tags: 20 | repo.delete_tag(tag) 21 | 22 | yield 23 | 24 | for name, ref in saved_tags.items(): 25 | repo.create_tag(name, ref) 26 | 27 | 28 | @pytest.fixture 29 | def semver_tag(repo: Repo): 30 | tag = repo.create_tag("v1.0.0") 31 | try: 32 | yield tag 33 | finally: 34 | repo.delete_tag(tag) 35 | 36 | 37 | @pytest.fixture 38 | def prerelease_tag(repo: Repo): 39 | tag = repo.create_tag("v1.0.0-alpha1") 40 | try: 41 | yield tag 42 | finally: 43 | repo.delete_tag(tag) 44 | 45 | 46 | @pytest.fixture 47 | def dev_tag(repo: Repo): 48 | tag = repo.create_tag("v1.0.0-dev1") 49 | try: 50 | yield tag 51 | finally: 52 | repo.delete_tag(tag) 53 | 54 | 55 | @pytest.fixture 56 | def uv_dynamic_versioning_bypass_with_semver_tag( 57 | semver_tag: TagReference, 58 | ): 59 | os.environ["UV_DYNAMIC_VERSIONING_BYPASS"] = semver_tag.name 60 | 61 | try: 62 | yield semver_tag.name 63 | finally: 64 | del os.environ["UV_DYNAMIC_VERSIONING_BYPASS"] 65 | 66 | 67 | @pytest.fixture 68 | def uv_dynamic_versioning_bypass_with_prerelease_tag(prerelease_tag: TagReference): 69 | os.environ["UV_DYNAMIC_VERSIONING_BYPASS"] = prerelease_tag.name 70 | 71 | try: 72 | yield prerelease_tag.name 73 | finally: 74 | del os.environ["UV_DYNAMIC_VERSIONING_BYPASS"] 75 | 76 | 77 | @pytest.fixture 78 | def uv_dynamic_versioning_bypass_with_dev_tag(dev_tag: TagReference): 79 | os.environ["UV_DYNAMIC_VERSIONING_BYPASS"] = dev_tag.name 80 | 81 | try: 82 | yield dev_tag.name 83 | finally: 84 | del os.environ["UV_DYNAMIC_VERSIONING_BYPASS"] 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uv-dynamic-versioning 2 | 3 | [![PyPI version](https://badge.fury.io/py/uv-dynamic-versioning.svg)](https://badge.fury.io/py/uv-dynamic-versioning) 4 | 5 | [poetry-dynamic-versioning](https://github.com/mtkennerly/poetry-dynamic-versioning) influenced dynamic versioning tool for [uv](https://github.com/astral-sh/uv)/[hatch](https://github.com/pypa/hatch), powered by [dunamai](https://github.com/mtkennerly/dunamai/). 6 | 7 | > [!NOTE] 8 | > This plugin doesn't work with [the uv build backend](https://docs.astral.sh/uv/concepts/build-backend/) right now. (ref. [astral-sh/uv#14561](https://github.com/astral-sh/uv/issues/14561)) 9 | 10 | ## Installation 11 | 12 | Update or add `build-system` to use `uv-dynamic-versioning`. 13 | 14 | ```toml 15 | [build-system] 16 | requires = ["hatchling", "uv-dynamic-versioning"] 17 | build-backend = "hatchling.build" 18 | ``` 19 | 20 | ## Plugins 21 | 22 | This project offers two plugins: 23 | 24 | - Version source plugin: is for setting a version based on VCS. 25 | - Metadata hook plugin: is for setting dependencies and optional-dependencies dynamically based on VCS version. This plugin is useful for monorepo. 26 | 27 | See [Version Source](https://github.com/ninoseki/uv-dynamic-versioning/blob/main/docs/version_source.md) and [Metadata Hook](https://github.com/ninoseki/uv-dynamic-versioning/blob/main/docs/metadata_hook.md) for more details. 28 | 29 | ## Tips 30 | 31 | See [Tips](https://github.com/ninoseki/uv-dynamic-versioning/blob/main/docs/tips.md). 32 | 33 | ## Examples 34 | 35 | See [Examples](https://github.com/ninoseki/uv-dynamic-versioning/tree/main/examples/). 36 | 37 | ## Projects Using `uv-dynamic-versioning` 38 | 39 | - [microsoft/essex-toolkit](https://github.com/microsoft/essex-toolkit): uses the version source plugin. 40 | - [modelcontextprotocol/python-sdk](https://github.com/modelcontextprotocol/python-sdk): uses the version source plugin. 41 | - [pydantic/pydantic-ai](https://github.com/pydantic/pydantic-ai): uses the version source and the metadata hook plugins. 42 | 43 | And more. 44 | 45 | ## Alternatives 46 | 47 | - [hatch-vcs](https://github.com/ofek/hatch-vcs): Hatch plugin for versioning with your preferred VCS. 48 | - [versioningit](https://github.com/jwodder/versioningit): Versioning It with your Version In Git. 49 | -------------------------------------------------------------------------------- /docs/metadata_hook.md: -------------------------------------------------------------------------------- 1 | # Metadata Hook 2 | 3 | `uv-dynamic-versioning` metadata hook allows you to set `dependencies` and `optional-dependencies` dynamically with a VCS based version. 4 | 5 | > [!NOTE] 6 | > VCS based version configuration is the same as described at [Version Source](./version_source.md). 7 | 8 | Add `[tool.hatch.metadata.hooks.uv-dynamic-versioning]` in your `pyproject.toml` to use it. 9 | 10 | ```toml 11 | [tool.hatch.metadata.hooks.uv-dynamic-versioning] 12 | dependencies = ["foo=={{ version }}"] 13 | ``` 14 | 15 | Also remove `dependencies` in `project` and set them in `project.dynamic` (`dynamic = ["dependencies"]`). 16 | 17 | **Before** 18 | 19 | ```toml 20 | [project] 21 | name = "..." 22 | dependencies = [] 23 | ``` 24 | 25 | **After** 26 | 27 | ```toml 28 | [project] 29 | name = "..." 30 | dynamic = ["dependencies"] 31 | ``` 32 | 33 | `optional-dependencies` can be set in the same way. 34 | 35 | ## Configuration 36 | 37 | - `dependencies`: is a list of Jinja2 templates. `dependencies` should be set in `project.dynamic`. 38 | 39 | Available variables: 40 | 41 | - `version`([dunamai.version](https://dunamai.readthedocs.io/en/latest/#dunamai.Version)) 42 | - `base` (string) 43 | - `stage` (string or None) 44 | - `revision` (integer or None) 45 | - `distance` (integer) 46 | - `commit` (string) 47 | - `dirty` (boolean) 48 | - `tagged_metadata` (string or None) 49 | - `version` (dunumai.Version) 50 | - `env` (dictionary of environment variables) 51 | - `branch` (string or None) 52 | - `branch_escaped` (string or None) 53 | - `timestamp` (string or None) 54 | - `major` (integer) 55 | - `minor` (integer) 56 | - `patch` (integer) 57 | 58 | Available functions: 59 | 60 | - `bump_version` ([from Dunamai](https://dunamai.readthedocs.io/en/latest/#dunamai.bump_version)) 61 | - `serialize_pep440` ([from Dunamai](https://dunamai.readthedocs.io/en/latest/#dunamai.serialize_pep440)) 62 | - `serialize_semver` ([from Dunamai](https://dunamai.readthedocs.io/en/latest/#dunamai.serialize_semver)) 63 | - `serialize_pvp` ([from Dunamai](https://dunamai.readthedocs.io/en/latest/#dunamai.serialize_pvp)) 64 | 65 | - `optional-dependencies`: is an optional dependencies and each dependency is a list of Jinja2 templates. `optional-dependencies` should be set in `project.dynamic`. Available variables are same as the above. 66 | -------------------------------------------------------------------------------- /src/uv_dynamic_versioning/template.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | import os 5 | import re 6 | from datetime import datetime 7 | from importlib import import_module 8 | 9 | import jinja2 10 | from dunamai import ( 11 | Version, 12 | bump_version, 13 | serialize_pep440, 14 | serialize_pvp, 15 | serialize_semver, 16 | ) 17 | 18 | from . import schemas 19 | 20 | 21 | def base_part(base: str, index: int) -> int: 22 | parts = base.split(".") 23 | result = 0 24 | 25 | with contextlib.suppress(KeyError, ValueError): 26 | result = int(parts[index]) 27 | 28 | return result 29 | 30 | 31 | def _escape_branch(value: str | None, escape_with: str | None) -> str | None: 32 | if value is None: 33 | return None 34 | 35 | return re.sub(r"[^a-zA-Z0-9]", escape_with or "", value) 36 | 37 | 38 | def _format_timestamp(value: datetime | None) -> str | None: 39 | if value is None: 40 | return None 41 | 42 | return value.strftime("%Y%m%d%H%M%S") 43 | 44 | 45 | def render_template( 46 | template: str, *, version: Version, config: schemas.UvDynamicVersioning 47 | ) -> str: 48 | default_context = { 49 | "version": version, 50 | "base": version.base, 51 | "stage": version.stage, 52 | "revision": version.revision, 53 | "distance": version.distance, 54 | "commit": version.commit, 55 | "dirty": version.dirty, 56 | "branch": version.branch, 57 | "tagged_metadata": version.tagged_metadata, 58 | "branch_escaped": _escape_branch(version.branch, config.escape_with), 59 | "timestamp": _format_timestamp(version.timestamp), 60 | "major": base_part(version.base, 0), 61 | "minor": base_part(version.base, 1), 62 | "patch": base_part(version.base, 2), 63 | "env": os.environ, 64 | "bump_version": bump_version, 65 | "serialize_pep440": serialize_pep440, 66 | "serialize_pvp": serialize_pvp, 67 | "serialize_semver": serialize_semver, 68 | } 69 | 70 | custom_context = {} 71 | if config.format_jinja_imports: 72 | for entry in config.format_jinja_imports: 73 | module = import_module(entry.module) 74 | if entry.item is not None: 75 | custom_context[entry.item] = getattr(module, entry.item) 76 | else: 77 | custom_context[entry.module] = module 78 | 79 | return jinja2.Template(template).render(**default_context, **custom_context) 80 | -------------------------------------------------------------------------------- /tests/test_schemas.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from uv_dynamic_versioning import schemas 4 | 5 | 6 | def test_bump_config(): 7 | config = schemas.BumpConfig.from_dict({"enable": True, "index": 2}) 8 | assert config.enable is True 9 | assert config.index == 2 10 | 11 | 12 | def test_bump_config_invalid_index(): 13 | with pytest.raises(ValueError): 14 | schemas.BumpConfig.from_dict({"enable": True, "index": "not-an-int"}) 15 | 16 | 17 | def test_uv_dynamic_versioning_from_dict_valid(): 18 | data = { 19 | "vcs": "git", 20 | "metadata": True, 21 | "bump": {"enable": True, "index": 1}, 22 | } 23 | config = schemas.UvDynamicVersioning.from_dict(data) 24 | assert config.vcs.name == "Git" 25 | assert config.metadata is True 26 | assert isinstance(config.bump, schemas.BumpConfig) 27 | 28 | 29 | def test_uv_dynamic_versioning_invalid_vcs(): 30 | with pytest.raises(ValueError): 31 | schemas.UvDynamicVersioning.from_dict({"vcs": "invalid-vcs"}) 32 | 33 | 34 | def test_tool_from_dict_valid(): 35 | data = { 36 | "uv_dynamic_versioning": { 37 | "vcs": "git", 38 | "metadata": False, 39 | } 40 | } 41 | tool = schemas.Tool.from_dict(data) 42 | assert isinstance(tool.uv_dynamic_versioning, schemas.UvDynamicVersioning) 43 | assert tool.uv_dynamic_versioning.vcs.name == "Git" 44 | 45 | 46 | def test_tool_from_dict_invalid(): 47 | with pytest.raises(ValueError): 48 | schemas.Tool.from_dict({"uv_dynamic_versioning": "not-a-dict"}) 49 | 50 | 51 | def test_project_from_dict_valid(): 52 | data = {"tool": {"uv_dynamic_versioning": {"vcs": "git"}}} 53 | project = schemas.Project.from_dict(data) 54 | assert isinstance(project.tool, schemas.Tool) 55 | assert isinstance(project.tool.uv_dynamic_versioning, schemas.UvDynamicVersioning) 56 | 57 | 58 | def test_project_from_dict_missing_tool(): 59 | with pytest.raises(ValueError): 60 | schemas.Project.from_dict({}) 61 | 62 | 63 | def test_metadata_hook_config_valid(): 64 | data = {"dependencies": ["a", "b"], "optional_dependencies": {"extra": ["c", "d"]}} 65 | config = schemas.MetadataHookConfig.from_dict(data) 66 | assert config.dependencies == ["a", "b"] 67 | assert config.optional_dependencies == {"extra": ["c", "d"]} 68 | 69 | 70 | def test_metadata_hook_config_invalid_dependencies(): 71 | with pytest.raises(ValueError): 72 | schemas.MetadataHookConfig.from_dict({"dependencies": "not-a-list"}) 73 | 74 | 75 | def test_metadata_hook_config_invalid_optional_dependencies(): 76 | with pytest.raises(ValueError): 77 | schemas.MetadataHookConfig.from_dict({"optional_dependencies": "not-a-dict"}) 78 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from dunamai import Style, Version 3 | 4 | from uv_dynamic_versioning import schemas 5 | from uv_dynamic_versioning.main import get_version 6 | 7 | 8 | @pytest.mark.usefixtures("semver_tag") 9 | def test_get_version_with_semver_tag(): 10 | config = schemas.UvDynamicVersioning() 11 | assert get_version(config)[0] == "1.0.0" 12 | 13 | 14 | @pytest.mark.usefixtures("prerelease_tag") 15 | def test_get_version_with_prerelease_tag(): 16 | config = schemas.UvDynamicVersioning() 17 | assert get_version(config)[0] == "1.0.0a1" 18 | 19 | 20 | @pytest.mark.usefixtures("dev_tag") 21 | def test_get_version_with_dev_tag(): 22 | config = schemas.UvDynamicVersioning() 23 | assert get_version(config)[0] == "1.0.0.dev1" 24 | 25 | 26 | def test_get_version_with_bump(): 27 | version_without_bump = get_version(schemas.UvDynamicVersioning()) 28 | version_with_bump = get_version(schemas.UvDynamicVersioning(bump=True)) 29 | # bumped version should be greater than non-bumped version 30 | assert Version(version_with_bump[0]) > Version(version_without_bump[0]) 31 | 32 | 33 | def test_get_version_with_bypass(uv_dynamic_versioning_bypass_with_semver_tag: str): 34 | assert get_version(schemas.UvDynamicVersioning()) == ( 35 | uv_dynamic_versioning_bypass_with_semver_tag, 36 | Version.parse(uv_dynamic_versioning_bypass_with_semver_tag), 37 | ) 38 | 39 | 40 | def test_get_version_with_bypass_with_format( 41 | uv_dynamic_versioning_bypass_with_semver_tag: str, 42 | ): 43 | # NOTE: format should be ignored when bypassing 44 | assert get_version( 45 | schemas.UvDynamicVersioning(format="v{base}+{distance}.{commit}") 46 | ) == ( 47 | uv_dynamic_versioning_bypass_with_semver_tag, 48 | Version.parse(uv_dynamic_versioning_bypass_with_semver_tag), 49 | ) 50 | 51 | 52 | @pytest.mark.usefixtures("semver_tag") 53 | def test_get_version_with_invalid_combination_of_format_jinja_and_style(): 54 | config = schemas.UvDynamicVersioning.from_dict( 55 | { 56 | "format-jinja": "invalid", 57 | "style": "pep440", 58 | } 59 | ) 60 | assert config.style == Style.Pep440 61 | with pytest.raises(ValueError): 62 | get_version(config) 63 | 64 | 65 | def test_get_version_with_format_jinja_imports_with_module_only(): 66 | config = schemas.UvDynamicVersioning.from_dict( 67 | { 68 | "format-jinja": "{{ math.pow(2, 2) }}", 69 | "format-jinja-imports": [ 70 | { 71 | "module": "math", 72 | } 73 | ], 74 | } 75 | ) 76 | assert get_version(config)[0] == "4.0" 77 | 78 | 79 | def test_get_version_with_format_jinja_imports_with_item(): 80 | config = schemas.UvDynamicVersioning.from_dict( 81 | { 82 | "format-jinja": "{{ pow(2, 2) }}", 83 | "format-jinja-imports": [ 84 | { 85 | "module": "math", 86 | "item": "pow", 87 | } 88 | ], 89 | } 90 | ) 91 | assert get_version(config)[0] == "4.0" 92 | -------------------------------------------------------------------------------- /src/uv_dynamic_versioning/metadata_hook.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from functools import cached_property 4 | 5 | from dunamai import Version 6 | from hatchling.metadata.plugin.interface import MetadataHookInterface 7 | 8 | from . import schemas 9 | from .base import BasePlugin 10 | from .main import get_version 11 | from .template import render_template 12 | 13 | 14 | class DependenciesMetadataHook(BasePlugin, MetadataHookInterface): 15 | """ 16 | Hatch metadata hook to populate `project.dependencies` and `project.optional-dependencies` 17 | """ 18 | 19 | PLUGIN_NAME = "uv-dynamic-versioning" 20 | 21 | @cached_property 22 | def plugin_config(self) -> schemas.MetadataHookConfig: 23 | return schemas.MetadataHookConfig.from_dict(self.config) 24 | 25 | @cached_property 26 | def version(self) -> Version: 27 | _, version = get_version(self.project_config) 28 | return version 29 | 30 | def render_dependencies(self) -> list[str] | None: 31 | if self.plugin_config.dependencies is None: 32 | return None 33 | 34 | return [ 35 | render_template(dep, version=self.version, config=self.project_config) 36 | for dep in self.plugin_config.dependencies 37 | ] 38 | 39 | def render_optional_dependencies(self) -> dict[str, list[str]] | None: 40 | if self.plugin_config.optional_dependencies is None: 41 | return None 42 | 43 | return { 44 | name: [ 45 | render_template(dep, version=self.version, config=self.project_config) 46 | for dep in deps 47 | ] 48 | for name, deps in self.plugin_config.optional_dependencies.items() 49 | } 50 | 51 | def update(self, metadata: dict) -> None: 52 | # check dynamic 53 | dynamic = metadata.get("dynamic", []) 54 | is_dynamic_dependencies = "dependencies" in dynamic 55 | is_dynamic_optional_dependencies = "optional-dependencies" in dynamic 56 | if not (is_dynamic_dependencies or is_dynamic_optional_dependencies): 57 | raise ValueError( 58 | "Cannot use this plugin when 'dependencies' or 'optional-dependencies' is not listed in 'project.dynamic'." 59 | ) 60 | 61 | # check consistency between dynamic and project 62 | has_dependencies = "dependencies" in metadata 63 | if is_dynamic_dependencies and has_dependencies: 64 | raise ValueError( 65 | "'dependencies' is dynamic but already listed in [project]." 66 | ) 67 | 68 | has_optional_dependencies = "optional-dependencies" in metadata 69 | if is_dynamic_optional_dependencies and has_optional_dependencies: 70 | raise ValueError( 71 | "'optional-dependencies' is dynamic but already listed in [project]." 72 | ) 73 | 74 | has_dependencies = self.plugin_config.dependencies is not None 75 | has_optional_dependencies = self.plugin_config.optional_dependencies is not None 76 | if not (has_dependencies or has_optional_dependencies): 77 | raise ValueError( 78 | "No dependencies or optional-dependencies found in the plugin config." 79 | ) 80 | 81 | rendered_dependencies = self.render_dependencies() 82 | if rendered_dependencies: 83 | metadata["dependencies"] = rendered_dependencies 84 | 85 | rendered_optional_dependencies = self.render_optional_dependencies() 86 | if rendered_optional_dependencies: 87 | metadata["optional-dependencies"] = rendered_optional_dependencies 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /tests/test_version_source.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Generator 2 | from unittest.mock import PropertyMock, patch 3 | 4 | import pytest 5 | from git import Repo, TagReference 6 | 7 | from uv_dynamic_versioning.version_source import DynamicVersionSource 8 | 9 | from .utils import dirty, empty_commit 10 | 11 | 12 | @pytest.fixture 13 | def mock_root() -> Generator[PropertyMock, None, None]: 14 | with patch( 15 | "uv_dynamic_versioning.version_source.DynamicVersionSource.root", 16 | new_callable=PropertyMock, 17 | ) as mock: 18 | yield mock 19 | 20 | 21 | def test_with_semver_tag(semver_tag: TagReference, mock_root: PropertyMock): 22 | source = DynamicVersionSource(str(semver_tag.repo.working_dir), {}) 23 | mock_root.return_value = "tests/fixtures/with-pep440/" 24 | 25 | version = source.get_version_data()["version"] 26 | assert version == "1.0.0" 27 | 28 | 29 | def test_with_prerelease_tag(prerelease_tag: TagReference, mock_root: PropertyMock): 30 | source = DynamicVersionSource(str(prerelease_tag.repo.working_dir), {}) 31 | mock_root.return_value = "tests/fixtures/with-pep440/" 32 | 33 | version = source.get_version_data()["version"] 34 | assert version == "1.0.0a1" 35 | 36 | 37 | def test_with_dev_tag(dev_tag: TagReference, mock_root: PropertyMock): 38 | source = DynamicVersionSource(str(dev_tag.repo.working_dir), {}) 39 | mock_root.return_value = "tests/fixtures/with-pep440/" 40 | 41 | version = source.get_version_data()["version"] 42 | assert version == "1.0.0.dev1" 43 | 44 | 45 | def test_with_semver(semver_tag: TagReference, mock_root: PropertyMock): 46 | source = DynamicVersionSource(str(semver_tag.repo.working_dir), {}) 47 | mock_root.return_value = "tests/fixtures/with-semver/" 48 | 49 | version = source.get_version_data()["version"] 50 | assert version == "1.0.0" 51 | 52 | 53 | def test_with_format(semver_tag: TagReference, mock_root: PropertyMock): 54 | source = DynamicVersionSource(str(semver_tag.repo.working_dir), {}) 55 | mock_root.return_value = "tests/fixtures/with-format/" 56 | 57 | version: str = source.get_version_data()["version"] 58 | assert version.startswith("v1.0.0+") 59 | 60 | 61 | def test_with_bump(semver_tag: TagReference, mock_root: PropertyMock, repo: Repo): 62 | source = DynamicVersionSource(str(semver_tag.repo.working_dir), {}) 63 | mock_root.return_value = "tests/fixtures/with-bump/" 64 | 65 | with empty_commit(repo): 66 | version: str = source.get_version_data()["version"] 67 | 68 | assert version.startswith("1.0.1.") 69 | 70 | 71 | def test_with_dirty(semver_tag: TagReference, mock_root: PropertyMock, repo: Repo): 72 | source = DynamicVersionSource(str(semver_tag.repo.working_dir), {}) 73 | mock_root.return_value = "tests/fixtures/with-dirty/" 74 | 75 | with dirty(repo): 76 | version: str = source.get_version_data()["version"] 77 | 78 | assert version.endswith("+dirty") 79 | 80 | 81 | def test_with_jinja2_format(semver_tag: TagReference, mock_root: PropertyMock): 82 | source = DynamicVersionSource(str(semver_tag.repo.working_dir), {}) 83 | mock_root.return_value = "tests/fixtures/with-jinja-format/" 84 | 85 | version: str = source.get_version_data()["version"] 86 | assert version.startswith("1.0.0.dev0+g") 87 | 88 | 89 | def test_with_pattern(semver_tag: TagReference, mock_root: PropertyMock): 90 | source = DynamicVersionSource(str(semver_tag.repo.working_dir), {}) 91 | mock_root.return_value = "tests/fixtures/with-pattern/" 92 | 93 | version: str = source.get_version_data()["version"] 94 | assert version == "1" 95 | 96 | 97 | def test_from_file(semver_tag: TagReference, mock_root: PropertyMock): 98 | source = DynamicVersionSource(str(semver_tag.repo.working_dir), {}) 99 | mock_root.return_value = "tests/fixtures/with-from-file/" 100 | 101 | version: str = source.get_version_data()["version"] 102 | assert version == "1.0.0" 103 | -------------------------------------------------------------------------------- /tests/test_metadata_hook.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Generator 2 | from unittest.mock import PropertyMock, patch 3 | 4 | import pytest 5 | from git import Repo, TagReference 6 | 7 | from uv_dynamic_versioning.metadata_hook import DependenciesMetadataHook 8 | 9 | from .utils import dirty 10 | 11 | 12 | def test_without_dynamic_dependencies(semver_tag: TagReference): 13 | hook = DependenciesMetadataHook(str(semver_tag.repo.working_dir), {}) 14 | 15 | with pytest.raises(ValueError): 16 | hook.update({}) 17 | 18 | 19 | @pytest.fixture 20 | def mock_config() -> Generator[PropertyMock, None, None]: 21 | with patch( 22 | "uv_dynamic_versioning.metadata_hook.DependenciesMetadataHook.config", 23 | new_callable=PropertyMock, 24 | ) as mock: 25 | yield mock 26 | 27 | 28 | @pytest.fixture 29 | def mock_root() -> Generator[PropertyMock, None, None]: 30 | with patch( 31 | "uv_dynamic_versioning.metadata_hook.DependenciesMetadataHook.root", 32 | new_callable=PropertyMock, 33 | ) as mock: 34 | yield mock 35 | 36 | 37 | def test_without_dependencies_in_config( 38 | semver_tag: TagReference, mock_config: PropertyMock 39 | ): 40 | mock_config.return_value = {} 41 | hook = DependenciesMetadataHook(str(semver_tag.repo.working_dir), {}) 42 | with pytest.raises(ValueError): 43 | hook.update( 44 | { 45 | "dynamic": ["dependencies"], 46 | } 47 | ) 48 | 49 | 50 | def test_render_dependencies_with_semver( 51 | semver_tag: TagReference, mock_config: PropertyMock 52 | ): 53 | mock_config.return_value = { 54 | "dependencies": ["foo=={{ version }}"], 55 | } 56 | 57 | hook = DependenciesMetadataHook(str(semver_tag.repo.working_dir), {}) 58 | 59 | assert hook.render_dependencies() == ["foo==1.0.0"] 60 | assert ( 61 | hook.update( 62 | { 63 | "dynamic": ["dependencies"], 64 | } 65 | ) 66 | is None 67 | ) 68 | 69 | 70 | def test_render_dependencies_with_dirty( 71 | semver_tag: TagReference, 72 | mock_config: PropertyMock, 73 | mock_root: PropertyMock, 74 | repo: Repo, 75 | ): 76 | mock_config.return_value = { 77 | "dependencies": ["foo=={{ version }}"], 78 | } 79 | 80 | mock_root.return_value = "tests/fixtures/with-dirty/" 81 | 82 | hook = DependenciesMetadataHook(str(semver_tag.repo.working_dir), {}) 83 | 84 | with dirty(repo): 85 | dependencies = hook.render_dependencies() or [] 86 | 87 | assert len(dependencies) == 1 88 | assert dependencies[0].endswith("+dirty") 89 | 90 | 91 | def test_render_dependencies_with_prerelease_tag( 92 | prerelease_tag: TagReference, 93 | mock_config: PropertyMock, 94 | ): 95 | mock_config.return_value = { 96 | "dependencies": ["foo=={{ version }}"], 97 | } 98 | 99 | hook = DependenciesMetadataHook(str(prerelease_tag.repo.working_dir), {}) 100 | 101 | dependencies = hook.render_dependencies() or [] 102 | 103 | assert len(dependencies) == 1 104 | assert dependencies[0].endswith("a1") 105 | 106 | 107 | def test_render_dependencies_with_bypass_with_semver_tag( 108 | semver_tag: TagReference, 109 | mock_config: PropertyMock, 110 | ): 111 | mock_config.return_value = { 112 | "dependencies": ["foo=={{ version }}"], 113 | } 114 | 115 | hook = DependenciesMetadataHook(str(semver_tag.repo.working_dir), {}) 116 | 117 | dependencies = hook.render_dependencies() or [] 118 | 119 | assert len(dependencies) == 1 120 | assert dependencies[0] == "foo==1.0.0" 121 | 122 | 123 | def test_render_dependencies_with_bypass_and_prerelease_tag( 124 | prerelease_tag: TagReference, 125 | mock_config: PropertyMock, 126 | ): 127 | mock_config.return_value = { 128 | "dependencies": ["foo=={{ version }}"], 129 | } 130 | 131 | hook = DependenciesMetadataHook(str(prerelease_tag.repo.working_dir), {}) 132 | 133 | dependencies = hook.render_dependencies() or [] 134 | 135 | assert len(dependencies) == 1 136 | assert dependencies[0] == "foo==1.0.0a1" 137 | 138 | 139 | def test_render_dependencies_with_bypass_and_dev_tag( 140 | dev_tag: TagReference, 141 | mock_config: PropertyMock, 142 | ): 143 | mock_config.return_value = { 144 | "dependencies": ["foo=={{ version }}"], 145 | } 146 | 147 | hook = DependenciesMetadataHook(str(dev_tag.repo.working_dir), {}) 148 | 149 | dependencies = hook.render_dependencies() or [] 150 | 151 | assert len(dependencies) == 1 152 | assert dependencies[0] == "foo==1.0.0.dev1" 153 | -------------------------------------------------------------------------------- /src/uv_dynamic_versioning/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import re 5 | from functools import partial 6 | from pathlib import Path 7 | 8 | import tomlkit 9 | from dunamai import _VALID_PEP440, _VALID_PVP, _VALID_SEMVER, Style, Version 10 | 11 | from uv_dynamic_versioning.template import render_template 12 | 13 | from . import schemas 14 | 15 | 16 | def read(root: str): 17 | pyproject = Path(root) / "pyproject.toml" 18 | return pyproject.read_text() 19 | 20 | 21 | def parse(text: str): 22 | return tomlkit.parse(text) 23 | 24 | 25 | def validate(project: tomlkit.TOMLDocument): 26 | return schemas.Project.from_dict(project.unwrap()) 27 | 28 | 29 | def _get_bypassed_version() -> str | None: 30 | return os.environ.get("UV_DYNAMIC_VERSIONING_BYPASS") 31 | 32 | 33 | def check_version_style(version: str, style: Style = Style.Pep440) -> None: 34 | """Check if a version is valid for a style.""" 35 | name, pattern = { 36 | Style.Pep440: ("PEP 440", _VALID_PEP440), 37 | Style.SemVer: ("Semantic Versioning", _VALID_SEMVER), 38 | Style.Pvp: ("PVP", _VALID_PVP), 39 | }[style] 40 | failure_message = f"Version '{version}' does not conform to the {name} style" 41 | if not re.search(pattern, version): 42 | raise ValueError(failure_message) 43 | 44 | if style == Style.SemVer: 45 | parts = re.split(r"[.-]", version.split("+", 1)[0]) 46 | if any(re.search(r"^0[0-9]+$", x) for x in parts): 47 | raise ValueError(failure_message) 48 | 49 | 50 | def _get_from_file_version(config: schemas.UvDynamicVersioning) -> str | None: 51 | if config.from_file is None: 52 | return None 53 | 54 | source, pattern = (config.from_file.source, config.from_file.pattern) 55 | content = Path(source).read_text().strip() 56 | 57 | if pattern is None: 58 | return content 59 | 60 | result = re.search(pattern, content, re.MULTILINE) 61 | if result is None: 62 | raise ValueError(f"File '{source}' did not contain a match for '{pattern}'") 63 | return str(result.group(1)) 64 | 65 | 66 | def _patch_version_serialize( 67 | version: Version, config: schemas.UvDynamicVersioning 68 | ) -> Version: 69 | # FIXME: a dirty hack to override serialize method 70 | # __str__ uses self.serialize() internally, 71 | # so we need to override it to apply config based parameters in the hooks 72 | version.serialize = partial( # type: ignore 73 | version.serialize, 74 | metadata=config.metadata, 75 | style=config.style, 76 | dirty=config.dirty, 77 | tagged_metadata=config.tagged_metadata, 78 | format=config.format, 79 | escape_with=config.escape_with, 80 | commit_prefix=config.commit_prefix, 81 | ) 82 | return version 83 | 84 | 85 | def _get_version(config: schemas.UvDynamicVersioning) -> Version: 86 | try: 87 | return Version.from_vcs( 88 | config.vcs, 89 | latest_tag=config.latest_tag, 90 | strict=config.strict, 91 | tag_branch=config.tag_branch, 92 | tag_dir=config.tag_dir, 93 | full_commit=config.full_commit, 94 | ignore_untracked=config.ignore_untracked, 95 | pattern=config.pattern, 96 | pattern_prefix=config.pattern_prefix, 97 | commit_length=config.commit_length, 98 | ) 99 | except RuntimeError as e: 100 | if fallback_version := config.fallback_version: 101 | return Version(fallback_version) 102 | raise e 103 | 104 | 105 | def get_version(config: schemas.UvDynamicVersioning) -> tuple[str, Version]: 106 | bypassed = _get_bypassed_version() 107 | if bypassed: 108 | parsed = Version.parse(bypassed, pattern=config.pattern) 109 | return bypassed, _patch_version_serialize(parsed, config) 110 | 111 | from_file = _get_from_file_version(config) 112 | if from_file: 113 | parsed = Version.parse(from_file, pattern=config.pattern) 114 | return from_file, _patch_version_serialize(parsed, config) 115 | 116 | got = _get_version(config) 117 | version = _patch_version_serialize(got, config) 118 | 119 | if config.format_jinja: 120 | updated = ( 121 | version.bump(index=config.bump_config.index) 122 | if config.bump_config.enable and version.distance > 0 123 | else version 124 | ) 125 | serialized = render_template( 126 | config.format_jinja, version=updated, config=config 127 | ) 128 | if config.style: 129 | check_version_style(serialized, config.style) 130 | else: 131 | updated = ( 132 | version.bump(smart=True, index=config.bump_config.index) 133 | if config.bump_config.enable 134 | else version 135 | ) 136 | serialized = updated.serialize() 137 | 138 | return (serialized, updated) 139 | -------------------------------------------------------------------------------- /tests/test_template.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime, timezone 4 | 5 | import pytest 6 | from dunamai import Version 7 | 8 | from uv_dynamic_versioning import schemas 9 | from uv_dynamic_versioning.template import render_template 10 | 11 | 12 | @pytest.fixture 13 | def version(): 14 | return Version( 15 | base="1.0.0", 16 | stage=("alpha", 1), 17 | distance=0, 18 | commit="message", 19 | dirty=False, 20 | branch="main", 21 | timestamp=datetime(2025, 4, 1, 12, 0, 0, tzinfo=timezone.utc), 22 | ) 23 | 24 | 25 | @pytest.fixture 26 | def config(): 27 | return schemas.UvDynamicVersioning() 28 | 29 | 30 | def test_when_rendering_basic_version_then_returns_base_version( 31 | version: Version, config: schemas.UvDynamicVersioning 32 | ): 33 | assert render_template("{{- base }}", version=version, config=config) == "1.0.0" 34 | 35 | 36 | def test_when_bumping_version_then_returns_bumped_version( 37 | version: Version, config: schemas.UvDynamicVersioning 38 | ): 39 | version.distance = 2 40 | version.revision = 1 41 | assert ( 42 | render_template("{{- base }}+r{{- revision }}", version=version, config=config) 43 | == "1.0.0+r1" 44 | ) 45 | 46 | 47 | def test_when_rendering_version_with_stage_and_revision_then_returns_formatted_version( 48 | version: Version, 49 | config: schemas.UvDynamicVersioning, 50 | ): 51 | assert ( 52 | render_template( 53 | "{{- base }}{{- stage }}{{- revision }}", version=version, config=config 54 | ) 55 | == "1.0.0alpha1" 56 | ) 57 | 58 | 59 | def test_when_rendering_version_with_commit_and_branch_then_returns_formatted_version( 60 | version: Version, 61 | config: schemas.UvDynamicVersioning, 62 | ): 63 | assert ( 64 | render_template("{{- commit }}-{{- branch }}", version=version, config=config) 65 | == "message-main" 66 | ) 67 | 68 | 69 | def test_when_rendering_version_with_timestamp_then_returns_formatted_timestamp( 70 | version: Version, 71 | config: schemas.UvDynamicVersioning, 72 | ): 73 | result = render_template("{{- timestamp }}", version=version, config=config) 74 | assert result == "20250401120000" 75 | 76 | 77 | def test_when_rendering_version_with_individual_parts_then_returns_formatted_version( 78 | version: Version, 79 | config: schemas.UvDynamicVersioning, 80 | ): 81 | assert ( 82 | render_template( 83 | "{{- major }}.{{- minor }}.{{- patch }}", version=version, config=config 84 | ) 85 | == "1.0.0" 86 | ) 87 | 88 | 89 | @pytest.mark.parametrize( 90 | "escape_with, expected", [(None, "featurenewbranch"), (".", "feature.new.branch")] 91 | ) 92 | def test_when_rendering_version_with_escaped_branch_then_returns_escaped_branch( 93 | version: Version, 94 | config: schemas.UvDynamicVersioning, 95 | escape_with: str | None, 96 | expected: str, 97 | ): 98 | config.escape_with = escape_with 99 | version.branch = "feature/new-branch" 100 | assert ( 101 | render_template("{{- branch_escaped }}", version=version, config=config) 102 | == expected 103 | ) 104 | 105 | 106 | def test_when_rendering_version_with_dirty_flag_then_returns_dirty_status( 107 | version: Version, 108 | config: schemas.UvDynamicVersioning, 109 | ): 110 | version.dirty = True 111 | assert ( 112 | render_template( 113 | "{{- 'dirty' if dirty else 'clean' }}", version=version, config=config 114 | ) 115 | == "dirty" 116 | ) 117 | 118 | 119 | def test_when_rendering_version_with_environment_variables_then_returns_env_value( 120 | version: Version, 121 | monkeypatch: pytest.MonkeyPatch, 122 | config: schemas.UvDynamicVersioning, 123 | ): 124 | monkeypatch.setenv("TEST_VAR", "test_value") 125 | assert ( 126 | render_template("{{ env.TEST_VAR }}", version=version, config=config) 127 | == "test_value" 128 | ) 129 | 130 | 131 | def test_when_rendering_version_with_serialization_functions_then_returns_serialized_version( 132 | version: Version, 133 | config: schemas.UvDynamicVersioning, 134 | ): 135 | assert ( 136 | render_template( 137 | "{{ serialize_pep440(base, stage, revision) }}", 138 | version=version, 139 | config=config, 140 | ) 141 | == "1.0.0a1" 142 | ) 143 | 144 | 145 | def test_when_rendering_version_with_serialization_functions_and_bump_then_returns_bumped_serialized_version( 146 | version: Version, 147 | config: schemas.UvDynamicVersioning, 148 | ): 149 | assert ( 150 | render_template( 151 | "{{ serialize_pep440(bump_version(base), stage, revision) }}", 152 | version=version, 153 | config=config, 154 | ) 155 | == "1.0.1a1" 156 | ) 157 | 158 | 159 | def test_when_rendering_version_with_tagged_metadata_then_returns_metadata( 160 | version: Version, 161 | config: schemas.UvDynamicVersioning, 162 | ): 163 | version.tagged_metadata = "build123" 164 | assert ( 165 | render_template("{{ tagged_metadata }}", version=version, config=config) 166 | == "build123" 167 | ) 168 | -------------------------------------------------------------------------------- /docs/version_source.md: -------------------------------------------------------------------------------- 1 | # Version Source 2 | 3 | `uv-dynamic-versioning` version source allows you to set a version based on VCS. 4 | 5 | Add `tool.hatch.version` & `build-system` in your `pyproject.toml` and configure them to use `uv-dynamic-versioning`. 6 | 7 | ```toml 8 | [build-system] 9 | requires = ["hatchling", "uv-dynamic-versioning"] 10 | build-backend = "hatchling.build" 11 | ``` 12 | 13 | Also remove `version` in `project` and set it in `project.dynamic` (`dynamic = ["version"]`). 14 | 15 | **Before** 16 | 17 | ```toml 18 | [project] 19 | name = "..." 20 | version = "0.1.0" 21 | ``` 22 | 23 | **After** 24 | 25 | ```toml 26 | [project] 27 | name = "..." 28 | dynamic = ["version"] 29 | ``` 30 | 31 | Then this plugin works out of the box (defaults to using the semver style). 32 | 33 | For example: 34 | 35 | ```bash 36 | $ git tag v1.0.0 37 | $ uv build 38 | Building source distribution... 39 | Building wheel from source distribution... 40 | Successfully built dist/foo-1.0.0.tar.gz 41 | Successfully built dist/foo-1.0.0-py3-none-any.whl 42 | # check METADATA file (ref. https://packaging.python.org/en/latest/specifications/core-metadata/) 43 | $ tar -xf dist/foo-1.0.0-py3-none-any.whl 44 | $ head foo-1.0.0.dist-info/METADATA 45 | Metadata-Version: 2.4 46 | Name: foo 47 | Version: 1.0.0 48 | ``` 49 | 50 | > [!NOTE] 51 | > You can use `uv-dynamic-versioning` command to check the version to be used: 52 | > 53 | > ```bash 54 | > $ uvx uv-dynamic-versioning 55 | > 1.0.0 56 | > ``` 57 | 58 | ## Configuration 59 | 60 | > [!NOTE] 61 | > 62 | > - Configuration is almost same as [poetry-dynamic-versioning](https://github.com/mtkennerly/poetry-dynamic-versioning). But `format-jinja-imports` and `fix-shallow-repository` are not supported. 63 | > - The following descriptions are excerpts from [poetry-dynamic-versioning](https://github.com/mtkennerly/poetry-dynamic-versioning). 64 | 65 | You may configure the following options under `[tool.uv-dynamic-versioning]`: 66 | 67 | - `vcs` (string, default: `any`): This is the version control system to check for a version. One of: `any`, `git`, `mercurial`, `darcs`, `bazaar`, `subversion`, `fossil`, `pijul`. 68 | - `metadata` (boolean, default: unset): If true, include the commit hash in the version, and also include a dirty flag if `dirty` is true. If unset, metadata will only be included if you are on a commit without a version tag. This is ignored when `format` or `format-jinja` is used. 69 | - `tagged-metadata` (boolean, default: false): If true, include any tagged metadata discovered as the first part of the metadata segment. Has no effect when `metadata` is set to false. This is ignored when `format` or `format-jinja` is used. 70 | - `dirty` (boolean, default: false): If true, include a dirty flag in the metadata, indicating whether there are any uncommitted changes. Has no effect when `metadata` is set to false. This is ignored when `format` or `format-jinja` is used. 71 | - `pattern` (string): This is a regular expression which will be used to find a tag representing a version. When this is unset, Dunamai's default pattern is used. There must be a capture group named `base` with the main part of the version. Optionally, it may contain another two groups named `stage` and `revision` for prereleases, and it may contain a group named `tagged_metadata` to be used with the `tagged-metadata` option. There may also be a group named `epoch` for the PEP 440 concept. If the `base` group is not included, then this will be interpreted as a named preset from the Dunamai `Pattern` class. This includes: `default`, `default-unprefixed` (makes the `v` prefix optional). You can check the default for your installed version of Dunamai by running this command: 72 | 73 | ```bash 74 | uv run python -c "import dunamai; print(dunamai.Pattern.Default.regex())" 75 | ``` 76 | 77 | Remember that backslashes must be escaped in the TOML file. 78 | 79 | ```toml 80 | # Regular expression: 81 | pattern = '(?P\d+\.\d+\.\d+)' 82 | # Named preset: 83 | pattern = "default-unprefixed" 84 | ``` 85 | 86 | - `pattern-prefix` (string): This will be inserted after the pattern's start anchor (`^`). For example, to match tags like `some-package-v1.2.3`, you can keep the default pattern and set the prefix to `some-package-`. 87 | - `format` (string, default: unset): This defines a custom output format for the version. Available substitutions: 88 | 89 | - `{base}` 90 | - `{stage}` 91 | - `{revision}` 92 | - `{distance}` 93 | - `{commit}` 94 | - `{dirty}` 95 | - `{tagged_metadata}` 96 | - `{branch}` 97 | - `{branch_escaped}` which omits any non-letter/number characters 98 | - `{timestamp}` of the current commit, which expands to YYYYmmddHHMMSS as UTC 99 | 100 | Example: `v{base}+{distance}.{commit}` 101 | 102 | - `format-jinja` (string, default: unset): 103 | This defines a custom output format for the version, using a [Jinja](https://pypi.org/project/Jinja2) template. When this is set, `format` is ignored. 104 | 105 | Available variables: 106 | 107 | - `version`([dunamai.version](https://dunamai.readthedocs.io/en/latest/#dunamai.Version)) 108 | - `base` (string) 109 | - `stage` (string or None) 110 | - `revision` (integer or None) 111 | - `distance` (integer) 112 | - `commit` (string) 113 | - `dirty` (boolean) 114 | - `tagged_metadata` (string or None) 115 | - `version` (dunumai.Version) 116 | - `env` (dictionary of environment variables) 117 | - `branch` (string or None) 118 | - `branch_escaped` (string or None) 119 | - `timestamp` (string or None) 120 | - `major` (integer) 121 | - `minor` (integer) 122 | - `patch` (integer) 123 | 124 | Available functions: 125 | 126 | - `bump_version` ([from Dunamai](https://dunamai.readthedocs.io/en/latest/#dunamai.bump_version)) 127 | - `serialize_pep440` ([from Dunamai](https://dunamai.readthedocs.io/en/latest/#dunamai.serialize_pep440)) 128 | - `serialize_semver` ([from Dunamai](https://dunamai.readthedocs.io/en/latest/#dunamai.serialize_semver)) 129 | - `serialize_pvp` ([from Dunamai](https://dunamai.readthedocs.io/en/latest/#dunamai.serialize_pvp)) 130 | 131 | Simple example: 132 | 133 | ```toml 134 | format-jinja = "{% if distance == 0 %}{{ base }}{% else %}{{ base }}+{{ distance }}.{{ commit }}{% endif %}" 135 | ``` 136 | 137 | Complex example: 138 | 139 | ```toml 140 | format-jinja = """ 141 | {%- if distance == 0 -%} 142 | {{ serialize_pep440(base, stage, revision) }} 143 | {%- elif revision is not none -%} 144 | {{ serialize_pep440(base, stage, revision + 1, dev=distance, metadata=[commit]) }} 145 | {%- else -%} 146 | {{ serialize_pep440(bump_version(base), stage, revision, dev=distance, metadata=[commit]) }} 147 | {%- endif -%} 148 | """ 149 | ``` 150 | 151 | - `format-jinja-imports` (array of tables, default: empty): 152 | This defines additional things to import and make available to the `format-jinja` template. 153 | Each table must contain a `module` key and may also contain an `item` key. Consider this example: 154 | 155 | ```toml 156 | format-jinja-imports = [ 157 | { module = "foo" }, 158 | { module = "bar", item = "baz" }, 159 | ] 160 | ``` 161 | 162 | This is roughly equivalent to: 163 | 164 | ```python 165 | import foo 166 | from bar import baz 167 | ``` 168 | 169 | `foo` and `baz` would then become available in the Jinja formatting. 170 | 171 | - `style` (string, default: unset): One of: `pep440`, `semver`, `pvp`. These are pre-configured output formats. If you set both a `style` and a `format`, then the format will be validated against the style's rules. If `style` is unset, the default output format will follow PEP 440, but a custom `format` will only be validated if `style` is set explicitly. 172 | Regardless of the style you choose, the dynamic version is ultimately subject to Hatchling's validation as well, and Hatchling is designed around PEP 440 versions. Hatchling can usually understand SemVer/etc input, but sometimes, Hatchling may reject an otherwise valid version format. 173 | - `latest-tag` (boolean, default: false): If true, then only check the latest tag for a version, rather than looking through all the tags until a suitable one is found to match the `pattern`. 174 | - `bump` (boolean or table, default: false): If enabled, then increment the last part of the version `base` by 1, unless the `stage` is set, in which case increment the `revision` by 1 or set it to a default of 2 if there was no `revision`. Does nothing when on a commit with a version tag. One of: 175 | 176 | - When set to a boolean, true means enable bumping, with other settings as default. 177 | - When set to a table, these fields are allowed: 178 | - `enable` (boolean, default: false): 179 | If true, enable bumping. 180 | - `index` (integer, default: -1): 181 | Numerical position to increment in the base. 182 | This follows Python indexing rules, so positive numbers start from 183 | the left side and count up from 0, while negative numbers start from 184 | the right side and count down from -1. 185 | 186 | Example, if there have been 3 commits since the `v1.3.1` tag: 187 | 188 | - PEP 440 with `bump = false`: `1.3.1.post3.dev0+28c1684` 189 | - PEP 440 with `bump = true`: `1.3.2.dev3+28c1684` 190 | 191 | - `tag-branch` (string, default: unset): Branch on which to find tags, if different than the current branch. This is only used for Git currently. 192 | - `full-commit` (boolean, default: false): If true, get the full commit hash instead of the short form. 193 | This is only used for Git and Mercurial. 194 | - `strict` (boolean, default: false): If true, then fail instead of falling back to 0.0.0 when there are no tags. 195 | - `ignore-untracked` (boolean, default: false): If true, ignore untracked files when determining whether the repository is dirty. 196 | - `commit-length` (integer, default: unset): Use this many characters from the start of the full commit hash. 197 | - `commit-prefix` (string, default: unset): Add this prefix to the commit ID when serializing. This can be helpful when an all-numeric commit would be misinterpreted. For example, "g" is a common prefix for Git commits. 198 | - `escape-with` (string, default: unset): When escaping, replace invalid characters with this substitution. The default is simply to remove invalid characters. 199 | - `fallback-version` (str, default: unset): Version to be used if an error occurs when obtaining the version, for example, there is no `.git/`. If not specified, unsuccessful version obtaining from vcs will raise an error. 200 | - `from-file`: 201 | This section lets you read the version from a file instead of the VCS. 202 | - `source` (string): 203 | If set, read the version from this file. 204 | It must be a path relative to the location of pyproject.toml. 205 | By default, the plugin will read the entire content of the file, 206 | without leading and trailing whitespace. 207 | - `pattern` (string): 208 | If set, use this regular expression to extract the version from the file. 209 | The first capture group must contain the version. 210 | 211 | ### Examples 212 | 213 | Default (no `tool.uv-dynamic-versioning` in `pyproject.toml`): 214 | 215 | ```bash 216 | $ git tag v1.0.0 217 | $ uv build 218 | Building source distribution... 219 | Building wheel from source distribution... 220 | Successfully built dist/foo-1.0.0.tar.gz 221 | Successfully built dist/foo-1.0.0-py3-none-any.whl 222 | ``` 223 | 224 | With `pattern`: 225 | 226 | ```toml 227 | [tool.uv-dynamic-versioning] 228 | pattern = "default-unprefixed" 229 | ``` 230 | 231 | ```bash 232 | $ git tag 1.0.0 233 | $ uv build 234 | Building source distribution... 235 | Building wheel from source distribution... 236 | Successfully built dist/foo-1.0.0.tar.gz 237 | Successfully built dist/foo-1.0.0-py3-none-any.whl 238 | ``` 239 | 240 | ## Environment Variables 241 | 242 | In addition to the project-specific configuration above, you can apply some global overrides via environment variables. 243 | 244 | - `UV_DYNAMIC_VERSIONING_BYPASS`: 245 | Use this to bypass the VCS mechanisms and use a static version instead. 246 | The value of the environment variable will be used as the version for the active project and any path/SSH dependencies that also use the plugin. 247 | This is mainly for distro package maintainers who need to patch existing releases, without needing access to the original repository. 248 | 249 | ## `__version__` Attribute 250 | 251 | You may want to set `__version__` attribute in your library. There are two ways for that. Using [importlib.metadata](https://docs.python.org/3/library/importlib.metadata.html) and using [version build hook](https://hatch.pypa.io/1.9/plugins/build-hook/version/). 252 | 253 | ### `importlib.metadata` 254 | 255 | > [!NOTE] 256 | > This is very handy, but it's known that `importlib.metadata` is relatively slow. 257 | > Don't use this method when performance is critical. 258 | 259 | ```py 260 | # __init__.py 261 | import importlib.metadata 262 | 263 | __version__ = importlib.metadata.version(__name__) 264 | ``` 265 | 266 | This trick may fail if a package is installed in [development mode](https://setuptools.pypa.io/en/latest/userguide/development_mode.html). Setting a fallback for `importlib.metadata.PackageNotFoundError` may be a good workaround. 267 | 268 | ```py 269 | import importlib.metadata 270 | 271 | try: 272 | __version__ = importlib.metadata.version(__name__) 273 | except importlib.metadata.PackageNotFoundError: 274 | __version__ = "0.0.0" 275 | ``` 276 | 277 | ### Version Build Hook 278 | 279 | You can write a version to a file when you run a build by using Hatch's official [version build hook](https://hatch.pypa.io/1.9/plugins/build-hook/version/). 280 | 281 | For example: 282 | 283 | ```toml 284 | [tool.hatch.build.hooks.version] 285 | path = "path/to/_version.py" 286 | template = ''' 287 | version = "{version}" 288 | ''' 289 | ``` 290 | 291 | > [!NOTE] 292 | > A version file should not be included in VCS. It's better to ignore it in `.gitignore`. 293 | > 294 | > **.gitignore** 295 | > 296 | > ```text 297 | > path/to/_version.py 298 | > ``` 299 | -------------------------------------------------------------------------------- /src/uv_dynamic_versioning/schemas.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass, is_dataclass 4 | from functools import cached_property 5 | from typing import Any 6 | 7 | from dunamai import Style, Vcs 8 | 9 | 10 | def _normalize(cls, data: dict[str, Any]): 11 | """Filter dict keys to match dataclass fields and handle kebab-case to snake_case conversion with validation.""" 12 | if not is_dataclass(cls): 13 | raise TypeError(f"{cls.__name__} is not a dataclass") 14 | 15 | fields = {f.name: f.type for f in cls.__dataclass_fields__.values()} 16 | result = {} 17 | for k, v in data.items(): 18 | key = k.replace("-", "_") 19 | if key in fields: 20 | result[key] = v 21 | 22 | return result 23 | 24 | 25 | @dataclass 26 | class BumpConfig: 27 | enable: bool = False 28 | index: int = -1 29 | 30 | def _validate_enable(self): 31 | if not isinstance(self.enable, bool): 32 | raise ValueError("bump-config: enable must be a boolean") 33 | 34 | def _validate_index(self): 35 | if not isinstance(self.index, int): 36 | raise ValueError("bump-config: index must be an integer") 37 | 38 | def __post_init__(self): 39 | """Validate the bump configuration.""" 40 | self._validate_enable() 41 | self._validate_index() 42 | 43 | @classmethod 44 | def from_dict(cls, data: dict) -> BumpConfig: 45 | """Create BumpConfig from dictionary with validation.""" 46 | validated_data = _normalize(cls, data) 47 | return cls(**validated_data) 48 | 49 | 50 | @dataclass 51 | class FromFile: 52 | source: str 53 | pattern: str | None = None 54 | 55 | def _validate_source(self): 56 | if not isinstance(self.source, str): 57 | raise ValueError("source must be a string") 58 | 59 | def _validate_pattern(self): 60 | if self.pattern is not None and not isinstance(self.pattern, str): 61 | raise ValueError("pattern must be a string or None") 62 | 63 | def __post_init__(self): 64 | self._validate_source() 65 | self._validate_pattern() 66 | 67 | @classmethod 68 | def from_dict(cls, data: dict) -> FromFile: 69 | """Create FromFile from dictionary with validation.""" 70 | validated_data = _normalize(cls, data) 71 | return cls(**validated_data) 72 | 73 | 74 | @dataclass 75 | class FormatJinjaImport: 76 | module: str 77 | item: str | None = None 78 | 79 | def _validate_module(self): 80 | if not isinstance(self.module, str): 81 | raise ValueError("module must be a string") 82 | 83 | def _validate_item(self): 84 | if self.item is not None and not isinstance(self.item, str): 85 | raise ValueError("item must be a string or None") 86 | 87 | def __post_init__(self): 88 | self._validate_module() 89 | self._validate_item() 90 | 91 | @classmethod 92 | def from_dict(cls, data: dict) -> FormatJinjaImport: 93 | validated_data = _normalize(cls, data) 94 | return cls(**validated_data) 95 | 96 | 97 | @dataclass 98 | class UvDynamicVersioning: 99 | vcs: Vcs = Vcs.Any 100 | metadata: bool | None = None 101 | tagged_metadata: bool = False 102 | dirty: bool = False 103 | pattern: str = "default" 104 | pattern_prefix: str | None = None 105 | format: str | None = None 106 | format_jinja: str | None = None 107 | format_jinja_imports: list[FormatJinjaImport] | None = None 108 | style: Style | None = None 109 | latest_tag: bool = False 110 | strict: bool = False 111 | tag_dir: str = "tags" 112 | tag_branch: str | None = None 113 | full_commit: bool = False 114 | ignore_untracked: bool = False 115 | commit_length: int | None = None 116 | commit_prefix: str | None = None 117 | escape_with: str | None = None 118 | bump: bool | BumpConfig = False 119 | fallback_version: str | None = None 120 | from_file: FromFile | None = None 121 | 122 | def _validate_vcs(self): 123 | if not isinstance(self.vcs, Vcs): 124 | raise ValueError(f"vcs is invalid - {self.vcs}") 125 | 126 | def _validate_metadata(self): 127 | if self.metadata is not None and not isinstance(self.metadata, bool): 128 | raise ValueError("metadata must be a boolean or None") 129 | 130 | def _validate_tagged_metadata(self): 131 | if not isinstance(self.tagged_metadata, bool): 132 | raise ValueError("tagged-metadata must be a boolean") 133 | 134 | def _validate_dirty(self): 135 | if not isinstance(self.dirty, bool): 136 | raise ValueError("dirty must be a boolean") 137 | 138 | def _validate_latest_tag(self): 139 | if not isinstance(self.latest_tag, bool): 140 | raise ValueError("latest-tag must be a boolean") 141 | 142 | def _validate_strict(self): 143 | if not isinstance(self.strict, bool): 144 | raise ValueError("strict must be a boolean") 145 | 146 | def _validate_full_commit(self): 147 | if not isinstance(self.full_commit, bool): 148 | raise ValueError("full-commit must be a boolean") 149 | 150 | def _validate_ignore_untracked(self): 151 | if not isinstance(self.ignore_untracked, bool): 152 | raise ValueError("ignore-untracked must be a boolean") 153 | 154 | def _validate_pattern(self): 155 | if self.pattern is not None and not isinstance(self.pattern, str): 156 | raise ValueError("pattern must be a string") 157 | 158 | def _validate_pattern_prefix(self): 159 | if self.pattern_prefix is not None and not isinstance(self.pattern_prefix, str): 160 | raise ValueError("pattern-prefix must be a string or None") 161 | 162 | def _validate_format(self): 163 | if self.format is not None and not isinstance(self.format, str): 164 | raise ValueError("format must be a string or None") 165 | 166 | def _validate_format_jinja(self): 167 | if self.format_jinja is not None and not isinstance(self.format_jinja, str): 168 | raise ValueError("format-jinja must be a string or None") 169 | 170 | def _validate_format_jinja_imports(self): 171 | if self.format_jinja_imports is not None: 172 | if not isinstance(self.format_jinja_imports, list): 173 | raise ValueError("format-jinja-imports must be a list or None") 174 | 175 | for item in self.format_jinja_imports: 176 | if not isinstance(item, FormatJinjaImport): 177 | raise ValueError( 178 | "format-jinja-imports must contain only FormatJinjaImport instances" 179 | ) 180 | 181 | def _validate_style(self): 182 | if self.style is not None and not isinstance(self.style, Style): 183 | raise ValueError(f"style is invalid - {self.style}") 184 | 185 | def _validate_tag_dir(self): 186 | if self.tag_dir is not None and not isinstance(self.tag_dir, str): 187 | raise ValueError("tag-dir must be a string") 188 | 189 | def _validate_tag_branch(self): 190 | if self.tag_branch is not None and not isinstance(self.tag_branch, str): 191 | raise ValueError("tag-branch must be a string or None") 192 | 193 | def _validate_commit_length(self): 194 | if self.commit_length is not None and not isinstance(self.commit_length, int): 195 | raise ValueError("commit-length must be an integer or None") 196 | 197 | def _validate_commit_prefix(self): 198 | if self.commit_prefix is not None and not isinstance(self.commit_prefix, str): 199 | raise ValueError("commit-prefix must be a string or None") 200 | 201 | def _validate_escape_with(self): 202 | if self.escape_with is not None and not isinstance(self.escape_with, str): 203 | raise ValueError("escape-with must be a string or None") 204 | 205 | def _validate_bump(self): 206 | if isinstance(self.bump, bool) and self.bump: 207 | self.bump = BumpConfig(enable=True) 208 | 209 | if isinstance(self.bump, dict): 210 | self.bump = BumpConfig.from_dict(self.bump) 211 | 212 | if not isinstance(self.bump, (bool, BumpConfig)): 213 | raise ValueError("bump must be a boolean or BumpConfig instance") 214 | 215 | def _validate_fallback_version(self): 216 | if self.fallback_version is not None and not isinstance( 217 | self.fallback_version, str 218 | ): 219 | raise ValueError("fallback-version must be a string or None") 220 | 221 | def __post_init__(self): 222 | """Validate the UvDynamicVersioning configuration.""" 223 | self._validate_vcs() 224 | self._validate_metadata() 225 | self._validate_tagged_metadata() 226 | self._validate_dirty() 227 | self._validate_latest_tag() 228 | self._validate_strict() 229 | self._validate_full_commit() 230 | self._validate_ignore_untracked() 231 | self._validate_pattern() 232 | self._validate_pattern_prefix() 233 | self._validate_format() 234 | self._validate_format_jinja() 235 | self._validate_format_jinja_imports() 236 | self._validate_style() 237 | self._validate_tag_dir() 238 | self._validate_tag_branch() 239 | self._validate_commit_length() 240 | self._validate_bump() 241 | self._validate_fallback_version() 242 | self._validate_commit_prefix() 243 | self._validate_escape_with() 244 | 245 | @cached_property 246 | def bump_config(self) -> BumpConfig: 247 | if self.bump is False: 248 | return BumpConfig() 249 | 250 | if self.bump is True: 251 | return BumpConfig(enable=True) 252 | 253 | return self.bump 254 | 255 | @classmethod 256 | def from_dict(cls, data: dict) -> UvDynamicVersioning: 257 | """Create UvDynamicVersioning from dictionary with validation.""" 258 | validated_data = _normalize(cls, data) 259 | 260 | # Special handling for enum fields 261 | if "vcs" in validated_data and isinstance(validated_data["vcs"], str): 262 | validated_data["vcs"] = Vcs(validated_data["vcs"]) 263 | 264 | if "style" in validated_data and isinstance(validated_data["style"], str): 265 | validated_data["style"] = Style(validated_data["style"]) 266 | 267 | # Special handling for bump field 268 | if "bump" in validated_data and isinstance(validated_data["bump"], dict): 269 | validated_data["bump"] = BumpConfig.from_dict(validated_data["bump"]) 270 | 271 | if "from_file" in validated_data and isinstance( 272 | validated_data["from_file"], dict 273 | ): 274 | validated_data["from_file"] = FromFile.from_dict( 275 | validated_data["from_file"] 276 | ) 277 | 278 | if "format_jinja_imports" in validated_data and isinstance( 279 | validated_data["format_jinja_imports"], list 280 | ): 281 | validated_data["format_jinja_imports"] = [ 282 | FormatJinjaImport.from_dict(item) 283 | for item in validated_data["format_jinja_imports"] 284 | if isinstance(item, dict) 285 | ] 286 | 287 | return cls(**validated_data) 288 | 289 | 290 | @dataclass 291 | class Tool: 292 | uv_dynamic_versioning: UvDynamicVersioning | None = None 293 | 294 | def __post_init__(self): 295 | """Validate the Tool configuration.""" 296 | if self.uv_dynamic_versioning is not None and not isinstance( 297 | self.uv_dynamic_versioning, UvDynamicVersioning 298 | ): 299 | raise ValueError( 300 | "uv-dynamic-versioning must be an instance of UvDynamicVersioning" 301 | ) 302 | 303 | @classmethod 304 | def from_dict(cls, data: dict) -> Tool: 305 | """Create Tool from dictionary with validation.""" 306 | validated_data = _normalize(cls, data) 307 | 308 | if "uv_dynamic_versioning" in validated_data and isinstance( 309 | validated_data["uv_dynamic_versioning"], dict 310 | ): 311 | validated_data["uv_dynamic_versioning"] = UvDynamicVersioning.from_dict( 312 | validated_data["uv_dynamic_versioning"] 313 | ) 314 | 315 | return cls(**validated_data) 316 | 317 | 318 | @dataclass 319 | class Project: 320 | tool: Tool 321 | 322 | def _validate_tool(self): 323 | if not isinstance(self.tool, Tool): 324 | raise ValueError("tool must be an instance of Tool") 325 | 326 | def __post_init__(self): 327 | """Validate the Project configuration.""" 328 | self._validate_tool() 329 | 330 | @classmethod 331 | def from_dict(cls, data: dict) -> Project: 332 | """Create Project from dictionary with validation.""" 333 | validated_data = _normalize(cls, data) 334 | 335 | if "tool" in validated_data and isinstance(validated_data["tool"], dict): 336 | validated_data["tool"] = Tool.from_dict(validated_data["tool"]) 337 | elif "tool" not in validated_data: 338 | raise ValueError("project must have a 'tool' field") 339 | 340 | return cls(**validated_data) 341 | 342 | 343 | @dataclass 344 | class MetadataHookConfig: 345 | dependencies: list[str] | None = None 346 | optional_dependencies: dict[str, list[str]] | None = None 347 | 348 | def _validate_dependencies(self): 349 | if self.dependencies is not None and not isinstance(self.dependencies, list): 350 | raise ValueError("dependencies must be a list or None") 351 | 352 | for v in self.dependencies or []: 353 | if not isinstance(v, str): 354 | raise ValueError("dependencies must be strings") 355 | 356 | def _validate_optional_dependencies(self): 357 | if self.optional_dependencies is None: 358 | return 359 | 360 | if not isinstance(self.optional_dependencies, dict): 361 | raise ValueError("optional-dependencies must be a dict or None") 362 | 363 | for key, value in self.optional_dependencies.items(): 364 | if not isinstance(key, str): 365 | raise ValueError("optional-dependency keys must be strings") 366 | if not isinstance(value, list): 367 | raise ValueError("optional-dependency values must be lists of strings") 368 | 369 | for v in value: 370 | if not isinstance(v, str): 371 | raise ValueError("optional-dependencies must be strings") 372 | 373 | def __post_init__(self): 374 | """Validate the MetadataHookConfig configuration.""" 375 | self._validate_dependencies() 376 | self._validate_optional_dependencies() 377 | 378 | @classmethod 379 | def from_dict(cls, data: dict) -> MetadataHookConfig: 380 | """Create MetadataHookConfig from dictionary with validation.""" 381 | validated_data = _normalize(cls, data) 382 | return cls(**validated_data) 383 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.9, <4.0" 4 | 5 | [[package]] 6 | name = "cfgv" 7 | version = "3.4.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, 12 | ] 13 | 14 | [[package]] 15 | name = "colorama" 16 | version = "0.4.6" 17 | source = { registry = "https://pypi.org/simple" } 18 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 19 | wheels = [ 20 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 21 | ] 22 | 23 | [[package]] 24 | name = "distlib" 25 | version = "0.4.0" 26 | source = { registry = "https://pypi.org/simple" } 27 | sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } 28 | wheels = [ 29 | { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, 30 | ] 31 | 32 | [[package]] 33 | name = "dunamai" 34 | version = "1.25.0" 35 | source = { registry = "https://pypi.org/simple" } 36 | dependencies = [ 37 | { name = "packaging" }, 38 | ] 39 | sdist = { url = "https://files.pythonhosted.org/packages/f1/2f/194d9a34c4d831c6563d2d990720850f0baef9ab60cb4ad8ae0eff6acd34/dunamai-1.25.0.tar.gz", hash = "sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1", size = 46155, upload-time = "2025-07-04T19:25:56.082Z" } 40 | wheels = [ 41 | { url = "https://files.pythonhosted.org/packages/36/41/04e2a649058b0713b00d6c9bd22da35618bb157289e05d068e51fddf8d7e/dunamai-1.25.0-py3-none-any.whl", hash = "sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab", size = 27022, upload-time = "2025-07-04T19:25:54.863Z" }, 42 | ] 43 | 44 | [[package]] 45 | name = "exceptiongroup" 46 | version = "1.3.0" 47 | source = { registry = "https://pypi.org/simple" } 48 | dependencies = [ 49 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 50 | ] 51 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } 52 | wheels = [ 53 | { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, 54 | ] 55 | 56 | [[package]] 57 | name = "filelock" 58 | version = "3.18.0" 59 | source = { registry = "https://pypi.org/simple" } 60 | sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } 61 | wheels = [ 62 | { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, 63 | ] 64 | 65 | [[package]] 66 | name = "gitdb" 67 | version = "4.0.12" 68 | source = { registry = "https://pypi.org/simple" } 69 | dependencies = [ 70 | { name = "smmap" }, 71 | ] 72 | sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } 73 | wheels = [ 74 | { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, 75 | ] 76 | 77 | [[package]] 78 | name = "gitpython" 79 | version = "3.1.45" 80 | source = { registry = "https://pypi.org/simple" } 81 | dependencies = [ 82 | { name = "gitdb" }, 83 | { name = "typing-extensions", marker = "python_full_version < '3.10'" }, 84 | ] 85 | sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } 86 | wheels = [ 87 | { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, 88 | ] 89 | 90 | [[package]] 91 | name = "hatchling" 92 | version = "1.27.0" 93 | source = { registry = "https://pypi.org/simple" } 94 | dependencies = [ 95 | { name = "packaging" }, 96 | { name = "pathspec" }, 97 | { name = "pluggy" }, 98 | { name = "tomli", marker = "python_full_version < '3.11'" }, 99 | { name = "trove-classifiers" }, 100 | ] 101 | sdist = { url = "https://files.pythonhosted.org/packages/8f/8a/cc1debe3514da292094f1c3a700e4ca25442489731ef7c0814358816bb03/hatchling-1.27.0.tar.gz", hash = "sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6", size = 54983, upload-time = "2024-12-15T17:08:11.894Z" } 102 | wheels = [ 103 | { url = "https://files.pythonhosted.org/packages/08/e7/ae38d7a6dfba0533684e0b2136817d667588ae3ec984c1a4e5df5eb88482/hatchling-1.27.0-py3-none-any.whl", hash = "sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b", size = 75794, upload-time = "2024-12-15T17:08:10.364Z" }, 104 | ] 105 | 106 | [[package]] 107 | name = "identify" 108 | version = "2.6.13" 109 | source = { registry = "https://pypi.org/simple" } 110 | sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload-time = "2025-08-09T19:35:00.6Z" } 111 | wheels = [ 112 | { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload-time = "2025-08-09T19:34:59.1Z" }, 113 | ] 114 | 115 | [[package]] 116 | name = "importlib-metadata" 117 | version = "8.7.0" 118 | source = { registry = "https://pypi.org/simple" } 119 | dependencies = [ 120 | { name = "zipp" }, 121 | ] 122 | sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } 123 | wheels = [ 124 | { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, 125 | ] 126 | 127 | [[package]] 128 | name = "iniconfig" 129 | version = "2.1.0" 130 | source = { registry = "https://pypi.org/simple" } 131 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 132 | wheels = [ 133 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 134 | ] 135 | 136 | [[package]] 137 | name = "jinja2" 138 | version = "3.1.6" 139 | source = { registry = "https://pypi.org/simple" } 140 | dependencies = [ 141 | { name = "markupsafe" }, 142 | ] 143 | sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } 144 | wheels = [ 145 | { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, 146 | ] 147 | 148 | [[package]] 149 | name = "markdown-it-py" 150 | version = "3.0.0" 151 | source = { registry = "https://pypi.org/simple" } 152 | dependencies = [ 153 | { name = "mdurl" }, 154 | ] 155 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } 156 | wheels = [ 157 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, 158 | ] 159 | 160 | [[package]] 161 | name = "markupsafe" 162 | version = "3.0.2" 163 | source = { registry = "https://pypi.org/simple" } 164 | sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } 165 | wheels = [ 166 | { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, 167 | { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, 168 | { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, 169 | { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, 170 | { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, 171 | { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, 172 | { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, 173 | { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, 174 | { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, 175 | { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, 176 | { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, 177 | { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, 178 | { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, 179 | { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, 180 | { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, 181 | { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, 182 | { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, 183 | { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, 184 | { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, 185 | { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, 186 | { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, 187 | { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, 188 | { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, 189 | { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, 190 | { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, 191 | { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, 192 | { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, 193 | { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, 194 | { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, 195 | { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, 196 | { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, 197 | { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, 198 | { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, 199 | { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, 200 | { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, 201 | { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, 202 | { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, 203 | { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, 204 | { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, 205 | { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, 206 | { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, 207 | { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, 208 | { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, 209 | { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, 210 | { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, 211 | { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, 212 | { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, 213 | { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, 214 | { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, 215 | { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, 216 | { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, 217 | { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, 218 | { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, 219 | { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, 220 | { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, 221 | { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, 222 | { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, 223 | { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, 224 | { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, 225 | { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, 226 | ] 227 | 228 | [[package]] 229 | name = "mdurl" 230 | version = "0.1.2" 231 | source = { registry = "https://pypi.org/simple" } 232 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } 233 | wheels = [ 234 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, 235 | ] 236 | 237 | [[package]] 238 | name = "nodeenv" 239 | version = "1.9.1" 240 | source = { registry = "https://pypi.org/simple" } 241 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } 242 | wheels = [ 243 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, 244 | ] 245 | 246 | [[package]] 247 | name = "packaging" 248 | version = "25.0" 249 | source = { registry = "https://pypi.org/simple" } 250 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 251 | wheels = [ 252 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 253 | ] 254 | 255 | [[package]] 256 | name = "pathspec" 257 | version = "0.12.1" 258 | source = { registry = "https://pypi.org/simple" } 259 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } 260 | wheels = [ 261 | { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, 262 | ] 263 | 264 | [[package]] 265 | name = "platformdirs" 266 | version = "4.3.8" 267 | source = { registry = "https://pypi.org/simple" } 268 | sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } 269 | wheels = [ 270 | { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, 271 | ] 272 | 273 | [[package]] 274 | name = "pluggy" 275 | version = "1.6.0" 276 | source = { registry = "https://pypi.org/simple" } 277 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 278 | wheels = [ 279 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 280 | ] 281 | 282 | [[package]] 283 | name = "pre-commit" 284 | version = "4.3.0" 285 | source = { registry = "https://pypi.org/simple" } 286 | dependencies = [ 287 | { name = "cfgv" }, 288 | { name = "identify" }, 289 | { name = "nodeenv" }, 290 | { name = "pyyaml" }, 291 | { name = "virtualenv" }, 292 | ] 293 | sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } 294 | wheels = [ 295 | { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, 296 | ] 297 | 298 | [[package]] 299 | name = "pygments" 300 | version = "2.19.2" 301 | source = { registry = "https://pypi.org/simple" } 302 | sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } 303 | wheels = [ 304 | { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, 305 | ] 306 | 307 | [[package]] 308 | name = "pytest" 309 | version = "8.4.2" 310 | source = { registry = "https://pypi.org/simple" } 311 | dependencies = [ 312 | { name = "colorama", marker = "sys_platform == 'win32'" }, 313 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 314 | { name = "iniconfig" }, 315 | { name = "packaging" }, 316 | { name = "pluggy" }, 317 | { name = "pygments" }, 318 | { name = "tomli", marker = "python_full_version < '3.11'" }, 319 | ] 320 | sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } 321 | wheels = [ 322 | { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, 323 | ] 324 | 325 | [[package]] 326 | name = "pytest-pretty" 327 | version = "1.3.0" 328 | source = { registry = "https://pypi.org/simple" } 329 | dependencies = [ 330 | { name = "pytest" }, 331 | { name = "rich" }, 332 | ] 333 | sdist = { url = "https://files.pythonhosted.org/packages/ba/d7/c699e0be5401fe9ccad484562f0af9350b4e48c05acf39fb3dab1932128f/pytest_pretty-1.3.0.tar.gz", hash = "sha256:97e9921be40f003e40ae78db078d4a0c1ea42bf73418097b5077970c2cc43bf3", size = 219297, upload-time = "2025-06-04T12:54:37.322Z" } 334 | wheels = [ 335 | { url = "https://files.pythonhosted.org/packages/ab/85/2f97a1b65178b0f11c9c77c35417a4cc5b99a80db90dad4734a129844ea5/pytest_pretty-1.3.0-py3-none-any.whl", hash = "sha256:074b9d5783cef9571494543de07e768a4dda92a3e85118d6c7458c67297159b7", size = 5620, upload-time = "2025-06-04T12:54:36.229Z" }, 336 | ] 337 | 338 | [[package]] 339 | name = "pytest-randomly" 340 | version = "4.0.1" 341 | source = { registry = "https://pypi.org/simple" } 342 | dependencies = [ 343 | { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, 344 | { name = "pytest" }, 345 | ] 346 | sdist = { url = "https://files.pythonhosted.org/packages/c4/1d/258a4bf1109258c00c35043f40433be5c16647387b6e7cd5582d638c116b/pytest_randomly-4.0.1.tar.gz", hash = "sha256:174e57bb12ac2c26f3578188490bd333f0e80620c3f47340158a86eca0593cd8", size = 14130, upload-time = "2025-09-12T15:23:00.085Z" } 347 | wheels = [ 348 | { url = "https://files.pythonhosted.org/packages/33/3e/a4a9227807b56869790aad3e24472a554b585974fe7e551ea350f50897ae/pytest_randomly-4.0.1-py3-none-any.whl", hash = "sha256:e0dfad2fd4f35e07beff1e47c17fbafcf98f9bf4531fd369d9260e2f858bfcb7", size = 8304, upload-time = "2025-09-12T15:22:58.946Z" }, 349 | ] 350 | 351 | [[package]] 352 | name = "pyyaml" 353 | version = "6.0.2" 354 | source = { registry = "https://pypi.org/simple" } 355 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } 356 | wheels = [ 357 | { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, 358 | { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, 359 | { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, 360 | { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, 361 | { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, 362 | { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, 363 | { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, 364 | { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, 365 | { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, 366 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, 367 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, 368 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, 369 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, 370 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, 371 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, 372 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, 373 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, 374 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, 375 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, 376 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, 377 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, 378 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, 379 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, 380 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, 381 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, 382 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, 383 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, 384 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, 385 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, 386 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, 387 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, 388 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, 389 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, 390 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, 391 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, 392 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, 393 | { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, 394 | { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, 395 | { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, 396 | { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, 397 | { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, 398 | { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, 399 | { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, 400 | { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, 401 | { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, 402 | ] 403 | 404 | [[package]] 405 | name = "rich" 406 | version = "14.1.0" 407 | source = { registry = "https://pypi.org/simple" } 408 | dependencies = [ 409 | { name = "markdown-it-py" }, 410 | { name = "pygments" }, 411 | ] 412 | sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } 413 | wheels = [ 414 | { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, 415 | ] 416 | 417 | [[package]] 418 | name = "smmap" 419 | version = "5.0.2" 420 | source = { registry = "https://pypi.org/simple" } 421 | sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } 422 | wheels = [ 423 | { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, 424 | ] 425 | 426 | [[package]] 427 | name = "tomli" 428 | version = "2.2.1" 429 | source = { registry = "https://pypi.org/simple" } 430 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } 431 | wheels = [ 432 | { 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, upload-time = "2024-11-27T22:37:54.956Z" }, 433 | { 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, upload-time = "2024-11-27T22:37:56.698Z" }, 434 | { 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, upload-time = "2024-11-27T22:37:57.63Z" }, 435 | { 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, upload-time = "2024-11-27T22:37:59.344Z" }, 436 | { 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, upload-time = "2024-11-27T22:38:00.429Z" }, 437 | { 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, upload-time = "2024-11-27T22:38:02.094Z" }, 438 | { 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, upload-time = "2024-11-27T22:38:03.206Z" }, 439 | { 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, upload-time = "2024-11-27T22:38:04.217Z" }, 440 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, 441 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, 442 | { 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, upload-time = "2024-11-27T22:38:07.731Z" }, 443 | { 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, upload-time = "2024-11-27T22:38:09.384Z" }, 444 | { 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, upload-time = "2024-11-27T22:38:10.329Z" }, 445 | { 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, upload-time = "2024-11-27T22:38:11.443Z" }, 446 | { 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, upload-time = "2024-11-27T22:38:13.099Z" }, 447 | { 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, upload-time = "2024-11-27T22:38:14.766Z" }, 448 | { 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, upload-time = "2024-11-27T22:38:15.843Z" }, 449 | { 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, upload-time = "2024-11-27T22:38:17.645Z" }, 450 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, 451 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, 452 | { 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, upload-time = "2024-11-27T22:38:21.659Z" }, 453 | { 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, upload-time = "2024-11-27T22:38:22.693Z" }, 454 | { 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, upload-time = "2024-11-27T22:38:24.367Z" }, 455 | { 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, upload-time = "2024-11-27T22:38:26.081Z" }, 456 | { 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, upload-time = "2024-11-27T22:38:27.921Z" }, 457 | { 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, upload-time = "2024-11-27T22:38:29.591Z" }, 458 | { 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, upload-time = "2024-11-27T22:38:30.639Z" }, 459 | { 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, upload-time = "2024-11-27T22:38:31.702Z" }, 460 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, 461 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, 462 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, 463 | ] 464 | 465 | [[package]] 466 | name = "tomlkit" 467 | version = "0.13.3" 468 | source = { registry = "https://pypi.org/simple" } 469 | sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } 470 | wheels = [ 471 | { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, 472 | ] 473 | 474 | [[package]] 475 | name = "trove-classifiers" 476 | version = "2025.8.6.13" 477 | source = { registry = "https://pypi.org/simple" } 478 | sdist = { url = "https://files.pythonhosted.org/packages/c3/21/707af14daa638b0df15b5d5700349e0abdd3e5140069f9ab6e0ccb922806/trove_classifiers-2025.8.6.13.tar.gz", hash = "sha256:5a0abad839d2ed810f213ab133d555d267124ddea29f1d8a50d6eca12a50ae6e", size = 16932, upload-time = "2025-08-06T13:26:26.479Z" } 479 | wheels = [ 480 | { url = "https://files.pythonhosted.org/packages/d5/44/323a87d78f04d5329092aada803af3612dd004a64b69ba8b13046601a8c9/trove_classifiers-2025.8.6.13-py3-none-any.whl", hash = "sha256:c4e7fc83012770d80b3ae95816111c32b085716374dccee0d3fbf5c235495f9f", size = 14121, upload-time = "2025-08-06T13:26:25.063Z" }, 481 | ] 482 | 483 | [[package]] 484 | name = "typing-extensions" 485 | version = "4.14.1" 486 | source = { registry = "https://pypi.org/simple" } 487 | sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } 488 | wheels = [ 489 | { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, 490 | ] 491 | 492 | [[package]] 493 | name = "uv-dynamic-versioning" 494 | source = { editable = "." } 495 | dependencies = [ 496 | { name = "dunamai" }, 497 | { name = "hatchling" }, 498 | { name = "jinja2" }, 499 | { name = "tomlkit" }, 500 | ] 501 | 502 | [package.dev-dependencies] 503 | dev = [ 504 | { name = "gitpython" }, 505 | { name = "pre-commit" }, 506 | { name = "pytest" }, 507 | { name = "pytest-pretty" }, 508 | { name = "pytest-randomly" }, 509 | ] 510 | 511 | [package.metadata] 512 | requires-dist = [ 513 | { name = "dunamai", specifier = "~=1.25" }, 514 | { name = "hatchling", specifier = "~=1.26" }, 515 | { name = "jinja2", specifier = "~=3.0" }, 516 | { name = "tomlkit", specifier = "~=0.13" }, 517 | ] 518 | 519 | [package.metadata.requires-dev] 520 | dev = [ 521 | { name = "gitpython", specifier = "~=3.1.45" }, 522 | { name = "pre-commit", specifier = "~=4.3.0" }, 523 | { name = "pytest", specifier = "~=8.4.2" }, 524 | { name = "pytest-pretty", specifier = "~=1.3.0" }, 525 | { name = "pytest-randomly", specifier = "~=4.0.1" }, 526 | ] 527 | 528 | [[package]] 529 | name = "virtualenv" 530 | version = "20.33.1" 531 | source = { registry = "https://pypi.org/simple" } 532 | dependencies = [ 533 | { name = "distlib" }, 534 | { name = "filelock" }, 535 | { name = "platformdirs" }, 536 | ] 537 | sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160, upload-time = "2025-08-05T16:10:55.605Z" } 538 | wheels = [ 539 | { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362, upload-time = "2025-08-05T16:10:52.81Z" }, 540 | ] 541 | 542 | [[package]] 543 | name = "zipp" 544 | version = "3.23.0" 545 | source = { registry = "https://pypi.org/simple" } 546 | sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } 547 | wheels = [ 548 | { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, 549 | ] 550 | --------------------------------------------------------------------------------