├── 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 | [](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 |
--------------------------------------------------------------------------------