diff --git a/.cursor/rules/test-running.mdc b/.cursor/rules/test-running.mdc new file mode 100644 index 00000000..201f9af6 --- /dev/null +++ b/.cursor/rules/test-running.mdc @@ -0,0 +1,12 @@ +--- +description: run tests with uv tooling +globs: +alwaysApply: true +--- + +use `uv run pytest` to run tests +use uv to manage dependencies + +follow preexisting conventions in the project + +- use the fixtures \ No newline at end of file diff --git a/.github/workflows/api-check.yml b/.github/workflows/api-check.yml new file mode 100644 index 00000000..4db2526b --- /dev/null +++ b/.github/workflows/api-check.yml @@ -0,0 +1,79 @@ +name: API Stability Check + +on: + pull_request: + push: + branches: + - "*" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + +jobs: + api-check: + name: Check API stability with griffe + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install -U pip setuptools + pip install -e .[test] + pip install griffe + + - name: Run griffe API check + id: griffe-check + continue-on-error: true + run: | + echo "Running griffe API stability check..." + if griffe check setuptools_scm -ssrc -f github; then + echo "api_check_result=success" >> $GITHUB_OUTPUT + echo "exit_code=0" >> $GITHUB_OUTPUT + else + exit_code=$? + echo "api_check_result=warning" >> $GITHUB_OUTPUT + echo "exit_code=$exit_code" >> $GITHUB_OUTPUT + exit $exit_code + fi + + - name: Report API check result + if: always() + uses: actions/github-script@v8 + with: + script: | + const result = '${{ steps.griffe-check.outputs.api_check_result }}' + const exitCode = '${{ steps.griffe-check.outputs.exit_code }}' + + if (result === 'success') { + core.notice('API stability check passed - no breaking changes detected') + await core.summary + .addHeading('✅ API Stability Check: Passed', 2) + .addRaw('No breaking changes detected in the public API') + .write() + } else if (result === 'warning') { + core.warning(`API stability check detected breaking changes (exit code: ${exitCode}). Please review the API changes above.`) + await core.summary + .addHeading('⚠️ API Stability Warning', 2) + .addRaw('Breaking changes detected in the public API. Please review the changes reported above.') + .addRaw(`\n\nExit code: ${exitCode}`) + .write() + } else { + core.error('API stability check failed to run properly') + await core.summary + .addHeading('❌ API Stability Check: Failed', 2) + .addRaw('The griffe check failed to execute. This may indicate griffe is not installed or there was an error.') + .write() + } \ No newline at end of file diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 393511fd..17953d55 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -7,6 +7,8 @@ on: - "*" tags: - "v*" + release: + types: [published] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} @@ -20,9 +22,12 @@ jobs: package: name: Build & inspect our package. runs-on: ubuntu-latest + env: + # Use no-local-version for package builds to ensure clean versions for PyPI uploads + SETUPTOOLS_SCM_NO_LOCAL: "1" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 @@ -39,18 +44,23 @@ jobs: include: - os: windows-latest python_version: 'msys2' + env: + # Enable tracemalloc to debug gc errors with popen objects (especially on Windows) + PYTHONTRACEMALLOC: "1" name: ${{ matrix.os }} - Python ${{ matrix.python_version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Setup python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 if: matrix.python_version != 'msys2' with: python-version: ${{ matrix.python_version }} architecture: x64 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v6 - name: Setup MSYS2 uses: msys2/setup-msys2@v2 if: matrix.python_version == 'msys2' @@ -58,7 +68,7 @@ jobs: msystem: MINGW64 install: git mingw-w64-x86_64-python mingw-w64-x86_64-python-setuptools update: true - - name: Setup GnuPG + - name: Setup GnuPG and Mercurial on Windows # At present, the Windows VMs only come with the copy of GnuPG that's bundled # with Git for Windows. If we want to use this version _and_ be able to set # arbitrary GnuPG home directories, then the test would need to figure out when @@ -74,50 +84,72 @@ jobs: # Additionally, we'll explicitly set `gpg.program` to ensure Git for Windows # doesn't invoke the bundled GnuPG, otherwise we'll run into # . See also: . + # + # Windows runners no longer ship with Mercurial pre-installed, so we install + # it via Chocolatey using the 'hg' package. run: | $env:PATH = "C:\Program Files\Git\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\ProgramData\Chocolatey\bin" [Environment]::SetEnvironmentVariable("Path", $env:PATH, "Machine") - choco install gnupg -y --no-progress + choco install gnupg hg -y --no-progress echo "C:\Program Files (x86)\gnupg\bin" >> $env:GITHUB_PATH + echo "C:\Program Files\Mercurial\" >> $env:GITHUB_PATH git config --system gpg.program "C:\Program Files (x86)\gnupg\bin\gpg.exe" if: runner.os == 'Windows' - - run: pip install -U 'setuptools>=61' - - uses: actions/download-artifact@v4 + - run: uv sync --group test --group docs --extra rich + - uses: actions/download-artifact@v5 with: name: Packages path: dist - shell: bash - run: pip install "$(echo -n dist/*whl)[toml,test]" + run: uv pip install "$(echo -n dist/*whl)" - run: | $(hg debuginstall --template "{pythonexe}") -m pip install hg-git --user if: matrix.os == 'ubuntu-latest' # this hopefully helps with os caches, hg init sometimes gets 20s timeouts - run: hg version - - run: pytest - timeout-minutes: 15 + - run: uv run pytest + timeout-minutes: 25 dist_upload: runs-on: ubuntu-latest - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + if: (github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')) || (github.event_name == 'release' && github.event.action == 'published') permissions: id-token: write needs: [test] steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: Packages path: dist - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + upload-release-assets: + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' + needs: [test] + permissions: + contents: write + steps: + - uses: actions/download-artifact@v5 + with: + name: Packages + path: dist + - name: Upload release assets + uses: softprops/action-gh-release@v2 + with: + files: dist/* + fail_on_unmatched_files: true + test-pypi-upload: runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: [test] permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: Packages path: dist diff --git a/.gitignore b/.gitignore index cd25c485..b790bb39 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,5 @@ coverage.xml # Sphinx documentation docs/_build/ + +.serena/cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a42e2d10..5f66a9f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,23 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: check-yaml - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.6 + rev: v0.13.3 hooks: - - id: ruff + - id: ruff-check args: [--fix, --exit-non-zero-on-fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v1.18.2 hooks: - id: mypy args: [--strict] - language_version: "3.10" additional_dependencies: - types-setuptools - tokenize-rt==3.2.0 @@ -28,6 +27,12 @@ repos: - rich - repo: https://github.com/scientific-python/cookie - rev: 2025.01.22 + rev: 2025.10.01 hooks: - id: sp-repo-review + +- repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + args: [-w, --ignore-words-list=hist,nd,te] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b9ae71f4..5aa34e7a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,16 +1,15 @@ version: 2 -build: - os: ubuntu-22.04 - tools: - python: "3.11" mkdocs: configuration: mkdocs.yml -# Optionally declare the Python requirements required to build your docs -python: - install: - - method: pip - path: . - extra_requirements: - - docs \ No newline at end of file + +build: + os: ubuntu-24.04 + tools: + python: "3.13" + jobs: + install: + - pip install -U pip # Official recommended way + - pip install . + - pip install --group docs diff --git a/.serena/memories/done_checklist.md b/.serena/memories/done_checklist.md new file mode 100644 index 00000000..8e0fc3e2 --- /dev/null +++ b/.serena/memories/done_checklist.md @@ -0,0 +1,16 @@ +Before considering a task done + +- Code quality + - Ruff clean: uv run ruff check . + - Types clean: uv run mypy +- Tests + - All tests green: uv run pytest + - New/changed behavior covered with tests (use project fixtures) +- Docs + - Update docs if user-facing behavior changed + - Build docs cleanly: uv run mkdocs build --clean --strict +- Packaging + - If relevant: uv run python -m build && uv run twine check dist/* +- Housekeeping + - Follow existing naming and module structure; keep functions focused and typed + - Update `CHANGELOG.md` when appropriate diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md new file mode 100644 index 00000000..cf2670d9 --- /dev/null +++ b/.serena/memories/project_overview.md @@ -0,0 +1,28 @@ +Project: setuptools-scm + +Purpose +- Extract and infer Python package versions from SCM metadata (Git/Mercurial) at build/runtime. +- Provide setuptools integrations (dynamic version, file finders) and fallbacks for archival/PKG-INFO. + +Tech Stack +- Language: Python (3.8–3.13) +- Packaging/build: setuptools (>=61), packaging; console scripts via entry points +- Tooling: uv (dependency and run), pytest, mypy (strict), ruff (lint, isort), mkdocs (docs), tox (optional/matrix), wheel/build + +Codebase Structure (high level) +- src/setuptools_scm/: library code + - _cli.py, __main__.py: CLI entry (`python -m setuptools_scm`, `setuptools-scm`) + - git.py, hg.py, hg_git.py: VCS parsing + - _file_finders/: discover files for sdist + - _integration/: setuptools and pyproject integration + - version.py and helpers: version schemes/local version logic + - discover.py, fallbacks.py: inference and archival fallbacks +- testing/: pytest suite and fixtures +- docs/: mkdocs documentation +- pyproject.toml: project metadata, pytest and ruff config +- tox.ini: alternate CI/matrix, flake8 defaults +- uv.lock: locked dependencies + +Conventions +- Use uv to run commands (`uv run ...`); tests live under `testing/` per pytest config. +- Type hints throughout; strict mypy enforced; ruff governs lint rules and import layout (isort in ruff). diff --git a/.serena/memories/style_and_conventions.md b/.serena/memories/style_and_conventions.md new file mode 100644 index 00000000..aec4e917 --- /dev/null +++ b/.serena/memories/style_and_conventions.md @@ -0,0 +1,17 @@ +Style and Conventions + +- Typing + - mypy strict is enabled; add precise type hints for public functions/classes. + - Prefer explicit/clear types; avoid `Any` and unsafe casts. +- Linting/Imports + - Ruff is the canonical linter (config in pyproject). Respect its rules and isort settings (single-line imports, ordered, types grouped). + - Flake8 config exists in tox.ini but ruff linting is primary. +- Formatting + - Follow ruff guidance; keep lines <= 88 where applicable (flake8 reference). +- Testing + - Pytest with `testing/` as testpath; default 5m timeout; warnings treated as errors. + - Use existing fixtures; add `@pytest.mark` markers if needed (see pyproject markers). +- Logging + - Tests run with log level info/debug; avoid noisy logs in normal library code. +- General + - Small, focused functions; early returns; explicit errors. Keep APIs documented with concise docstrings. diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md new file mode 100644 index 00000000..8eeeab96 --- /dev/null +++ b/.serena/memories/suggested_commands.md @@ -0,0 +1,30 @@ +Environment +- Install deps (uses default groups test, docs): + - uv sync + +Core Dev +- Run tests: + - uv run pytest +- Lint (ruff): + - uv run ruff check . + - uv run ruff check . --fix # optional autofix +- Type check (mypy strict): + - uv run mypy +- Build docs: + - uv run mkdocs serve --dev-addr localhost:8000 + - uv run mkdocs build --clean --strict + +Entrypoints / Tooling +- CLI version/debug: + - uv run python -m setuptools_scm --help + - uv run python -m setuptools_scm + - uv run setuptools-scm --help +- Build dist and verify: + - uv run python -m build + - uv run twine check dist/* +- Optional matrix via tox: + - uv run tox -q + +Git/Linux Utilities (Linux host) +- git status / git log --oneline --graph --decorate +- ls -la; find . -name "pattern"; grep -R "text" . diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 00000000..505274b8 --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,68 @@ +# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) +# * For C, use cpp +# * For JavaScript, use typescript +# Special requirements: +# * csharp: Requires the presence of a .sln file in the project folder. +language: python + +# whether to use the project's gitignore file to ignore files +# Added on 2025-04-07 +ignore_all_files_in_gitignore: true +# list of additional paths to ignore +# same syntax as gitignore, so you can use * and ** +# Was previously called `ignored_dirs`, please update your config if you are using that. +# Added (renamed)on 2025-04-07 +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file or directory. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +project_name: "setuptools_scm" diff --git a/CHANGELOG.md b/CHANGELOG.md index c8823c7e..b588430e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,148 @@ # Changelog + +## v9.2.2 + +### Fixed + +- fix #1231: don't warn about `tool.setuptools.dynamic.version` when only using file finder. + The warning about combining version guessing with setuptools dynamic versions should only + be issued when setuptools-scm is performing version inference, not when it's only being + used for its file finder functionality. + + +## v9.2.1 + +### Fixed + +- fix #1216: accept and create a warning for usages of `version = attr:` in setuptools config. + unfortunately dozens of projects cargo-culted that antipattern + + +## v9.2.0 + +### Added + +- add simplified activation via `setuptools-scm[simple]` extra + + A new streamlined way to enable version inference without requiring a `[tool.setuptools_scm]` section. + When `setuptools-scm[simple]` is in `build-system.requires` and `version` is in `project.dynamic`, + version inference is automatically enabled with default settings. + + +### Removed + +- unchecked simplified activation - too many projects use setups where it would fail + +### Changed + +- refine activation logic and add unittest for the relevant cases instead of trying to speedrun setuptools + +## v9.1.1 (yanked) + +### Fixed + +- fix #1194: correctly handle version keyword when pyproject metadata is missing + + +## v9.1.0 (yanked) + +### Fixed + +- complete reiteration of the decision logic for enabling version inference on setuptools_scm + + - shared logic for the important parts + - proper deferring based in precedence of finalize options vs version keyword + - unittestable for the parsing parts and the decision steps + + + +## v9.0.3 (yanked) + +### Fixed + +- fix #1184: verify version is dynamic if the dependency is used as indicator for enabling + +## v9.0.2 (yanked) + +### Fixed + +- fix #1184: in case setuptools-scm is a indirect dependency and no pyproject.toml section exists - don't infer the version + + +## v9.0.1 (yanked) + +### Fixed + +- fix #1180: ensure version dumping works when no scm_version is given (problems in downstreams) +- fix #1181: config - reintroduce control over when we expect a section to be present + as it turns out there's valid use cases where setuptools_scm is not direct part of the dependencies +- add codespell pre-commit hook + +## v9.0.0 (yanked) + +### Breaking + +- fix #1019: pass python version build tags from scm version to results properly + +### Added + +- add `setuptools-scm` console_scripts entry point to make the CLI directly executable +- make Mercurial command configurable by environment variable `SETUPTOOLS_SCM_HG_COMMAND` +- fix #1099 use file modification times for dirty working directory timestamps instead of current time +- fix #1059: add `SETUPTOOLS_SCM_PRETEND_METADATA` environment variable to override individual ScmVersion fields +- add `scm` parameter support to `get_version()` function for nested SCM configuration +- fix #987: expand documentation on git archival files and add cli tools for good defaults +- fix #311: document github/gitlab ci pipelines that enable auto-upload to test-pypi/pypi +- fix #1022: allow `version_keyword` to override `infer_version` when configuration differs +- fix #554: document `fallback_root` parameter in programmatic usage and configuration + + +### Changed + +- add `pip` to test optional dependencies for improved uv venv compatibility +- migrate to selectable entrypoints for better extensibility +- improve typing for entry_points +- refactor file modification time logic into shared helper function for better maintainability +- reduce complexity of HgWorkdir.get_meta method by extracting focused helper methods +- fix #1150: enable setuptools-scm when we are a build requirement +- feature #1154: add the commit id the the default version file template +- drop scriv +- fix #921: document setuptools version requirements more consistently - 61 as minimum asn 8 as recommended minimum + +### Fixed + +- fix #1145: ensure GitWorkdir.get_head_date returns consistent UTC dates regardless of local timezone +- fix #687: ensure calendar versioning tests use consistent time context to prevent failures around midnight in non-UTC timezones +- reintroduce Python 3.9 entrypoints shim for compatibility +- fix #1136: update customizing.md to fix missing import +- fix #1001: document the missing version schemes and add examples in the docs +- fix #1115: explicitly document file finder behaviour +- fix #879: add test that validates case different behavior on windows +- migrate git describe command to new scm config +- add support for failing on missing submodules +- fix #279: expand errors when scm can be found upwards and relative_to wasn't used +- fix #577: introduce explicit scmversion node and short node +- fix #1100: add workaround for readthedocs worktress to the docs +- fix #790: document shallow fail for rtd +- fix #474: expand version not found error message to provide clearer guidance about SETUPTOOLS_SCM_PRETEND_VERSION_FOR_* environment variables +- fix #324: document/recommend the v tag prefix +- fix #501: add py.typed +- fix #804: git - use fallback version instead of 0.0 when no version is found at all +- fix #1139: use logging.lastResort instead of a own replica to avoid polluting logging._handlerList +- fix #873: don't infer version in cli if --no-version is given +- fix #535: accept tags from a release action in the gh ui +- fix #1073: explain namespaces for release-branch-semver +- fix #1052: use consistent node hash length across all SCM backends +- fix #1045: reindent the `__all__` in the version template for better readability +- fix #968: harden environment override finding with better normalization and typo suggestions +- fix #846: add support for failing on missing submodules + ## v8.3.1 ### Fixed -- fixed #1131: allow self-build without importlib_metadata avaliable on python3.9 +- fixed #1131: allow self-build without importlib_metadata available on python3.9 ## v8.3.0 diff --git a/MANIFEST.in b/MANIFEST.in index 6b9e3204..b793e6c0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -18,8 +18,10 @@ include CHANGELOG.md recursive-include testing *.bash prune nextgen +prune .cursor recursive-include docs *.md include docs/examples/version_scheme_code/*.py include docs/examples/version_scheme_code/*.toml include mkdocs.yml +include uv.lock \ No newline at end of file diff --git a/README.md b/README.md index e1f06f82..f4ca4bf9 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,20 @@ files that are managed by the SCM Unwanted files must be excluded via `MANIFEST.in` or [configuring Git archive][git-archive-docs]. +> **⚠️ Important:** Installing setuptools-scm automatically enables a file finder that includes **all SCM-tracked files** in your source distributions. This can be surprising if you have development files tracked in Git/Mercurial that you don't want in your package. Use `MANIFEST.in` to exclude unwanted files. See the [documentation] for details. + ## `pyproject.toml` usage The preferred way to configure [setuptools-scm] is to author settings in a `tool.setuptools_scm` section of `pyproject.toml`. -This feature requires setuptools 61 or later. +This feature requires setuptools 61 or later (recommended: >=80 for best compatibility). First, ensure that [setuptools-scm] is present during the project's build step by specifying it as one of the build requirements. ```toml title="pyproject.toml" [build-system] -requires = ["setuptools>=64", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" ``` @@ -51,6 +53,24 @@ dynamic = ["version"] [tool.setuptools_scm] ``` +!!! note "Simplified Configuration" + + Starting with setuptools-scm 8.1+, if `setuptools_scm` (or `setuptools-scm`) is + present in your `build-system.requires`, the `[tool.setuptools_scm]` section + becomes optional! You can now enable setuptools-scm with just: + + ```toml title="pyproject.toml" + [build-system] + requires = ["setuptools>=80", "setuptools-scm>=8"] + build-backend = "setuptools.build_meta" + + [project] + dynamic = ["version"] + ``` + + The `[tool.setuptools_scm]` section is only needed if you want to customize + configuration options. + Additionally, a version file can be written by specifying: ```toml title="pyproject.toml" @@ -88,6 +108,12 @@ modern [setuptools-scm] is unable to support them sensibly. It's strongly recommended to build a wheel artifact using modern Python and setuptools, then installing the artifact instead of trying to run against old setuptools versions. +!!! note "Legacy Setuptools Support" + While setuptools-scm recommends setuptools >=80, it maintains compatibility with setuptools 61+ + to support legacy deployments that cannot easily upgrade. Support for setuptools <80 is deprecated + and will be removed in a future release. This allows enterprise environments and older CI/CD systems + to continue using setuptools-scm while still encouraging adoption of newer versions. + ## Code of Conduct diff --git a/_own_version_helper.py b/_own_version_helper.py index d0d7433c..12ffeb07 100644 --- a/_own_version_helper.py +++ b/_own_version_helper.py @@ -9,6 +9,7 @@ from __future__ import annotations import logging +import os from typing import Callable @@ -22,6 +23,7 @@ from setuptools_scm.fallbacks import parse_pkginfo from setuptools_scm.version import ScmVersion from setuptools_scm.version import get_local_node_and_date +from setuptools_scm.version import get_no_local_node from setuptools_scm.version import guess_next_dev_version log = logging.getLogger("setuptools_scm") @@ -48,11 +50,18 @@ def parse(root: str, config: Configuration) -> ScmVersion | None: def scm_version() -> str: + # Use no-local-version if SETUPTOOLS_SCM_NO_LOCAL is set (for CI uploads) + local_scheme = ( + get_no_local_node + if os.environ.get("SETUPTOOLS_SCM_NO_LOCAL") + else get_local_node_and_date + ) + return get_version( relative_to=__file__, parse=parse, version_scheme=guess_next_dev_version, - local_scheme=get_local_node_and_date, + local_scheme=local_scheme, ) diff --git a/docs/config.md b/docs/config.md index b30fce86..83d11e2b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,31 @@ # Configuration +## When is configuration needed? + +setuptools-scm provides flexible activation options: + +### Simplified Activation (No Configuration Needed) + +For basic usage, use the `simple` extra with no configuration: + +```toml title="pyproject.toml" +[build-system] +requires = ["setuptools>=80", "setuptools-scm[simple]>=8"] + +[project] +dynamic = ["version"] +``` + +This automatically enables version inference with default settings. + +### Explicit Configuration (Full Control) + +Use the `[tool.setuptools_scm]` section when you need to: + - Write version files (`version_file`) + - Customize version schemes (`version_scheme`, `local_scheme`) + - Set custom tag patterns (`tag_regex`) + - Configure fallback behavior (`fallback_version`) + - Or any other non-default behavior ## configuration parameters @@ -55,6 +81,14 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_ named `version`, that captures the actual version information. Defaults to the value of [setuptools_scm._config.DEFAULT_TAG_REGEX][] + which supports tags with optional "v" prefix (recommended), project prefixes, + and various version formats. + + !!! tip + + The default regex supports common tag formats like `v1.0.0`, `myproject-v1.0.0`, + and `1.0.0`. For best practices on tag naming, see + [Version Tag Formats](usage.md#version-tag-formats). `parentdir_prefix_version: str|None = None` : If the normal methods for detecting the version (SCM version, @@ -74,17 +108,46 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_ unset (the default), `setuptools-scm` will error if it fails to detect the version. +`fallback_root: Path | PathLike[str] = "."` +: The directory to use when SCM metadata is not available (e.g., in extracted + archives like PyPI tarballs). This is particularly useful for legacy + configurations that need to work both in development (with SCM metadata) + and from archives (without SCM metadata). Defaults to the current directory. + + When SCM metadata is present, the `root` parameter is used; when it's not + available, `fallback_root` is used instead. This allows the same configuration + to work in both scenarios without modification. + `parse: Callable[[Path, Config], ScmVersion] | None = None` : A function that will be used instead of the discovered SCM for parsing the version. Use with caution, this is a function for advanced use and you should be familiar with the `setuptools-scm` internals to use it. -`git_describe_command` +`scm.git.describe_command` : This command will be used instead the default `git describe --long` command. Defaults to the value set by [setuptools_scm.git.DEFAULT_DESCRIBE][] +`scm.git.pre_parse` +: A string specifying which git pre-parse function to use before parsing version information. + Available options: + + - `"warn_on_shallow"` (default): Warns when the repository is shallow + - `"fail_on_shallow"`: Fails with an error when the repository is shallow + - `"fetch_on_shallow"`: Automatically fetches to rectify shallow repositories + - `"fail_on_missing_submodules"`: Fails when submodules are defined but not initialized + + The `"fail_on_missing_submodules"` option is useful to prevent packaging incomplete + projects when submodules are required for a complete build. + + Note: This setting is overridden by any explicit `pre_parse` parameter passed to the git parse function. + +`git_describe_command` (deprecated) +: **Deprecated since 8.4.0**: Use `scm.git.describe_command` instead. + + This field is maintained for backward compatibility but will issue a deprecation warning when used. + `normalize` : A boolean flag indicating if the version string should be normalized. Defaults to `True`. Setting this to `False` is equivalent to setting @@ -118,7 +181,7 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_ it is strongly recommended to use distribution-specific pretend versions (see below). -`SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}` +`SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}` : used as the primary source for the version number, in which case it will be an unparsed string. Specifying distribution-specific pretend versions will @@ -143,8 +206,73 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_ : a ``os.pathsep`` separated list of directory names to ignore for root finding +`SETUPTOOLS_SCM_HG_COMMAND` +: command used for running Mercurial (defaults to ``hg``) + + for example, set this to ``chg`` to reduce start-up overhead of Mercurial + + + + + +## automatic file inclusion + +!!! warning "Setuptools File Finder Integration" + + `setuptools-scm` automatically registers a setuptools file finder that includes all SCM-tracked files in source distributions. This behavior is **always active** when setuptools-scm is installed, regardless of whether you use it for versioning. + +**How it works:** + +`setuptools-scm` provides a `setuptools.file_finders` entry point that: + +1. Automatically discovers SCM-managed files (Git, Mercurial) +2. Includes them in source distributions (`python -m build --sdist`) +3. Works for `include_package_data = True` in package building + +**Entry point registration:** +```toml +[project.entry-points."setuptools.file_finders"] +setuptools_scm = "setuptools_scm._file_finders:find_files" +``` + +**Files included by default:** +- All files tracked by Git (`git ls-files`) +- All files tracked by Mercurial (`hg files`) +- Includes: source code, documentation, tests, config files, etc. +- Excludes: untracked files, files in `.gitignore`/`.hgignore` + +**Controlling inclusion:** + +Use `MANIFEST.in` to override the automatic behavior: + +```text title="MANIFEST.in" +# Exclude development files +exclude .pre-commit-config.yaml +exclude tox.ini +global-exclude *.pyc __pycache__/ + +# Exclude entire directories +prune docs/ +prune testing/ + +# Include non-SCM files +include data/important.json +``` + +**Debugging file inclusion:** + +```bash +# List files that will be included +python -m setuptools_scm ls + +# Build and inspect sdist contents +python -m build --sdist +tar -tzf dist/package-*.tar.gz +``` +!!! note "Cannot be disabled" + The file finder cannot be disabled through configuration - it's automatically active when setuptools-scm is installed. If you need to disable it completely, you must remove setuptools-scm from your build environment (which also means you can't use it for versioning). ## api reference diff --git a/docs/customizing.md b/docs/customizing.md index 616e12e9..18ee8765 100644 --- a/docs/customizing.md +++ b/docs/customizing.md @@ -33,7 +33,7 @@ setup(use_scm_version={"version_scheme": myversion_func}) ``` { .toml title="pyproject.toml" file="docs/examples/version_scheme_code/pyproject.toml" } [build-system] -requires = ["setuptools>=64", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm>=8"] # setuptools>=61 minimum, >=80 recommended build-backend = "setuptools.build_meta" [project] @@ -55,7 +55,7 @@ dynamic = [ With the pep 517/518 build backend, setuptools-scm is importable from `setup.py` ``` { .python title="setup.py" } -import setuptools +from setuptools import setup from setuptools_scm.version import get_local_dirty_tag def clean_scheme(version): diff --git a/docs/examples/version_scheme_code/pyproject.toml b/docs/examples/version_scheme_code/pyproject.toml index 389aad09..a30582fe 100644 --- a/docs/examples/version_scheme_code/pyproject.toml +++ b/docs/examples/version_scheme_code/pyproject.toml @@ -1,6 +1,6 @@ # ~/~ begin <>[init] [build-system] -requires = ["setuptools>=64", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm>=8"] # setuptools>=61 minimum, >=80 recommended build-backend = "setuptools.build_meta" [project] diff --git a/docs/extending.md b/docs/extending.md index 66f1ffd4..c4cc2e03 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -53,9 +53,41 @@ representing the version. `guess-next-dev (default)` : Automatically guesses the next development version (default). Guesses the upcoming release by incrementing the pre-release segment if present, - otherwise by incrementing the micro segment. Then appends :code:`.devN`. + otherwise by incrementing the micro segment. Then appends `.devN`. In case the tag ends with `.dev0` the version is not bumped - and custom `.devN` versions will trigger a error. + and custom `.devN` versions will trigger an error. + + **Examples:** + - Tag `v1.0.0` → version `1.0.1.dev0` (if dirty or distance > 0) + - Tag `v1.0.0` → version `1.0.0` (if exact match) + +`calver-by-date` +: Calendar versioning scheme that generates versions based on dates. + Uses the format `YY.MM.DD.patch` or `YYYY.MM.DD.patch` depending on the existing tag format. + If the commit is on the same date as the latest tag, increments the patch number. + Otherwise, uses the current date with patch 0. Supports branch-specific versioning + for release branches. + + **Examples:** + - Tag `v23.01.15.0` on same day → version `23.01.15.1.devN` + - Tag `v23.01.15.0` on different day (e.g., 2023-01-16) → version `23.01.16.0.devN` + - Tag `v2023.01.15.0` → uses 4-digit year format for new versions + +`no-guess-dev` +: Does no next version guessing, just adds `.post1.devN`. + This is the recommended replacement for the deprecated `post-release` scheme. + + **Examples:** + - Tag `v1.0.0` → version `1.0.0.post1.devN` (if distance > 0) + - Tag `v1.0.0` → version `1.0.0` (if exact match) + +`only-version` +: Only use the version from the tag, as given. + + !!! warning "This means version is no longer pseudo unique per commit" + + **Examples:** + - Tag `v1.0.0` → version `1.0.0` (always, regardless of distance or dirty state) `post-release (deprecated)` : Generates post release versions (adds `.postN`) @@ -64,6 +96,9 @@ representing the version. !!! warning "the recommended replacement is `no-guess-dev`" + **Examples:** + - Tag `1.0.0` → version `1.0.0.postN` (where N is the distance) + `python-simplified-semver` : Basic semantic versioning. @@ -73,6 +108,10 @@ representing the version. This scheme is not compatible with pre-releases. + **Examples:** + - Tag `1.0.0` on non-feature branch → version `1.0.1.devN` + - Tag `1.0.0` on feature branch → version `1.1.0.devN` + `release-branch-semver` : Semantic versioning for projects with release branches. The same as `guess-next-dev` (incrementing the pre-release or micro segment) @@ -81,14 +120,13 @@ representing the version. non-release branch, increments the minor segment and sets the micro segment to zero, then appends `.devN` -`no-guess-dev` -: Does no next version guessing, just adds `.post1.devN` + Namespaces are unix pathname separated parts of a branch/tag name. -`only-version` -: Only use the version from the tag, as given. - - !!! warning "This means version is no longer pseudo unique per commit" + **Examples:** + - Tag `1.0.0` on release branch `release-1.0` → version `1.0.1.devN` + - Tag `1.0.0` on release branch `release/v1.0` → version `1.0.1.devN` + - Tag `1.0.0` on development branch → version `1.1.0.devN` ### `setuptools_scm.local_scheme` Configures how the local part of a version is rendered given a diff --git a/docs/index.md b/docs/index.md index b40dbf42..c86f93ce 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,6 +10,16 @@ files that are managed by the SCM Unwanted files must be excluded via `MANIFEST.in` or [configuring Git archive][git-archive-docs]. +!!! warning "Automatic File Inclusion Behavior" + + **Important:** Simply installing `setuptools-scm` as a build dependency will automatically enable its file finder, which includes **all SCM-tracked files** in your source distributions. This happens even if you're not using setuptools-scm for versioning. + + - ✅ **Expected**: All Git/Mercurial tracked files will be included in your sdist + - ⚠️ **Surprise**: This includes development files, configs, tests, docs, etc. + - 🛠️ **Control**: Use `MANIFEST.in` to exclude unwanted files + + See the [File Finder Documentation](usage.md#file-finders-hook-makes-most-of-manifestin-unnecessary) for details. + [git-archive-docs]: usage.md#builtin-mechanisms-for-obtaining-version-numbers ## Basic usage @@ -17,12 +27,14 @@ or [configuring Git archive][git-archive-docs]. ### With setuptools Note: `setuptools-scm>=8` intentionally doesn't depend on setuptools to ease non-setuptools usage. -Please ensure a recent version of setuptools (>=64) is installed. +Please ensure a recent version of setuptools is installed (minimum: >=61, recommended: >=80 for best compatibility). +Support for setuptools <80 is deprecated and will be removed in a future release. +**Simplified setup (recommended for basic usage):** ```toml title="pyproject.toml" [build-system] -requires = ["setuptools>=64", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm[simple]>=8"] build-backend = "setuptools.build_meta" [project] @@ -30,13 +42,40 @@ name = "example" # Important: Remove any existing version declaration # version = "0.0.1" dynamic = ["version"] -# more missing -[tool.setuptools_scm] +# No additional configuration needed! +``` + +**With custom configuration:** + +```toml title="pyproject.toml" +[build-system] +requires = ["setuptools>=80", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" +[project] +name = "example" +dynamic = ["version"] + +[tool.setuptools_scm] +# Custom configuration options go here ``` +!!! tip "Recommended Tag Format" + + Use the **"v" prefix** for your version tags for best compatibility: + + ```bash + git tag v1.0.0 + git tag v1.1.0 + git tag v2.0.0-rc1 + ``` + + This is a widely adopted convention that works well with setuptools-scm and other tools. + See the [Version Tag Formats](usage.md#version-tag-formats) section for more details. + + ### With hatch [Hatch-vcs](https://github.com/ofek/hatch-vcs) integrates with setuptools-scm diff --git a/docs/integrations.md b/docs/integrations.md new file mode 100644 index 00000000..69b18486 --- /dev/null +++ b/docs/integrations.md @@ -0,0 +1,345 @@ +# Integrations + +## ReadTheDocs + +### Avoid having a dirty Git index + +When building documentation on ReadTheDocs, file changes during the build process can cause setuptools-scm to detect a "dirty" working directory. + +To avoid this issue, ReadTheDocs recommends using build customization to clean the Git state after checkout: + +```yaml title=".readthedocs.yaml" +version: 2 +build: + os: "ubuntu-22.04" + tools: + python: "3.10" + jobs: + post_checkout: + # Avoid setuptools-scm dirty Git index issues + - git reset --hard HEAD + - git clean -fdx +``` + +This ensures a clean Git working directory before setuptools-scm detects the version, preventing unwanted local version components. + + + +Reference: [ReadTheDocs Build Customization - Avoid having a dirty Git index](https://docs.readthedocs.com/platform/stable/build-customization.html#avoid-having-a-dirty-git-index) + + +### Enforce fail on shallow repositories + +ReadTheDocs may sometimes use shallow Git clones that lack the full history needed for proper version detection. You can use setuptools-scm's environment variable override system to enforce `fail_on_shallow` when building on ReadTheDocs: + +```yaml title=".readthedocs.yaml" +version: 2 +build: + os: "ubuntu-22.04" + tools: + python: "3.10" + jobs: + post_checkout: + # Avoid setuptools-scm dirty Git index issues + - git reset --hard HEAD + - git clean -fdx + # Enforce fail_on_shallow for setuptools-scm + - export SETUPTOOLS_SCM_OVERRIDES_FOR_${READTHEDOCS_PROJECT//-/_}='{scm.git.pre_parse="fail_on_shallow"}' +``` + +This configuration uses the `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}` environment variable to override the `scm.git.pre_parse` setting specifically for your project when building on ReadTheDocs, forcing setuptools-scm to fail with a clear error if the repository is shallow. + +## CI/CD and Package Publishing + +### Publishing to PyPI from CI/CD + +When publishing packages to PyPI or test-PyPI from CI/CD pipelines, you often need to remove local version components that are not allowed on public package indexes according to [PEP 440](https://peps.python.org/pep-0440/#local-version-identifiers). + +setuptools-scm provides the `no-local-version` local scheme and environment variable overrides to handle this scenario cleanly. + +#### The Problem + +By default, setuptools-scm generates version numbers like: +- `1.2.3.dev4+g1a2b3c4d5` (development version with git hash) +- `1.2.3+dirty` (dirty working directory) + +These local version components (`+g1a2b3c4d5`, `+dirty`) prevent uploading to PyPI. + +#### The Solution + +Use the `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}` environment variable to override the `local_scheme` to `no-local-version` when building for upload to PyPI. + +### GitHub Actions Example + +Here's a complete GitHub Actions workflow that: +- Runs tests on all branches +- Uploads development versions to test-PyPI from feature branches +- Uploads development versions to PyPI from the main branch (with no-local-version) +- Uploads tagged releases to PyPI (using exact tag versions) + +```yaml title=".github/workflows/ci.yml" +name: CI/CD + +on: + push: + branches: ["main", "develop"] + pull_request: + branches: ["main", "develop"] + release: + types: [published] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + with: + # Fetch full history for setuptools-scm + fetch-depth: 0 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build pytest + pip install -e . + + - name: Run tests + run: pytest + + publish-test-pypi: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref != 'refs/heads/main' + env: + # Replace MYPACKAGE with your actual package name (normalized) + # For package "my-awesome.package", use "MY_AWESOME_PACKAGE" + SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE: '{"local_scheme": "no-local-version"}' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Upload to test-PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + + publish-pypi: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + # Replace MYPACKAGE with your actual package name (normalized) + SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE: '{"local_scheme": "no-local-version"}' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Upload to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + + publish-release: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'release' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Upload to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} +``` + +### GitLab CI Example + +Here's an equivalent GitLab CI configuration: + +```yaml title=".gitlab-ci.yml" +stages: + - test + - publish + +variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + +cache: + paths: + - .cache/pip/ + +before_script: + - python -m pip install --upgrade pip + +test: + stage: test + image: python:3.11 + script: + - pip install build pytest + - pip install -e . + - pytest + parallel: + matrix: + - PYTHON_VERSION: ["3.8", "3.9", "3.10", "3.11", "3.12"] + image: python:${PYTHON_VERSION} + +publish-test-pypi: + stage: publish + image: python:3.11 + variables: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: $TEST_PYPI_API_TOKEN + # Replace MYPACKAGE with your actual package name (normalized) + SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE: '{"local_scheme": "no-local-version"}' + script: + - pip install build twine + - python -m build + - twine upload --repository testpypi dist/* + rules: + - if: $CI_COMMIT_BRANCH != "main" && $CI_PIPELINE_SOURCE == "push" + +publish-pypi: + stage: publish + image: python:3.11 + variables: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: $PYPI_API_TOKEN + # Replace MYPACKAGE with your actual package name (normalized) + SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE: '{"local_scheme": "no-local-version"}' + script: + - pip install build twine + - python -m build + - twine upload dist/* + rules: + - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push" + +publish-release: + stage: publish + image: python:3.11 + variables: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: $PYPI_API_TOKEN + script: + - pip install build twine + - python -m build + - twine upload dist/* + rules: + - if: $CI_COMMIT_TAG +``` + +### Configuration Details + +#### Environment Variable Format + +The environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}` must be set where: + +1. **`${DIST_NAME}`** is your package name normalized according to PEP 503: + - Convert to uppercase + - Replace hyphens and dots with underscores + - Examples: `my-package` → `MY_PACKAGE`, `my.package` → `MY_PACKAGE` + +2. **Value** must be a valid TOML inline table format: + ```bash + SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE='{"local_scheme": "no-local-version"}' + ``` + +#### Alternative Approaches + +**Option 1: pyproject.toml Configuration** + +Instead of environment variables, you can configure this in your `pyproject.toml`: + +```toml title="pyproject.toml" +[tool.setuptools_scm] +# Use no-local-version by default for CI builds +local_scheme = "no-local-version" +``` + +However, the environment variable approach is preferred for CI/CD as it allows different schemes for local development vs. CI builds. + +#### Version Examples + +**Development versions from main branch** (with `local_scheme = "no-local-version"`): +- Development commit: `1.2.3.dev4+g1a2b3c4d5` → `1.2.3.dev4` ✅ (uploadable to PyPI) +- Dirty working directory: `1.2.3+dirty` → `1.2.3` ✅ (uploadable to PyPI) + +**Tagged releases** (without overrides, using default local scheme): +- Tagged commit: `1.2.3` → `1.2.3` ✅ (uploadable to PyPI) +- Tagged release on dirty workdir: `1.2.3+dirty` → `1.2.3+dirty` ❌ (should not happen in CI) + +### Security Notes + +- Store PyPI API tokens as repository secrets +- Use separate tokens for test-PyPI and production PyPI +- Consider using [Trusted Publishers](https://docs.pypi.org/trusted-publishers/) for enhanced security + +### Troubleshooting + +**Package name normalization**: If your override isn't working, verify the package name normalization: + +```python +import re +dist_name = "my-awesome.package" +normalized = re.sub(r"[-_.]+", "-", dist_name) +env_var_name = normalized.replace("-", "_").upper() +print(f"SETUPTOOLS_SCM_OVERRIDES_FOR_{env_var_name}") +# Output: SETUPTOOLS_SCM_OVERRIDES_FOR_MY_AWESOME_PACKAGE +``` + +**Fetch depth**: Always use `fetch-depth: 0` in GitHub Actions to ensure setuptools-scm has access to the full git history for proper version calculation. \ No newline at end of file diff --git a/docs/overrides.md b/docs/overrides.md index 5a6093bb..4d136db2 100644 --- a/docs/overrides.md +++ b/docs/overrides.md @@ -7,12 +7,82 @@ setuptools-scm provides a mechanism to override the version number build time. the environment variable `SETUPTOOLS_SCM_PRETEND_VERSION` is used as the override source for the version number unparsed string. -to be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}` +to be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}` where the dist name normalization follows adapted PEP 503 semantics. +## pretend metadata + +setuptools-scm provides a mechanism to override individual version metadata fields at build time. + +The environment variable `SETUPTOOLS_SCM_PRETEND_METADATA` accepts a TOML inline table +with field overrides for the ScmVersion object. + +To be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_METADATA_FOR_${DIST_NAME}` +where the dist name normalization follows adapted PEP 503 semantics. + +### Supported fields + +The following ScmVersion fields can be overridden: + +- `distance` (int): Number of commits since the tag +- `node` (str): The commit hash/node identifier +- `dirty` (bool): Whether the working directory has uncommitted changes +- `branch` (str): The branch name +- `node_date` (date): The date of the commit (TOML date format: `2024-01-15`) +- `time` (datetime): The version timestamp (TOML datetime format) +- `preformatted` (bool): Whether the version string is preformatted +- `tag`: The version tag (can be string or version object) + +### Examples + +Override commit hash and distance: +```bash +export SETUPTOOLS_SCM_PRETEND_METADATA='{node="g1337beef", distance=4}' +``` + +Override multiple fields with proper TOML types: +```bash +export SETUPTOOLS_SCM_PRETEND_METADATA='{node="gabcdef12", distance=7, dirty=true, node_date=2024-01-15}' +``` + +Use with a specific package: +```bash +export SETUPTOOLS_SCM_PRETEND_METADATA_FOR_MY_PACKAGE='{node="g1234567", distance=2}' +``` + +!!! note "Node ID Prefixes" + + Node IDs must include the appropriate SCM prefix: + + - Use `g` prefix for git repositories (e.g., `g1a2b3c4d5`) + - Use `h` prefix for mercurial repositories (e.g., `h1a2b3c4d5`) + + This ensures consistency with setuptools-scm's automatic node ID formatting. + +### Use case: CI/CD environments + +This is particularly useful for solving issues where version file templates need access to +commit metadata that may not be available in certain build environments: + +```toml +[tool.setuptools_scm] +version_file = "src/mypackage/_version.py" +version_file_template = ''' +version = "{version}" +commit_hash = "{scm_version.node}" +commit_count = {scm_version.distance} +''' +``` + +With pretend metadata, you can ensure the template gets the correct values: +```bash +export SETUPTOOLS_SCM_PRETEND_VERSION="1.2.3.dev4+g1337beef" +export SETUPTOOLS_SCM_PRETEND_METADATA='{node="g1337beef", distance=4}' +``` + ## config overrides -setuptools-scm parses the environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` +setuptools-scm parses the environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}` as a toml inline map to override the configuration data from `pyproject.toml`. ## subprocess timeouts diff --git a/docs/usage.md b/docs/usage.md index 49716ee6..53f70445 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -2,14 +2,49 @@ ## At build time -The preferred way to configure `setuptools-scm` is to author -settings in the `tool.setuptools_scm` section of `pyproject.toml`. +!!! note "Setuptools Version Requirements" + setuptools-scm requires setuptools 61 or later (minimum), but recommends >=80 for best compatibility. + Support for setuptools <80 is deprecated and will be removed in a future release. + The examples below use `setuptools>=80` as the recommended version. -It's necessary to use a setuptools version released after 2022. +There are three ways to enable `setuptools-scm` at build time: + +### Simplified Activation (new) + +For basic usage without custom configuration, use the `simple` extra: ```toml title="pyproject.toml" [build-system] -requires = ["setuptools>=64", "setuptools-scm>=8"] +requires = ["setuptools>=80", "setuptools-scm[simple]>=8"] +build-backend = "setuptools.build_meta" + +[project] +# version = "0.0.1" # Remove any existing version parameter. +dynamic = ["version"] + +# No [tool.setuptools_scm] section needed for basic usage! +``` + +This streamlined approach automatically enables version inference when: +- `setuptools-scm[simple]` is listed in `build-system.requires` +- `version` is included in `project.dynamic` + +!!! tip "When to use simplified activation" + + Use simplified activation when you: + - Want basic SCM version inference with default settings + - Don't need custom version schemes or file writing + - Prefer minimal configuration + + Upgrade to explicit configuration if you need customization. + +### Explicit Configuration (full control) + +Add a `tool.setuptools_scm` section for custom configuration: + +```toml title="pyproject.toml" +[build-system] +requires = ["setuptools>=80", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] @@ -17,13 +52,34 @@ build-backend = "setuptools.build_meta" dynamic = ["version"] [tool.setuptools_scm] -# can be empty if no extra settings are needed, presence enables setuptools-scm +# Configure custom options here (version schemes, file writing, etc.) +version_file = "src/mypackage/_version.py" + +# Example: Git-specific configuration +[tool.setuptools_scm.scm.git] +pre_parse = "fail_on_missing_submodules" # Fail if submodules are not initialized +describe_command = "git describe --dirty --tags --long --exclude *js*" # Custom describe command ``` -That will be sufficient to require `setuptools-scm` for projects -that support PEP 518 ([pip](https://pypi.org/project/pip) and -[pep517](https://pypi.org/project/pep517/)). -Tools that still invoke `setup.py` must ensure build requirements are installed +Projects must support PEP 518 ([pip](https://pypi.org/project/pip) and +[pep517](https://pypi.org/project/pep517/)). Tools that still invoke `setup.py` +must ensure build requirements are installed. + +### Using the setup keyword + +Alternatively, enable `setuptools-scm` via the `use_scm_version` keyword in `setup.py`. +This also counts as an explicit opt-in and does not require a tool section. + +!!! note "Legacy simplified activation" + + Previous versions had a "simplified" activation where listing `setuptools_scm` + in `build-system.requires` together with `project.dynamic = ["version"]` would + auto-enable version inference. This behavior was removed due to regressions and + ambiguous activation. + + The new simplified activation using the `[simple]` extra provides the same + convenience but with explicit opt-in, making it clear when version inference + should be enabled. ### Version files @@ -137,8 +193,24 @@ from setuptools_scm import get_version version = get_version(root='..', relative_to=__file__) ``` +For legacy configurations or when working with extracted archives (like PyPI tarballs), +you may need to specify a `fallback_root` parameter. This is particularly useful +for legacy Sphinx configurations that use `get_version()` instead of getting the +version from the installed package: + +```python +from setuptools_scm import get_version +# For legacy Sphinx conf.py that needs to work both in development and from archives +version = get_version(root='..', fallback_root='..', relative_to=__file__) +``` + +The `fallback_root` parameter specifies the directory to use when the SCM metadata +is not available (e.g., in extracted tarballs), while `root` is used when SCM +metadata is present. + ### Usage from Sphinx +The recommended approach for Sphinx configurations is to use the installed package metadata: ``` {.python file=docs/.entangled/sphinx_conf.py} from importlib.metadata import version as get_version @@ -151,6 +223,21 @@ The underlying reason is that services like *Read the Docs* sometimes change the working directory for good reasons and using the installed metadata prevents using needless volatile data there. +!!! note "Legacy Sphinx configurations" + + If you have a legacy Sphinx configuration that still uses `setuptools_scm.get_version()` + directly (instead of `importlib.metadata`), you may need to use the `fallback_root` + parameter to ensure it works both in development and when building from archives: + + ```python + from setuptools_scm import get_version + # Legacy approach - use fallback_root for archive compatibility + release = get_version(root='..', fallback_root='..', relative_to=__file__) + version = ".".join(release.split('.')[:2]) + ``` + + However, it's strongly recommended to migrate to the `importlib.metadata` approach above. + ### With Docker/Podman @@ -185,7 +272,7 @@ Note that running this Dockerfile requires docker with BuildKit enabled To avoid BuildKit and mounting of the .git folder altogether, one can also pass the desired version as a build argument. -Note that `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}` +Note that `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}` is preferred over `SETUPTOOLS_SCM_PRETEND_VERSION`. @@ -217,6 +304,100 @@ For Git projects, the version relies on [git describe](https://git-scm.com/docs so you will see an additional `g` prepended to the `{revision hash}`. +## Version Tag Formats + +setuptools-scm automatically detects version information from SCM tags. The default tag regex +supports a wide variety of tag formats, with the **"v" prefix being recommended** for clarity +and consistency. + +### Recommended Tag Format + +**Use the "v" prefix for version tags:** + +```bash +git tag v1.0.0 # Recommended +git tag v2.1.3 +git tag v1.0.0-alpha1 +git tag v1.0.0-rc1 +``` + +### Supported Tag Formats + +setuptools-scm's default tag regex supports: + +- **Version prefix**: `v` or `V` (optional, but recommended) +- **Project prefix**: Optional project name followed by dashes (e.g., `myproject-v1.0.0`) +- **Version number**: Standard semantic versioning patterns +- **Pre-release suffixes**: Alpha, beta, RC versions +- **Build metadata**: Anything after `+` is ignored + +**Examples of valid tags:** +```bash +# Recommended formats (with v prefix) +v1.0.0 +v2.1.3 +v1.0.0-alpha1 +v1.0.0-beta2 +v1.0.0-rc1 +v1.2.3-dev +V1.0.0 # Capital V also works + +# Project-prefixed formats +myproject-v1.0.0 +my-lib-v2.1.0 + +# Without v prefix (supported but not recommended) +1.0.0 +2.1.3 +1.0.0-alpha1 + +# With build metadata (metadata after + is ignored) +v1.0.0+build.123 +v1.0.0+20240115 +``` + +### Why Use the "v" Prefix? + +1. **Clarity**: Makes it immediately obvious that the tag represents a version +2. **Convention**: Widely adopted standard across the software industry +3. **Git compatibility**: Works well with git's tag sorting and filtering +4. **Tool compatibility**: Many other tools expect version tags to have a "v" prefix + +### Custom Tag Patterns + +If you need different tag patterns, you can customize the tag regex: + +```toml title="pyproject.toml" +[tool.setuptools_scm] +tag_regex = "^release-(?P[0-9]+\\.[0-9]+\\.[0-9]+)$" +``` + +## Node ID Prefixes + +setuptools-scm automatically prepends identifying characters to node IDs (commit/revision hashes) +to distinguish between different SCM systems: + +- **Git repositories**: Node IDs are prefixed with `g` (e.g., `g1a2b3c4d5`) +- **Mercurial repositories**: Node IDs are prefixed with `h` (e.g., `h1a2b3c4d5`) + +This prefixing serves several purposes: + +1. **SCM identification**: Makes it clear which version control system was used +2. **Consistency**: Ensures predictable node ID format across different SCM backends +3. **Debugging**: Helps identify the source SCM when troubleshooting version issues + +The prefixes are automatically added by setuptools-scm and should be included when manually +specifying node IDs in environment variables like `SETUPTOOLS_SCM_PRETEND_METADATA`. + +**Examples:** +```bash +# Git node ID +1.0.0.dev5+g1a2b3c4d5 + +# Mercurial node ID +1.0.0.dev5+h1a2b3c4d5 +``` + !!! note According to [PEP 440](https://peps.python.org/pep-0440/#local-version-identifiers>), @@ -246,15 +427,41 @@ accordingly. ### Git archives -Git archives are supported, but a few changes to your repository are required. +Git archives are supported, but require specific setup and understanding of how they work with package building. + +#### Overview + +When you create a `.git_archival.txt` file in your repository, it enables setuptools-scm to extract version information from git archives (e.g., GitHub's source downloads). However, this file contains template placeholders that must be expanded by `git archive` - they won't work when building directly from your working directory. -Ensure the content of the following files: +#### Setting up git archival support +You can generate a `.git_archival.txt` file using the setuptools-scm CLI: + +```commandline +# Generate a stable archival file (recommended for releases) +$ python -m setuptools_scm create-archival-file --stable + +# Generate a full archival file with all metadata (use with caution) +$ python -m setuptools_scm create-archival-file --full +``` + +Alternatively, you can create the file manually: + +**Stable version (recommended):** ```{ .text file=".git_archival.txt"} +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +``` +**Full version (with branch information - can cause instability):** +```{ .text file=".git_archival.txt"} +# WARNING: Including ref-names can make archive checksums unstable +# after commits are added post-release. Use only if describe-name is insufficient. node: $Format:%H$ node-date: $Format:%cI$ describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +ref-names: $Format:%D$ ``` Feel free to alter the `match` field in `describe-name` to match your project's @@ -272,30 +479,168 @@ tagging style. .git_archival.txt export-subst ``` -Finally, don't forget to commit the two files: +Finally, commit both files: ```commandline -$ git add .git_archival.txt .gitattributes && git commit -m "add export config" +$ git add .git_archival.txt .gitattributes && git commit -m "add git archive support" +``` + +#### Understanding the warnings + +If you see warnings like these when building your package: + +``` +UserWarning: git archive did not support describe output +UserWarning: unprocessed git archival found (no export subst applied) ``` +This typically happens when: -Note that if you are creating a `_version.py` file, note that it should not -be kept in version control. It's strongly recommended to be put into gitignore. +1. **Building from working directory**: You're running `python -m build` directly in your repository +2. **Sdist extraction**: A build tool extracts your sdist to build wheels, but the extracted directory isn't a git repository + +#### Recommended build workflows + +**For development builds:** +Exclude `.git_archival.txt` from your package to avoid warnings: + +```{ .text file="MANIFEST.in"} +# Exclude archival file from development builds +exclude .git_archival.txt +``` + +**For release builds from archives:** +Build from an actual git archive to ensure proper template expansion: + +```commandline +# Create archive from a specific tag/commit +$ git archive --output=../source_archive.tar v1.2.3 +$ cd .. +$ tar -xf source_archive.tar +$ cd extracted_directory/ +$ python -m build . +``` + +**For automated releases:** +Many CI systems and package repositories (like GitHub Actions) automatically handle this correctly when building from git archives. + +#### Integration with package managers + +**MANIFEST.in exclusions:** +```{ .text file="MANIFEST.in"} +# Exclude development files from packages +exclude .git_archival.txt +exclude .gitattributes +``` + + +```{ .text file=".gitattributes"} +# Archive configuration +.git_archival.txt export-subst +.gitignore export-ignore +``` + +#### Troubleshooting + +**Problem: "unprocessed git archival found" warnings** +- ✅ **Solution**: Add `exclude .git_archival.txt` to `MANIFEST.in` for development builds +- ✅ **Alternative**: Build from actual git archives for releases + +**Problem: "git archive did not support describe output" warnings** +- ℹ️ **Information**: This is expected when `.git_archival.txt` contains unexpanded templates +- ✅ **Solution**: Same as above - exclude file or build from git archives + +**Problem: Version detection fails in git archives** +- ✅ **Check**: Is `.gitattributes` configured with `export-subst`? +- ✅ **Check**: Are you building from a properly created git archive? +- ✅ **Check**: Does your git hosting provider support archive template expansion? + +!!! warning "Branch Names and Archive Stability" + + Including `ref-names: $Format:%D$` in your `.git_archival.txt` can make archive checksums change when new commits are added to branches referenced in the archive. This primarily affects GitHub's automatic source archives. Use the stable format (without `ref-names`) unless you specifically need branch information and understand the stability implications. + +!!! note "Version Files" + + If you are creating a `_version.py` file, it should not be kept in version control. Add it to `.gitignore`: + ``` + # Generated version file + src/mypackage/_version.py + ``` [git-archive-issue]: https://github.com/pypa/setuptools-scm/issues/806 ### File finders hook makes most of `MANIFEST.in` unnecessary +!!! warning "Automatic File Inclusion" + + **`setuptools-scm` automatically provides a setuptools file finder by default.** This means that when you install setuptools-scm, it will automatically include **all SCM-tracked files** in your source distributions (sdist) without requiring a `MANIFEST.in` file. + + This automatic behavior can be surprising if you're not expecting it. The file finder is active as soon as setuptools-scm is installed in your build environment. + `setuptools-scm` implements a [file_finders] entry point which returns all files tracked by your SCM. This eliminates the need for a manually constructed `MANIFEST.in` in most cases where this -would be required when not using `setuptools-scm`, namely: +would be required when not using `setuptools-scm`. + +[file_finders]: https://setuptools.pypa.io/en/stable/userguide/extension.html + +#### How it works + +1. **Automatic Discovery**: When building source distributions (`python -m build --sdist`), setuptools automatically calls the `setuptools-scm` file finder +2. **SCM Integration**: The file finder queries your SCM (Git/Mercurial) for all tracked files +3. **Inclusion**: All tracked files are automatically included in the sdist + +#### Controlling file inclusion + +**To exclude unwanted files:** + +1. **Use `MANIFEST.in`** to exclude specific files/patterns: + ``` + exclude development.txt + recursive-exclude tests *.pyc + ``` + +2. **Configure Git archive** (for Git repositories): + ```bash + # Add to .gitattributes + tests/ export-ignore + *.md export-ignore + ``` + +3. **Use `.hgignore`** or **Mercurial archive configuration** (for Mercurial repositories) + +#### Troubleshooting + +**Problem: Unwanted files in my package** +- ✅ **Solution**: Add exclusions to `MANIFEST.in` +- ✅ **Alternative**: Use Git/Mercurial archive configuration + +**Problem: Missing files in package** +- ✅ **Check**: Are the files tracked in your SCM? +- ✅ **Solution**: `git add` missing files or override with `MANIFEST.in` + +**Problem: File finder not working** +- ✅ **Check**: Is setuptools-scm installed in your build environment? +- ✅ **Check**: Are you in a valid SCM repository? + +### Timestamps for Local Development Versions + +!!! info "Improved Timestamp Behavior" + + When your working directory has uncommitted changes (dirty), setuptools-scm now uses the **actual modification time of changed files** instead of the current time for local version schemes like `node-and-date`. + + **Before**: Dirty working directories always used current time (`now`) + **Now**: Uses the latest modification time of changed files, falling back to current time only if no changed files are found + + This provides more stable and meaningful timestamps that reflect when you actually made changes to your code. + +**How it works:** -* To ensure all relevant files are packaged when running the `sdist` command. - * When using [include_package_data] to include package data as part of the `build` or `bdist_wheel`. +1. **Clean repository**: Uses commit timestamp from SCM +2. **Dirty repository**: Uses latest modification time of changed files +3. **Fallback**: Uses current time if no modification times can be determined -`MANIFEST.in` may still be used: anything defined there overrides the hook. -This is mostly useful to exclude files tracked in your SCM from packages, -although in principle it can be used to explicitly include non-tracked files too. +**Benefits:** -[file_finders]: https://setuptools.pypa.io/en/latest/userguide/extension.html#adding-support-for-revision-control-systems -[include_package_data]: https://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files +- More stable builds during development +- Timestamps reflect actual change times +- Better for reproducible development workflows diff --git a/hatch.toml b/hatch.toml index aad1b874..52ba7b06 100644 --- a/hatch.toml +++ b/hatch.toml @@ -11,13 +11,9 @@ python = ["3.8", "3.9", "3.10", "3.11"] [envs.docs] python = "3.11" extras = ["docs"] -dependencies = ["scriv"] [envs.docs.scripts] build = "mkdocs build --clean --strict" serve = "mkdocs serve --dev-addr localhost:8000" init = "mkdocs {args}" -sync = ["entangled sync"] - -changelog-create = "scriv create {args}" -changelog-collect = "scriv collect {args}" \ No newline at end of file +sync = ["entangled sync"] \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 7d0553f8..5b1f42ef 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,7 @@ nav: - usage.md - customizing.md - config.md + - integrations.md - extending.md - overrides.md - changelog.md diff --git a/pyproject.toml b/pyproject.toml index b66940ee..d6005cd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,11 +46,15 @@ dependencies = [ "setuptools", # >= 61", 'tomli>=1; python_version < "3.11"', 'typing-extensions; python_version < "3.10"', - 'importlib-metadata>=4.6; python_version < "3.10"', ] [project.optional-dependencies] +rich = ["rich"] +simple = [] +toml = [] + +[dependency-groups] docs = [ - "entangled-cli~=2.0", + #"entangled-cli~=2.0", "mkdocs", "mkdocs-entangled-plugin", "mkdocs-include-markdown-plugin", @@ -58,23 +62,27 @@ docs = [ "mkdocstrings[python]", "pygments", ] -rich = [ - "rich", -] test = [ + "pip", "build", "pytest", + "pytest-timeout", # Timeout protection for CI/CD "rich", + "ruff", + "mypy~=1.13.0", # pinned to old for python 3.8 'typing-extensions; python_version < "3.11"', "wheel", -] -toml = [ + "griffe", + "flake8", ] [project.urls] documentation = "https://setuptools-scm.readthedocs.io/" repository = "https://github.com/pypa/setuptools-scm/" +[project.entry-points.console_scripts] +setuptools-scm = "setuptools_scm._cli:main" + [project.entry-points."distutils.setup_keywords"] use_scm_version = "setuptools_scm._integration.setuptools:version_keyword" @@ -147,6 +155,7 @@ ignore = ["PP305", "GH103", "GH212", "MY100", "PC111", "PC160", "PC170", "PC180" [tool.pytest.ini_options] minversion = "8" testpaths = ["testing"] +timeout = 300 # 5 minutes timeout per test for CI protection filterwarnings = [ "error", "ignore:.*tool\\.setuptools_scm.*", @@ -155,11 +164,11 @@ filterwarnings = [ log_level = "debug" log_cli_level = "info" # disable unraisable until investigated -addopts = ["-ra", "--strict-config", "--strict-markers", "-p", "no:unraisableexception"] +addopts = ["-ra", "--strict-config", "--strict-markers"] markers = [ "issue(id): reference to github issue", "skip_commit: allows to skip committing in the helpers", ] -[tool.scriv] -format = "md" +[tool.uv] +default-groups = ["test", "docs"] diff --git a/src/setuptools_scm/_cli.py b/src/setuptools_scm/_cli.py index b54903a4..1f104f46 100644 --- a/src/setuptools_scm/_cli.py +++ b/src/setuptools_scm/_cli.py @@ -5,6 +5,7 @@ import os import sys +from pathlib import Path from typing import Any from setuptools_scm import Configuration @@ -33,10 +34,13 @@ def main(args: list[str] | None = None) -> int: file=sys.stderr, ) config = Configuration(root=inferred_root) - - version = _get_version( - config, force_write_version_files=opts.force_write_version_files - ) + version: str | None + if opts.no_version: + version = "0.0.0+no-version-was-requested.fake-version" + else: + version = _get_version( + config, force_write_version_files=opts.force_write_version_files + ) if version is None: raise SystemExit("ERROR: no version found for", opts) if opts.strip_dev: @@ -103,6 +107,28 @@ def _get_cli_opts(args: list[str] | None) -> argparse.Namespace: # We avoid `metavar` to prevent printing repetitive information desc = "List information about the package, e.g. included files" sub.add_parser("ls", help=desc[0].lower() + desc[1:], description=desc) + + # Add create-archival-file subcommand + archival_desc = "Create .git_archival.txt file for git archive support" + archival_parser = sub.add_parser( + "create-archival-file", + help=archival_desc[0].lower() + archival_desc[1:], + description=archival_desc, + ) + archival_group = archival_parser.add_mutually_exclusive_group(required=True) + archival_group.add_argument( + "--stable", + action="store_true", + help="create stable archival file (recommended, no branch names)", + ) + archival_group.add_argument( + "--full", + action="store_true", + help="create full archival file with branch information (can cause instability)", + ) + archival_parser.add_argument( + "--force", action="store_true", help="overwrite existing .git_archival.txt file" + ) return parser.parse_args(args) @@ -113,6 +139,9 @@ def command(opts: argparse.Namespace, version: str, config: Configuration) -> in if opts.command == "ls": opts.query = ["files"] + if opts.command == "create-archival-file": + return _create_archival_file(opts, config) + if opts.query == []: opts.no_version = True sys.stderr.write("Available queries:\n\n") @@ -184,3 +213,79 @@ def _find_pyproject(parent: str) -> str: return os.path.abspath( "pyproject.toml" ) # use default name to trigger the default errors + + +def _create_archival_file(opts: argparse.Namespace, config: Configuration) -> int: + """Create .git_archival.txt file with appropriate content.""" + archival_path = Path(config.root, ".git_archival.txt") + + # Check if file exists and force flag + if archival_path.exists() and not opts.force: + print( + f"Error: {archival_path} already exists. Use --force to overwrite.", + file=sys.stderr, + ) + return 1 + + if opts.stable: + content = _get_stable_archival_content() + print("Creating stable .git_archival.txt (recommended for releases)") + elif opts.full: + content = _get_full_archival_content() + print("Creating full .git_archival.txt with branch information") + print("WARNING: This can cause archive checksums to be unstable!") + + try: + archival_path.write_text(content, encoding="utf-8") + print(f"Created: {archival_path}") + + gitattributes_path = Path(config.root, ".gitattributes") + needs_gitattributes = True + + if gitattributes_path.exists(): + # TODO: more nuanced check later + gitattributes_content = gitattributes_path.read_text("utf-8") + if ( + ".git_archival.txt" in gitattributes_content + and "export-subst" in gitattributes_content + ): + needs_gitattributes = False + + if needs_gitattributes: + print("\nNext steps:") + print("1. Add this line to .gitattributes:") + print(" .git_archival.txt export-subst") + print("2. Commit both files:") + print(" git add .git_archival.txt .gitattributes") + print(" git commit -m 'add git archive support'") + else: + print("\nNext step:") + print("Commit the archival file:") + print(" git add .git_archival.txt") + print(" git commit -m 'update git archival file'") + + return 0 + except OSError as e: + print(f"Error: Could not create {archival_path}: {e}", file=sys.stderr) + return 1 + + +def _get_stable_archival_content() -> str: + """Generate stable archival file content (no branch names).""" + return """\ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +""" + + +def _get_full_archival_content() -> str: + """Generate full archival file content with branch information.""" + return """\ +# WARNING: Including ref-names can make archive checksums unstable +# after commits are added post-release. Use only if describe-name is insufficient. +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +ref-names: $Format:%D$ +""" diff --git a/src/setuptools_scm/_compat.py b/src/setuptools_scm/_compat.py new file mode 100644 index 00000000..4e9e301f --- /dev/null +++ b/src/setuptools_scm/_compat.py @@ -0,0 +1,65 @@ +"""Compatibility utilities for cross-platform functionality.""" + +from __future__ import annotations + + +def normalize_path_for_assertion(path: str) -> str: + """Normalize path separators for cross-platform assertions. + + On Windows, this converts backslashes to forward slashes to ensure + path comparisons work correctly. On other platforms, returns the path unchanged. + The length of the string is not changed by this operation. + + Args: + path: The path string to normalize + + Returns: + The path with normalized separators + """ + return path.replace("\\", "/") + + +def strip_path_suffix( + full_path: str, suffix_path: str, error_msg: str | None = None +) -> str: + """Strip a suffix from a path, with cross-platform path separator handling. + + This function first normalizes path separators for Windows compatibility, + then asserts that the full path ends with the suffix, and finally returns + the path with the suffix removed. This is the common pattern used for + computing parent directories from git output. + + Args: + full_path: The full path string + suffix_path: The suffix path to strip from the end + error_msg: Optional custom error message for the assertion + + Returns: + The prefix path with the suffix removed + + Raises: + AssertionError: If the full path doesn't end with the suffix + """ + normalized_full = normalize_path_for_assertion(full_path) + + if error_msg: + assert normalized_full.endswith(suffix_path), error_msg + else: + assert normalized_full.endswith(suffix_path), ( + f"Path assertion failed: {full_path!r} does not end with {suffix_path!r}" + ) + + return full_path[: -len(suffix_path)] + + +# Legacy aliases for backward compatibility during transition +def assert_path_endswith( + full_path: str, suffix_path: str, error_msg: str | None = None +) -> None: + """Legacy alias - use strip_path_suffix instead.""" + strip_path_suffix(full_path, suffix_path, error_msg) + + +def compute_path_prefix(full_path: str, suffix_path: str) -> str: + """Legacy alias - use strip_path_suffix instead.""" + return strip_path_suffix(full_path, suffix_path) diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index 6ed520f9..49fac2a4 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -8,12 +8,17 @@ import warnings from pathlib import Path +from typing import TYPE_CHECKING from typing import Any from typing import Pattern from typing import Protocol +if TYPE_CHECKING: + from . import git + from . import _log from . import _types as _t +from ._integration.pyproject_reading import PyProjectData from ._integration.pyproject_reading import ( get_args_for_pyproject as _get_args_for_pyproject, ) @@ -25,6 +30,57 @@ log = _log.log.getChild("config") + +def _is_called_from_dataclasses() -> bool: + """Check if the current call is from the dataclasses module.""" + import inspect + + frame = inspect.currentframe() + try: + # Walk up to 7 frames to check for dataclasses calls + current_frame = frame + assert current_frame is not None + for _ in range(7): + current_frame = current_frame.f_back + if current_frame is None: + break + if "dataclasses.py" in current_frame.f_code.co_filename: + return True + return False + finally: + del frame + + +class _GitDescribeCommandDescriptor: + """Data descriptor for deprecated git_describe_command field.""" + + def __get__( + self, obj: Configuration | None, objtype: type[Configuration] | None = None + ) -> _t.CMD_TYPE | None: + if obj is None: + return self # type: ignore[return-value] + + # Only warn if not being called by dataclasses.replace or similar introspection + is_from_dataclasses = _is_called_from_dataclasses() + if not is_from_dataclasses: + warnings.warn( + "Configuration field 'git_describe_command' is deprecated. " + "Use 'scm.git.describe_command' instead.", + DeprecationWarning, + stacklevel=2, + ) + return obj.scm.git.describe_command + + def __set__(self, obj: Configuration, value: _t.CMD_TYPE | None) -> None: + warnings.warn( + "Configuration field 'git_describe_command' is deprecated. " + "Use 'scm.git.describe_command' instead.", + DeprecationWarning, + stacklevel=2, + ) + obj.scm.git.describe_command = value + + DEFAULT_TAG_REGEX = re.compile( r"^(?:[\w-]+-)?(?P[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$" ) @@ -43,14 +99,21 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]: group_names = regex.groupindex.keys() if regex.groups == 0 or (regex.groups > 1 and "version" not in group_names): - warnings.warn( - "Expected tag_regex to contain a single match group or a group named" - " 'version' to identify the version part of any tag." + raise ValueError( + f"Expected tag_regex '{regex.pattern}' to contain a single match group or" + " a group named 'version' to identify the version part of any tag." ) return regex +def _get_default_git_pre_parse() -> git.GitPreParse: + """Get the default git pre_parse enum value""" + from . import git + + return git.GitPreParse.WARN_ON_SHALLOW + + class ParseFunction(Protocol): def __call__( self, root: _t.PathT, *, config: Configuration @@ -82,6 +145,54 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str: return os.path.abspath(root) +@dataclasses.dataclass +class GitConfiguration: + """Git-specific configuration options""" + + pre_parse: git.GitPreParse = dataclasses.field( + default_factory=lambda: _get_default_git_pre_parse() + ) + describe_command: _t.CMD_TYPE | None = None + + @classmethod + def from_data(cls, data: dict[str, Any]) -> GitConfiguration: + """Create GitConfiguration from configuration data, converting strings to enums""" + git_data = data.copy() + + # Convert string pre_parse values to enum instances + if "pre_parse" in git_data and isinstance(git_data["pre_parse"], str): + from . import git + + try: + git_data["pre_parse"] = git.GitPreParse(git_data["pre_parse"]) + except ValueError as e: + valid_options = [option.value for option in git.GitPreParse] + raise ValueError( + f"Invalid git pre_parse function '{git_data['pre_parse']}'. " + f"Valid options are: {', '.join(valid_options)}" + ) from e + + return cls(**git_data) + + +@dataclasses.dataclass +class ScmConfiguration: + """SCM-specific configuration options""" + + git: GitConfiguration = dataclasses.field(default_factory=GitConfiguration) + + @classmethod + def from_data(cls, data: dict[str, Any]) -> ScmConfiguration: + """Create ScmConfiguration from configuration data""" + scm_data = data.copy() + + # Handle git-specific configuration + git_data = scm_data.pop("git", {}) + git_config = GitConfiguration.from_data(git_data) + + return cls(git=git_config, **scm_data) + + @dataclasses.dataclass class Configuration: """Global configuration model""" @@ -99,13 +210,57 @@ class Configuration: version_file: _t.PathT | None = None version_file_template: str | None = None parse: ParseFunction | None = None - git_describe_command: _t.CMD_TYPE | None = None + git_describe_command: dataclasses.InitVar[_t.CMD_TYPE | None] = ( + _GitDescribeCommandDescriptor() + ) + dist_name: str | None = None version_cls: type[_VersionT] = _Version search_parent_directories: bool = False parent: _t.PathT | None = None + # Nested SCM configurations + scm: ScmConfiguration = dataclasses.field( + default_factory=lambda: ScmConfiguration() + ) + + # Deprecated fields (handled in __post_init__) + + def __post_init__(self, git_describe_command: _t.CMD_TYPE | None) -> None: + self.tag_regex = _check_tag_regex(self.tag_regex) + + # Handle deprecated git_describe_command + # Check if it's a descriptor object (happens when no value is passed) + if git_describe_command is not None and not isinstance( + git_describe_command, _GitDescribeCommandDescriptor + ): + # Check if this is being called from dataclasses + is_from_dataclasses = _is_called_from_dataclasses() + + same_value = ( + self.scm.git.describe_command is not None + and self.scm.git.describe_command == git_describe_command + ) + + if is_from_dataclasses and same_value: + # Ignore the passed value - it's from dataclasses.replace() with same value + pass + else: + warnings.warn( + "Configuration field 'git_describe_command' is deprecated. " + "Use 'scm.git.describe_command' instead.", + DeprecationWarning, + stacklevel=2, + ) + # Check for conflicts + if self.scm.git.describe_command is not None: + raise ValueError( + "Cannot specify both 'git_describe_command' (deprecated) and " + "'scm.git.describe_command'. Please use only 'scm.git.describe_command'." + ) + self.scm.git.describe_command = git_describe_command + @property def absolute_root(self) -> str: return _check_absolute_root(self.root, self.relative_to) @@ -115,17 +270,22 @@ def from_file( cls, name: str | os.PathLike[str] = "pyproject.toml", dist_name: str | None = None, - _require_section: bool = True, + pyproject_data: PyProjectData | None = None, **kwargs: Any, ) -> Configuration: """ - Read Configuration from pyproject.toml (or similar). - Raises exceptions when file is not found or toml is - not installed or the file has invalid format or does - not contain the [tool.setuptools_scm] section. + Read Configuration from pyproject.toml (or similar). + Raises exceptions when file is not found or toml is + not installed or the file has invalid format. + + Parameters: + - name: path to pyproject.toml + - dist_name: name of the distribution + - **kwargs: additional keyword arguments to pass to the Configuration constructor """ - pyproject_data = _read_pyproject(Path(name), require_section=_require_section) + if pyproject_data is None: + pyproject_data = _read_pyproject(Path(name)) args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs) args.update(read_toml_overrides(args["dist_name"])) @@ -140,13 +300,19 @@ def from_data( given configuration data create a config instance after validating tag regex/version class """ - tag_regex = _check_tag_regex(data.pop("tag_regex", None)) version_cls = _validate_version_cls( data.pop("version_cls", None), data.pop("normalize", True) ) + + # Handle nested SCM configuration + scm_data = data.pop("scm", {}) + + # Handle nested SCM configuration + + scm_config = ScmConfiguration.from_data(scm_data) return cls( relative_to=relative_to, version_cls=version_cls, - tag_regex=tag_regex, + scm=scm_config, **data, ) diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py index 3333eb5c..74a18a7d 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -7,7 +7,6 @@ from typing import Callable from typing import Iterator from typing import cast -from typing import overload from . import _log from . import version @@ -21,22 +20,29 @@ from ._config import Configuration from ._config import ParseFunction - if sys.version_info[:2] < (3, 10): - import importlib_metadata as im - else: - from importlib import metadata as im - +from importlib import metadata as im log = _log.log.getChild("entrypoints") -def entry_points(**kw: Any) -> im.EntryPoints: - if sys.version_info[:2] < (3, 10): - import importlib_metadata as im - else: - import importlib.metadata as im +if sys.version_info[:2] < (3, 10): + + def entry_points(*, group: str, name: str | None = None) -> list[im.EntryPoint]: + # Python 3.9: entry_points() returns dict, need to handle filtering manually + + eps = im.entry_points() # Returns dict - return im.entry_points(**kw) + group_eps = eps.get(group, []) + if name is not None: + return [ep for ep in group_eps if ep.name == name] + return group_eps +else: + + def entry_points(*, group: str, name: str | None = None) -> im.EntryPoints: + kw = {"group": group} + if name is not None: + kw["name"] = name + return im.entry_points(**kw) def version_from_entrypoint( @@ -95,32 +101,26 @@ def _iter_version_schemes( yield scheme_value -@overload def _call_version_scheme( version: version.ScmVersion, entrypoint: str, given_value: _t.VERSION_SCHEMES, - default: str, -) -> str: ... - - -@overload -def _call_version_scheme( - version: version.ScmVersion, - entrypoint: str, - given_value: _t.VERSION_SCHEMES, - default: None, -) -> str | None: ... - - -def _call_version_scheme( - version: version.ScmVersion, - entrypoint: str, - given_value: _t.VERSION_SCHEMES, - default: str | None, -) -> str | None: + default: str | None = None, +) -> str: + found_any_implementation = False for scheme in _iter_version_schemes(entrypoint, given_value): + found_any_implementation = True result = scheme(version) if result is not None: return result - return default + if not found_any_implementation: + raise ValueError( + f'Couldn\'t find any implementations for entrypoint "{entrypoint}"' + f' with value "{given_value}".' + ) + if default is not None: + return default + raise ValueError( + f'None of the "{entrypoint}" entrypoints matching "{given_value}"' + " returned a value." + ) diff --git a/src/setuptools_scm/_file_finders/git.py b/src/setuptools_scm/_file_finders/git.py index 0eb23ced..4379c21a 100644 --- a/src/setuptools_scm/_file_finders/git.py +++ b/src/setuptools_scm/_file_finders/git.py @@ -39,11 +39,9 @@ def _git_toplevel(path: str) -> str | None: # ``cwd`` is absolute path to current working directory. # the below method removes the length of ``out`` from # ``cwd``, which gives the git toplevel - assert cwd.replace("\\", "/").endswith(out), f"cwd={cwd!r}\nout={out!r}" - # In windows cwd contains ``\`` which should be replaced by ``/`` - # for this assertion to work. Length of string isn't changed by replace - # ``\\`` is just and escape for `\` - out = cwd[: -len(out)] + from .._compat import strip_path_suffix + + out = strip_path_suffix(cwd, out, f"cwd={cwd!r}\nout={out!r}") log.debug("find files toplevel %s", out) return norm_real(out) except subprocess.CalledProcessError: @@ -84,8 +82,16 @@ def _git_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]: # ensure we avoid resource warnings by cleaning up the process proc.stdout.close() proc.terminate() + # Wait for process to actually terminate and be reaped + try: + proc.wait(timeout=5) # Add timeout to avoid hanging + except subprocess.TimeoutExpired: + log.warning("git archive process did not terminate gracefully, killing") + proc.kill() + proc.wait() except Exception: - if proc.wait() != 0: + # proc.wait() already called in finally block, check if it failed + if proc.returncode != 0: log.error("listing git files failed - pretending there aren't any") return set(), set() diff --git a/src/setuptools_scm/_file_finders/hg.py b/src/setuptools_scm/_file_finders/hg.py index 9115a5fa..182429c3 100644 --- a/src/setuptools_scm/_file_finders/hg.py +++ b/src/setuptools_scm/_file_finders/hg.py @@ -7,7 +7,7 @@ from .. import _types as _t from .._file_finders import is_toplevel_acceptable from .._file_finders import scm_find_files -from .._run_cmd import run as _run +from ..hg import run_hg from ..integration import data_from_mime from .pathtools import norm_real @@ -16,8 +16,8 @@ def _hg_toplevel(path: str) -> str | None: try: - return _run( - ["hg", "root"], + return run_hg( + ["root"], cwd=(path or "."), check=True, ).parse_success(norm_real) @@ -32,7 +32,7 @@ def _hg_toplevel(path: str) -> str | None: def _hg_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]: hg_files: set[str] = set() hg_dirs = {toplevel} - res = _run(["hg", "files"], cwd=toplevel) + res = run_hg(["files"], cwd=toplevel) if res.returncode: return set(), set() for name in res.stdout.splitlines(): diff --git a/src/setuptools_scm/_get_version_impl.py b/src/setuptools_scm/_get_version_impl.py index cced45e2..31bc9c39 100644 --- a/src/setuptools_scm/_get_version_impl.py +++ b/src/setuptools_scm/_get_version_impl.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses import logging import re import warnings @@ -56,12 +57,18 @@ def parse_fallback_version(config: Configuration) -> ScmVersion | None: def parse_version(config: Configuration) -> ScmVersion | None: - return ( + # First try to get a version from the normal flow + scm_version = ( _read_pretended_version_for(config) or parse_scm_version(config) or parse_fallback_version(config) ) + # Apply any metadata overrides to the version we found + from ._overrides import _apply_metadata_overrides + + return _apply_metadata_overrides(scm_version, config) + def write_version_files( config: Configuration, version: str, scm_version: ScmVersion @@ -113,21 +120,73 @@ def _get_version( return version_string +def _find_scm_in_parents(config: Configuration) -> Path | None: + """ + Search parent directories for SCM repositories when relative_to is not set. + Uses the existing entrypoint system for SCM discovery. + """ + if config.search_parent_directories: + return None + + searching_config = dataclasses.replace(config, search_parent_directories=True) + + from .discover import iter_matching_entrypoints + + for _ep in iter_matching_entrypoints( + config.absolute_root, "setuptools_scm.parse_scm", searching_config + ): + # xxx: iter_matching_entrypoints should return the parent directory, we do a hack atm + assert searching_config.parent is not None + return Path(searching_config.parent) + + return None + + def _version_missing(config: Configuration) -> NoReturn: - raise LookupError( + base_error = ( f"setuptools-scm was unable to detect version for {config.absolute_root}.\n\n" - "Make sure you're either building from a fully intact git repository " - "or PyPI tarballs. Most other sources (such as GitHub's tarballs, a " - "git checkout without the .git folder) don't contain the necessary " - "metadata and will not work.\n\n" - "For example, if you're using pip, instead of " - "https://github.com/user/proj/archive/master.zip " - "use git+https://github.com/user/proj.git#egg=proj\n\n" - "Alternatively, set the version with the environment variable " - "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME} as described " - "in https://setuptools-scm.readthedocs.io/en/latest/config." ) + # If relative_to is not set, check for SCM repositories in parent directories + scm_parent = None + if config.relative_to is None: + scm_parent = _find_scm_in_parents(config) + + if scm_parent is not None: + # Found an SCM repository in a parent directory + error_msg = ( + base_error + + f"However, a repository was found in a parent directory: {scm_parent}\n\n" + f"To fix this, you have a few options:\n\n" + f"1. Use the 'relative_to' parameter to specify the file that setuptools-scm should use as reference:\n" + f" setuptools_scm.get_version(relative_to=__file__)\n\n" + f"2. Enable parent directory search in your configuration:\n" + f" [tool.setuptools_scm]\n" + f" search_parent_directories = true\n\n" + f"3. Change your working directory to the repository root: {scm_parent}\n\n" + f"4. Set the root explicitly in your configuration:\n" + f" [tool.setuptools_scm]\n" + f' root = "{scm_parent}"\n\n' + "For more information, see: https://setuptools-scm.readthedocs.io/en/latest/config/" + ) + else: + # No SCM repository found in parent directories either + error_msg = ( + base_error + + "Make sure you're either building from a fully intact git repository " + "or PyPI tarballs. Most other sources (such as GitHub's tarballs, a " + "git checkout without the .git folder) don't contain the necessary " + "metadata and will not work.\n\n" + "For example, if you're using pip, instead of " + "https://github.com/user/proj/archive/master.zip " + "use git+https://github.com/user/proj.git#egg=proj\n\n" + "Alternatively, set the version with the environment variable " + "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME} as described " + "in https://setuptools-scm.readthedocs.io/en/latest/config/" + ) + + raise LookupError(error_msg) + def get_version( root: _t.PathT = ".", @@ -148,6 +207,7 @@ def get_version( version_cls: Any | None = None, normalize: bool = True, search_parent_directories: bool = False, + scm: dict[str, Any] | None = None, ) -> str: """ If supplied, relative_to should be a file from which root may @@ -159,7 +219,19 @@ def get_version( version_cls = _validate_version_cls(version_cls, normalize) del normalize tag_regex = parse_tag_regex(tag_regex) - config = Configuration(**locals()) + + # Handle scm parameter by converting it to ScmConfiguration + if scm is not None: + scm_config = _config.ScmConfiguration.from_data(scm) + else: + scm_config = _config.ScmConfiguration() + + # Remove scm from locals() since we handle it separately + config_params = locals().copy() + config_params.pop("scm", None) + config_params.pop("scm_config", None) + + config = _config.Configuration(scm=scm_config, **config_params) maybe_version = _get_version(config, force_write_version_files=True) if maybe_version is None: diff --git a/src/setuptools_scm/_integration/deprecation.py b/src/setuptools_scm/_integration/deprecation.py new file mode 100644 index 00000000..a1b36155 --- /dev/null +++ b/src/setuptools_scm/_integration/deprecation.py @@ -0,0 +1,20 @@ +import warnings + +from pathlib import Path + + +def warn_dynamic_version(path: Path, section: str, expression: str) -> None: + warnings.warn( + f"{path}: at [{section}]\n" + f"{expression} is forcing setuptools to override the version setuptools-scm did already set\n" + "When using setuptools-scm it's invalid to use setuptools dynamic version as well, please remove it.\n" + "Setuptools-scm is responsible for setting the version, forcing setuptools to override creates errors." + ) + + +def warn_pyproject_setuptools_dynamic_version(path: Path) -> None: + warn_dynamic_version(path, "tool.setuptools.dynamic", "version = {attr = ...}") + + +def warn_setup_cfg_dynamic_version(path: Path) -> None: + warn_dynamic_version(path, "metadata", "version = attr: ...") diff --git a/src/setuptools_scm/_integration/dump_version.py b/src/setuptools_scm/_integration/dump_version.py index a7bfcae7..06081c9f 100644 --- a/src/setuptools_scm/_integration/dump_version.py +++ b/src/setuptools_scm/_integration/dump_version.py @@ -11,12 +11,20 @@ log = parent_log.getChild("dump_version") + TEMPLATES = { ".py": """\ # file generated by setuptools-scm # don't change, don't track in version control -__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] TYPE_CHECKING = False if TYPE_CHECKING: @@ -24,16 +32,22 @@ from typing import Union VERSION_TUPLE = Tuple[Union[int, str], ...] + COMMIT_ID = Union[str, None] else: VERSION_TUPLE = object + COMMIT_ID = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE +commit_id: COMMIT_ID +__commit_id__: COMMIT_ID __version__ = version = {version!r} __version_tuple__ = version_tuple = {version_tuple!r} + +__commit_id__ = commit_id = {scm_version.short_node!r} """, ".txt": "{version}", } @@ -81,19 +95,34 @@ def _validate_template(target: Path, template: str | None) -> str: return template +class DummyScmVersion: + @property + def short_node(self) -> str | None: + return None + + def write_version_to_path( - target: Path, template: str | None, version: str, scm_version: ScmVersion | None + target: Path, + template: str | None, + version: str, + scm_version: ScmVersion | None = None, ) -> None: final_template = _validate_template(target, template) log.debug("dump %s into %s", version, target) version_tuple = _version_as_tuple(version) - if scm_version is not None: - content = final_template.format( - version=version, - version_tuple=version_tuple, - scm_version=scm_version, + if scm_version is None: + warnings.warn( + "write_version_to_path called without scm_version parameter. " + "This will be required in a future version. " + "Pass scm_version=None explicitly to suppress this warning.", + DeprecationWarning, + stacklevel=2, ) - else: - content = final_template.format(version=version, version_tuple=version_tuple) + + content = final_template.format( + version=version, + version_tuple=version_tuple, + scm_version=scm_version or DummyScmVersion(), + ) target.write_text(content, encoding="utf-8") diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index 0e4f9aa1..75d86f62 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -2,12 +2,15 @@ import warnings +from dataclasses import dataclass from pathlib import Path -from typing import NamedTuple +from typing import Sequence from .. import _log -from .setuptools import read_dist_name_from_setup_cfg +from .. import _types as _t +from .._requirement_cls import extract_package_name from .toml import TOML_RESULT +from .toml import InvalidTomlError from .toml import read_toml_content log = _log.log.getChild("pyproject_reading") @@ -15,35 +18,234 @@ _ROOT = "root" -class PyProjectData(NamedTuple): +DEFAULT_PYPROJECT_PATH = Path("pyproject.toml") +DEFAULT_TOOL_NAME = "setuptools_scm" + + +@dataclass +class PyProjectData: path: Path tool_name: str project: TOML_RESULT section: TOML_RESULT + is_required: bool + section_present: bool + project_present: bool + build_requires: list[str] + + @classmethod + def for_testing( + cls, + *, + is_required: bool = False, + section_present: bool = False, + project_present: bool = False, + project_name: str | None = None, + has_dynamic_version: bool = True, + build_requires: list[str] | None = None, + local_scheme: str | None = None, + ) -> PyProjectData: + """Create a PyProjectData instance for testing purposes.""" + project: TOML_RESULT + if project_name is not None: + project = {"name": project_name} + assert project_present + else: + project = {} + + # If project is present and has_dynamic_version is True, add dynamic=['version'] + if project_present and has_dynamic_version: + project["dynamic"] = ["version"] + + if build_requires is None: + build_requires = [] + if local_scheme is not None: + assert section_present + section = {"local_scheme": local_scheme} + else: + section = {} + return cls( + path=DEFAULT_PYPROJECT_PATH, + tool_name=DEFAULT_TOOL_NAME, + project=project, + section=section, + is_required=is_required, + section_present=section_present, + project_present=project_present, + build_requires=build_requires, + ) + + @classmethod + def empty( + cls, path: Path = DEFAULT_PYPROJECT_PATH, tool_name: str = DEFAULT_TOOL_NAME + ) -> PyProjectData: + return cls( + path=path, + tool_name=tool_name, + project={}, + section={}, + is_required=False, + section_present=False, + project_present=False, + build_requires=[], + ) @property def project_name(self) -> str | None: return self.project.get("name") + @property + def project_version(self) -> str | None: + """Return the static version from [project] if present. + + When the project declares dynamic = ["version"], the version + is intentionally omitted from [project] and this returns None. + """ + return self.project.get("version") + + def should_infer(self) -> bool: + """ + Determine if setuptools_scm should infer version based on configuration. + + Infer when: + 1. An explicit [tool.setuptools_scm] section is present, OR + 2. setuptools-scm[simple] is in build-system.requires AND + version is in project.dynamic + + Returns: + True if [tool.setuptools_scm] is present, otherwise False + """ + # Original behavior: explicit tool section + if self.section_present: + return True + + # New behavior: simple extra + dynamic version + if self.project_present: + dynamic_fields = self.project.get("dynamic", []) + if "version" in dynamic_fields: + if has_build_package_with_extra( + self.build_requires, "setuptools-scm", "simple" + ): + return True + + return False + + +def has_build_package( + requires: Sequence[str], canonical_build_package_name: str +) -> bool: + for requirement in requires: + package_name = extract_package_name(requirement) + if package_name == canonical_build_package_name: + return True + return False + + +def has_build_package_with_extra( + requires: Sequence[str], canonical_build_package_name: str, extra_name: str +) -> bool: + """Check if a build dependency has a specific extra. + + Args: + requires: List of requirement strings from build-system.requires + canonical_build_package_name: The canonical package name to look for + extra_name: The extra name to check for (e.g., "simple") + + Returns: + True if the package is found with the specified extra + """ + from .._requirement_cls import Requirement + + for requirement_string in requires: + try: + requirement = Requirement(requirement_string) + package_name = extract_package_name(requirement_string) + if package_name == canonical_build_package_name: + if extra_name in requirement.extras: + return True + except Exception: + # If parsing fails, continue to next requirement + continue + return False + def read_pyproject( - path: Path = Path("pyproject.toml"), - tool_name: str = "setuptools_scm", - require_section: bool = True, + path: Path = DEFAULT_PYPROJECT_PATH, + tool_name: str = DEFAULT_TOOL_NAME, + canonical_build_package_name: str = "setuptools-scm", + _given_result: _t.GivenPyProjectResult = None, + _given_definition: TOML_RESULT | None = None, ) -> PyProjectData: - defn = read_toml_content(path, None if require_section else {}) - try: - section = defn.get("tool", {})[tool_name] - except LookupError as e: - error = f"{path} does not contain a tool.{tool_name} section" - if require_section: - raise LookupError(error) from e - else: - log.warning("toml section missing %r", error, exc_info=True) - section = {} + """Read and parse pyproject configuration. + + This function supports dependency injection for tests via ``_given_result`` + and ``_given_definition``. + + :param path: Path to the pyproject file + :param tool_name: The tool section name (default: ``setuptools_scm``) + :param canonical_build_package_name: Normalized build requirement name + :param _given_result: Optional testing hook. Can be: + - ``PyProjectData``: returned directly + - ``InvalidTomlError`` | ``FileNotFoundError``: raised directly + - ``None``: read from filesystem (default) + :param _given_definition: Optional testing hook to provide parsed TOML content. + When provided, this dictionary is used instead of reading and parsing + the file from disk. Ignored if ``_given_result`` is provided. + """ + + if _given_result is not None: + if isinstance(_given_result, PyProjectData): + return _given_result + if isinstance(_given_result, (InvalidTomlError, FileNotFoundError)): + raise _given_result + + if _given_definition is not None: + defn = _given_definition + else: + defn = read_toml_content(path) + + requires: list[str] = defn.get("build-system", {}).get("requires", []) + is_required = has_build_package(requires, canonical_build_package_name) + + tool_section = defn.get("tool", {}) + section = tool_section.get(tool_name, {}) + section_present = tool_name in tool_section + + if not section_present: + log.warning( + "toml section missing %r does not contain a tool.%s section", + path, + tool_name, + ) project = defn.get("project", {}) - return PyProjectData(path, tool_name, project, section) + project_present = "project" in defn + pyproject_data = PyProjectData( + path, + tool_name, + project, + section, + is_required, + section_present, + project_present, + requires, + ) + + setuptools_dynamic_version = ( + defn.get("tool", {}) + .get("setuptools", {}) + .get("dynamic", {}) + .get("version", None) + ) + # Only warn if setuptools-scm is being used for version inference + # (not just file finding). When only file finders are used, it's valid + # to use tool.setuptools.dynamic.version for versioning. + if setuptools_dynamic_version is not None and pyproject_data.should_infer(): + from .deprecation import warn_pyproject_setuptools_dynamic_version + + warn_pyproject_setuptools_dynamic_version(path) + + return pyproject_data def get_args_for_pyproject( @@ -70,8 +272,6 @@ def get_args_for_pyproject( if dist_name is None: # minimal pep 621 support for figuring the pretend keys dist_name = pyproject.project_name - if dist_name is None: - dist_name = read_dist_name_from_setup_cfg() if _ROOT in kwargs: if kwargs[_ROOT] is None: kwargs.pop(_ROOT, None) diff --git a/src/setuptools_scm/_integration/setup_cfg.py b/src/setuptools_scm/_integration/setup_cfg.py new file mode 100644 index 00000000..893a9ad4 --- /dev/null +++ b/src/setuptools_scm/_integration/setup_cfg.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import os + +from dataclasses import dataclass +from pathlib import Path + +import setuptools + + +@dataclass +class SetuptoolsBasicData: + path: Path + name: str | None + version: str | None + + +def read_setup_cfg(input: str | os.PathLike[str] = "setup.cfg") -> SetuptoolsBasicData: + """Parse setup.cfg and return unified data. Does not raise if file is missing.""" + import configparser + + path = Path(input) + parser = configparser.ConfigParser() + parser.read([input], encoding="utf-8") + + name = parser.get("metadata", "name", fallback=None) + version = parser.get("metadata", "version", fallback=None) + if version is not None and "attr" in version: + from .deprecation import warn_setup_cfg_dynamic_version + + warn_setup_cfg_dynamic_version(path) + version = None + return SetuptoolsBasicData(path=path, name=name, version=version) + + +def extract_from_legacy( + dist: setuptools.Distribution, + *, + _given_legacy_data: SetuptoolsBasicData | None = None, +) -> SetuptoolsBasicData: + base = _given_legacy_data if _given_legacy_data is not None else read_setup_cfg() + if base.name is None: + base.name = dist.metadata.name + if base.version is None: + base.version = dist.metadata.version + return base diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 55ca1660..aa1c645a 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -import os import warnings from typing import Any @@ -9,23 +8,17 @@ import setuptools -from .. import _config +from .. import _types as _t +from .pyproject_reading import PyProjectData +from .pyproject_reading import read_pyproject +from .setup_cfg import SetuptoolsBasicData +from .setup_cfg import extract_from_legacy +from .toml import InvalidTomlError +from .version_inference import get_version_inference_config log = logging.getLogger(__name__) -def read_dist_name_from_setup_cfg( - input: str | os.PathLike[str] = "setup.cfg", -) -> str | None: - # minimal effort to read dist_name off setup.cfg metadata - import configparser - - parser = configparser.ConfigParser() - parser.read([input], encoding="utf-8") - dist_name = parser.get("metadata", "name", fallback=None) - return dist_name - - def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None: if int(_version.split(".")[0]) < 61: warnings.warn( @@ -34,7 +27,7 @@ def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None: ERROR: setuptools=={_version} is used in combination with setuptools-scm>=8.x Your build configuration is incomplete and previously worked by accident! -setuptools-scm requires setuptools>=61 +setuptools-scm requires setuptools>=61 (recommended: >=80) Suggested workaround if applicable: - migrating from the deprecated setup_requires mechanism to pep517/518 @@ -45,79 +38,122 @@ def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None: ) -def _assign_version( - dist: setuptools.Distribution, config: _config.Configuration -) -> None: - from .._get_version_impl import _get_version - from .._get_version_impl import _version_missing - - # todo: build time plugin - maybe_version = _get_version(config, force_write_version_files=True) - - if maybe_version is None: - _version_missing(config) - else: - assert dist.metadata.version is None - dist.metadata.version = maybe_version - - _warn_on_old_setuptools() def _log_hookstart(hook: str, dist: setuptools.Distribution) -> None: - log.debug("%s %r", hook, vars(dist.metadata)) + log.debug( + "%s %s %s %r", + hook, + id(dist), + id(dist.metadata), + {**vars(dist.metadata), "long_description": ...}, + ) -def version_keyword( - dist: setuptools.Distribution, - keyword: str, +def get_keyword_overrides( value: bool | dict[str, Any] | Callable[[], dict[str, Any]], -) -> None: - overrides: dict[str, Any] +) -> dict[str, Any]: + """normalize the version keyword input""" if value is True: - overrides = {} + return {} elif callable(value): - overrides = value() + return value() else: assert isinstance(value, dict), "version_keyword expects a dict or True" - overrides = value + return value + + +def version_keyword( + dist: setuptools.Distribution, + keyword: str, + value: bool | dict[str, Any] | Callable[[], dict[str, Any]], + *, + _given_pyproject_data: _t.GivenPyProjectResult = None, + _given_legacy_data: SetuptoolsBasicData | None = None, + _get_version_inference_config: _t.GetVersionInferenceConfig = get_version_inference_config, +) -> None: + """apply version infernce when setup(use_scm_version=...) is used + this takes priority over the finalize_options based version + """ + + _log_hookstart("version_keyword", dist) + + # Parse overrides (integration point responsibility) + overrides = get_keyword_overrides(value) assert "dist_name" not in overrides, ( "dist_name may not be specified in the setup keyword " ) - dist_name: str | None = dist.metadata.name - _log_hookstart("version_keyword", dist) - if dist.metadata.version is not None: - warnings.warn(f"version of {dist_name} already set") + legacy_data = extract_from_legacy(dist, _given_legacy_data=_given_legacy_data) + dist_name: str | None = legacy_data.name + + was_set_by_infer = getattr(dist, "_setuptools_scm_version_set_by_infer", False) + + # Exit early if overrides is empty dict AND version was set by infer + if overrides == {} and was_set_by_infer: return - if dist_name is None: - dist_name = read_dist_name_from_setup_cfg() + # Get pyproject data (support direct injection for tests) + try: + pyproject_data = read_pyproject(_given_result=_given_pyproject_data) + except FileNotFoundError: + log.debug("pyproject.toml not found, proceeding with empty configuration") + pyproject_data = PyProjectData.empty() + except InvalidTomlError as e: + log.debug("Configuration issue in pyproject.toml: %s", e) + return + + # Pass None as current_version if overrides is truthy AND version was set by infer + current_version = ( + None + if (overrides and was_set_by_infer) + else (legacy_data.version or pyproject_data.project_version) + ) - config = _config.Configuration.from_file( + result = _get_version_inference_config( dist_name=dist_name, - _require_section=False, - **overrides, + current_version=current_version, + pyproject_data=pyproject_data, + overrides=overrides, ) - _assign_version(dist, config) + result.apply(dist) + + +def infer_version( + dist: setuptools.Distribution, + *, + _given_pyproject_data: _t.GivenPyProjectResult = None, + _given_legacy_data: SetuptoolsBasicData | None = None, + _get_version_inference_config: _t.GetVersionInferenceConfig = get_version_inference_config, +) -> None: + """apply version inference from the finalize_options hook + this is the default for pyproject.toml based projects that don't use the use_scm_version keyword + + if the version keyword is used, it will override the version from this hook + as user might have passed custom code version schemes + """ -def infer_version(dist: setuptools.Distribution) -> None: _log_hookstart("infer_version", dist) - log.debug("dist %s %s", id(dist), id(dist.metadata)) - if dist.metadata.version is not None: - return # metadata already added by hook - dist_name = dist.metadata.name - if dist_name is None: - dist_name = read_dist_name_from_setup_cfg() - if not os.path.isfile("pyproject.toml"): + + legacy_data = extract_from_legacy(dist, _given_legacy_data=_given_legacy_data) + dist_name = legacy_data.name + + try: + pyproject_data = read_pyproject(_given_result=_given_pyproject_data) + except FileNotFoundError: + log.debug("pyproject.toml not found, skipping infer_version") return - if dist_name == "setuptools-scm": + except InvalidTomlError as e: + log.debug("Configuration issue in pyproject.toml: %s", e) return - try: - config = _config.Configuration.from_file(dist_name=dist_name) - except LookupError as e: - log.info(e, exc_info=True) - else: - _assign_version(dist, config) + + # Only infer when tool section present per get_version_inference_config + result = _get_version_inference_config( + dist_name=dist_name, + current_version=legacy_data.version or pyproject_data.project_version, + pyproject_data=pyproject_data, + ) + result.apply(dist) diff --git a/src/setuptools_scm/_integration/toml.py b/src/setuptools_scm/_integration/toml.py index 8ca38d97..2253287c 100644 --- a/src/setuptools_scm/_integration/toml.py +++ b/src/setuptools_scm/_integration/toml.py @@ -29,6 +29,10 @@ TOML_LOADER: TypeAlias = Callable[[str], TOML_RESULT] +class InvalidTomlError(ValueError): + """Raised when TOML data cannot be parsed.""" + + def read_toml_content(path: Path, default: TOML_RESULT | None = None) -> TOML_RESULT: try: data = path.read_text(encoding="utf-8") @@ -39,7 +43,10 @@ def read_toml_content(path: Path, default: TOML_RESULT | None = None) -> TOML_RE log.debug("%s missing, presuming default %r", path, default) return default else: - return load_toml(data) + try: + return load_toml(data) + except Exception as e: # tomllib/tomli raise different decode errors + raise InvalidTomlError(f"Invalid TOML in {path}") from e class _CheatTomlData(TypedDict): @@ -52,8 +59,11 @@ def load_toml_or_inline_map(data: str | None) -> dict[str, Any]: """ if not data: return {} - elif data[0] == "{": - data = "cheat=" + data - loaded: _CheatTomlData = cast(_CheatTomlData, load_toml(data)) - return loaded["cheat"] - return load_toml(data) + try: + if data[0] == "{": + data = "cheat=" + data + loaded: _CheatTomlData = cast(_CheatTomlData, load_toml(data)) + return loaded["cheat"] + return load_toml(data) + except Exception as e: # tomllib/tomli raise different decode errors + raise InvalidTomlError("Invalid TOML content") from e diff --git a/src/setuptools_scm/_integration/version_inference.py b/src/setuptools_scm/_integration/version_inference.py new file mode 100644 index 00000000..6258d90b --- /dev/null +++ b/src/setuptools_scm/_integration/version_inference.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING +from typing import Any +from typing import Union + +from setuptools import Distribution + +from .. import _log + +if TYPE_CHECKING: + from .pyproject_reading import PyProjectData + +log = _log.log.getChild("version_inference") + + +@dataclass +class VersionInferenceConfig: + """Configuration for version inference.""" + + dist_name: str | None + pyproject_data: PyProjectData | None + overrides: dict[str, Any] | None + + def apply(self, dist: Distribution) -> None: + """Apply version inference to the distribution.""" + version_string = infer_version_string( + self.dist_name, + self.pyproject_data, # type: ignore[arg-type] + self.overrides, + force_write_version_files=True, + ) + dist.metadata.version = version_string + + # Mark that this version was set by infer_version if overrides is None (infer_version context) + if self.overrides is None: + dist._setuptools_scm_version_set_by_infer = True # type: ignore[attr-defined] + + +@dataclass +class VersionInferenceWarning: + """Error message for user.""" + + message: str + + def apply(self, dist: Distribution) -> None: + """Apply error handling to the distribution.""" + import warnings + + warnings.warn(self.message) + + +@dataclass(frozen=True) +class VersionInferenceNoOp: + """No operation result - silent skip.""" + + def apply(self, dist: Distribution) -> None: + """Apply no-op to the distribution.""" + + +VersionInferenceResult = Union[ + VersionInferenceConfig, # Proceed with inference + VersionInferenceWarning, # Show warning + VersionInferenceNoOp, # Don't infer (silent) +] + + +def infer_version_string( + dist_name: str | None, + pyproject_data: PyProjectData, + overrides: dict[str, Any] | None = None, + *, + force_write_version_files: bool = False, +) -> str: + """ + Compute the inferred version string from the given inputs without requiring a + setuptools Distribution instance. This is a pure helper that simplifies + integration tests by avoiding file I/O and side effects on a Distribution. + + Parameters: + dist_name: Optional distribution name (used for overrides and env scoping) + pyproject_data: Parsed PyProjectData (may be constructed via for_testing()) + overrides: Optional override configuration (same keys as [tool.setuptools_scm]) + force_write_version_files: When True, apply write_to/version_file effects + + Returns: + The computed version string. + """ + from .. import _config as _config_module + from .._get_version_impl import _get_version + from .._get_version_impl import _version_missing + + config = _config_module.Configuration.from_file( + dist_name=dist_name, pyproject_data=pyproject_data, **(overrides or {}) + ) + + maybe_version = _get_version( + config, force_write_version_files=force_write_version_files + ) + if maybe_version is None: + _version_missing(config) + return maybe_version + + +def get_version_inference_config( + dist_name: str | None, + current_version: str | None, + pyproject_data: PyProjectData, + overrides: dict[str, Any] | None = None, +) -> VersionInferenceResult: + """ + Determine whether and how to perform version inference. + + Args: + dist_name: The distribution name + current_version: Current version if any + pyproject_data: PyProjectData from parser (None if file doesn't exist) + overrides: Override configuration (None for no overrides) + + Returns: + VersionInferenceResult with the decision and configuration + """ + + config = VersionInferenceConfig( + dist_name=dist_name, + pyproject_data=pyproject_data, + overrides=overrides, + ) + + inference_implied = pyproject_data.should_infer() or overrides is not None + + if inference_implied: + if current_version is None: + return config + else: + return VersionInferenceWarning( + f"version of {dist_name} already set", + ) + else: + return VersionInferenceNoOp() diff --git a/src/setuptools_scm/_log.py b/src/setuptools_scm/_log.py index 7de41231..ea17f375 100644 --- a/src/setuptools_scm/_log.py +++ b/src/setuptools_scm/_log.py @@ -39,9 +39,9 @@ def make_default_handler() -> logging.Handler: return RichHandler(console=console) except ImportError: - handler = AlwaysStdErrHandler() - handler.setFormatter(logging.Formatter("%(levelname)s %(name)s %(message)s")) - return handler + last_resort = logging.lastResort + assert last_resort is not None + return last_resort _default_handler = make_default_handler() diff --git a/src/setuptools_scm/_node_utils.py b/src/setuptools_scm/_node_utils.py new file mode 100644 index 00000000..1a7a2274 --- /dev/null +++ b/src/setuptools_scm/_node_utils.py @@ -0,0 +1,46 @@ +"""Private utilities for consistent node ID handling across SCM backends.""" + +from __future__ import annotations + +# Standard node ID length used across all SCM backends +_NODE_ID_LENGTH = 10 + + +def _slice_node_id(node_id: str) -> str: + """ + Slice a node ID to a consistent length. + + This ensures that all SCM backends (git, mercurial, archival) + return the same length node IDs for consistency. + + Args: + node_id: The full node ID/hash from the SCM + + Returns: + The node ID sliced to the standard length + """ + return node_id[:_NODE_ID_LENGTH] + + +def _format_node_for_output(node_id: str | None) -> str | None: + """ + Format a node ID for output, applying consistent slicing. + + Args: + node_id: The full node ID/hash from the SCM or None + + Returns: + The node ID sliced to standard length for output, or None if input was None + """ + if node_id is None: + return None + + # Handle mercurial nodes with 'h' prefix + if node_id.startswith("h"): + # For mercurial nodes, slice the part after 'h' and reconstruct + hg_hash = node_id[1:] # Remove 'h' prefix + sliced_hash = _slice_node_id(hg_hash) + return "h" + sliced_hash + + # For git nodes (with or without 'g' prefix) and others + return _slice_node_id(node_id) diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py index ee9269a7..4e06b7a7 100644 --- a/src/setuptools_scm/_overrides.py +++ b/src/setuptools_scm/_overrides.py @@ -1,9 +1,13 @@ from __future__ import annotations +import dataclasses import os -import re +from difflib import get_close_matches from typing import Any +from typing import Mapping + +from packaging.utils import canonicalize_name from . import _config from . import _log @@ -14,20 +18,261 @@ PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION" PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}" +PRETEND_METADATA_KEY = "SETUPTOOLS_SCM_PRETEND_METADATA" +PRETEND_METADATA_KEY_NAMED = PRETEND_METADATA_KEY + "_FOR_{name}" + + +def _search_env_vars_with_prefix( + prefix: str, dist_name: str, env: Mapping[str, str] +) -> list[tuple[str, str]]: + """Search environment variables with a given prefix for potential dist name matches. + + Args: + prefix: The environment variable prefix (e.g., "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_") + dist_name: The original dist name to match against + env: Environment dictionary to search in + + Returns: + List of (env_var_name, env_var_value) tuples for potential matches + """ + # Get the canonical name for comparison + canonical_dist_name = canonicalize_name(dist_name) + + matches = [] + for env_var, value in env.items(): + if env_var.startswith(prefix): + suffix = env_var[len(prefix) :] + # Normalize the suffix and compare to canonical dist name + try: + normalized_suffix = canonicalize_name(suffix.lower().replace("_", "-")) + if normalized_suffix == canonical_dist_name: + matches.append((env_var, value)) + except Exception: + # If normalization fails for any reason, skip this env var + continue + + return matches + + +def _find_close_env_var_matches( + prefix: str, expected_suffix: str, env: Mapping[str, str], threshold: float = 0.6 +) -> list[str]: + """Find environment variables with similar suffixes that might be typos. + + Args: + prefix: The environment variable prefix + expected_suffix: The expected suffix (canonicalized dist name in env var format) + env: Environment dictionary to search in + threshold: Similarity threshold for matches (0.0 to 1.0) + + Returns: + List of environment variable names that are close matches + """ + candidates = [] + for env_var in env: + if env_var.startswith(prefix): + suffix = env_var[len(prefix) :] + candidates.append(suffix) + + # Use difflib to find close matches + close_matches = get_close_matches( + expected_suffix, candidates, n=3, cutoff=threshold + ) + + return [f"{prefix}{match}" for match in close_matches if match != expected_suffix] def read_named_env( - *, tool: str = "SETUPTOOLS_SCM", name: str, dist_name: str | None + *, + tool: str = "SETUPTOOLS_SCM", + name: str, + dist_name: str | None, + env: Mapping[str, str] = os.environ, ) -> str | None: - """ """ + """Read a named environment variable, with fallback search for dist-specific variants. + + This function first tries the standard normalized environment variable name. + If that's not found and a dist_name is provided, it searches for alternative + normalizations and warns about potential issues. + + Args: + tool: The tool prefix (default: "SETUPTOOLS_SCM") + name: The environment variable name component + dist_name: The distribution name for dist-specific variables + env: Environment dictionary to search in (defaults to os.environ) + + Returns: + The environment variable value if found, None otherwise + """ + + # First try the generic version + generic_val = env.get(f"{tool}_{name}") + if dist_name is not None: - # Normalize the dist name as per PEP 503. - normalized_dist_name = re.sub(r"[-_.]+", "-", dist_name) - env_var_dist_name = normalized_dist_name.replace("-", "_").upper() - val = os.environ.get(f"{tool}_{name}_FOR_{env_var_dist_name}") + # Normalize the dist name using packaging.utils.canonicalize_name + canonical_dist_name = canonicalize_name(dist_name) + env_var_dist_name = canonical_dist_name.replace("-", "_").upper() + expected_env_var = f"{tool}_{name}_FOR_{env_var_dist_name}" + + # Try the standard normalized name first + val = env.get(expected_env_var) if val is not None: return val - return os.environ.get(f"{tool}_{name}") + + # If not found, search for alternative normalizations + prefix = f"{tool}_{name}_FOR_" + alternative_matches = _search_env_vars_with_prefix(prefix, dist_name, env) + + if alternative_matches: + # Found alternative matches - use the first one but warn + env_var, value = alternative_matches[0] + log.warning( + "Found environment variable '%s' for dist name '%s', " + "but expected '%s'. Consider using the standard normalized name.", + env_var, + dist_name, + expected_env_var, + ) + if len(alternative_matches) > 1: + other_vars = [var for var, _ in alternative_matches[1:]] + log.warning( + "Multiple alternative environment variables found: %s. Using '%s'.", + other_vars, + env_var, + ) + return value + + # No exact or alternative matches found - look for potential typos + close_matches = _find_close_env_var_matches(prefix, env_var_dist_name, env) + if close_matches: + log.warning( + "Environment variable '%s' not found for dist name '%s' " + "(canonicalized as '%s'). Did you mean one of these? %s", + expected_env_var, + dist_name, + canonical_dist_name, + close_matches, + ) + + return generic_val + + +def _read_pretended_metadata_for( + config: _config.Configuration, +) -> dict[str, Any] | None: + """read overridden metadata from the environment + + tries ``SETUPTOOLS_SCM_PRETEND_METADATA`` + and ``SETUPTOOLS_SCM_PRETEND_METADATA_FOR_$UPPERCASE_DIST_NAME`` + + Returns a dictionary with metadata field overrides like: + {"node": "g1337beef", "distance": 4} + """ + log.debug("dist name: %s", config.dist_name) + + pretended = read_named_env(name="PRETEND_METADATA", dist_name=config.dist_name) + + if pretended: + try: + metadata_overrides = load_toml_or_inline_map(pretended) + # Validate that only known ScmVersion fields are provided + valid_fields = { + "tag", + "distance", + "node", + "dirty", + "preformatted", + "branch", + "node_date", + "time", + } + invalid_fields = set(metadata_overrides.keys()) - valid_fields + if invalid_fields: + log.warning( + "Invalid metadata fields in pretend metadata: %s. " + "Valid fields are: %s", + invalid_fields, + valid_fields, + ) + # Remove invalid fields but continue processing + for field in invalid_fields: + metadata_overrides.pop(field) + + return metadata_overrides or None + except Exception as e: + log.error("Failed to parse pretend metadata: %s", e) + return None + else: + return None + + +def _apply_metadata_overrides( + scm_version: version.ScmVersion | None, + config: _config.Configuration, +) -> version.ScmVersion | None: + """Apply metadata overrides to a ScmVersion object. + + This function reads pretend metadata from environment variables and applies + the overrides to the given ScmVersion. TOML type coercion is used so values + should be provided in their correct types (int, bool, datetime, etc.). + + Args: + scm_version: The ScmVersion to apply overrides to, or None + config: Configuration object + + Returns: + Modified ScmVersion with overrides applied, or None + """ + metadata_overrides = _read_pretended_metadata_for(config) + + if not metadata_overrides: + return scm_version + + if scm_version is None: + log.warning( + "PRETEND_METADATA specified but no base version found. " + "Metadata overrides cannot be applied without a base version." + ) + return None + + log.info("Applying metadata overrides: %s", metadata_overrides) + + # Define type checks and field mappings + from datetime import date + from datetime import datetime + + field_specs: dict[str, tuple[type | tuple[type, type], str]] = { + "distance": (int, "int"), + "dirty": (bool, "bool"), + "preformatted": (bool, "bool"), + "node_date": (date, "date"), + "time": (datetime, "datetime"), + "node": ((str, type(None)), "str or None"), + "branch": ((str, type(None)), "str or None"), + # tag is special - can be multiple types, handled separately + } + + # Apply each override individually using dataclasses.replace for type safety + result = scm_version + + for field, value in metadata_overrides.items(): + if field in field_specs: + expected_type, type_name = field_specs[field] + assert isinstance(value, expected_type), ( + f"{field} must be {type_name}, got {type(value).__name__}: {value!r}" + ) + result = dataclasses.replace(result, **{field: value}) + elif field == "tag": + # tag can be Version, NonNormalizedVersion, or str - we'll let the assignment handle validation + result = dataclasses.replace(result, tag=value) + else: + # This shouldn't happen due to validation in _read_pretended_metadata_for + log.warning("Unknown field '%s' in metadata overrides", field) + + # Ensure config is preserved (should not be overridden) + assert result.config is config, "Config must be preserved during metadata overrides" + + return result def _read_pretended_version_for( @@ -43,8 +288,6 @@ def _read_pretended_version_for( pretended = read_named_env(name="PRETEND_VERSION", dist_name=config.dist_name) if pretended: - # we use meta here since the pretended version - # must adhere to the pep to begin with return version.meta(tag=pretended, preformatted=True, config=config) else: return None diff --git a/src/setuptools_scm/_requirement_cls.py b/src/setuptools_scm/_requirement_cls.py new file mode 100644 index 00000000..9bb88462 --- /dev/null +++ b/src/setuptools_scm/_requirement_cls.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +__all__ = ["Requirement", "extract_package_name"] + +try: + from packaging.requirements import Requirement + from packaging.utils import canonicalize_name +except ImportError: + from setuptools.extern.packaging.requirements import ( # type: ignore[import-not-found,no-redef] + Requirement as Requirement, + ) + from setuptools.extern.packaging.utils import ( # type: ignore[import-not-found,no-redef] + canonicalize_name as canonicalize_name, + ) + +from . import _log + +log = _log.log.getChild("requirement_cls") + + +def extract_package_name(requirement_string: str) -> str: + """Extract the canonical package name from a requirement string. + + This function uses packaging.requirements.Requirement to properly parse + the requirement and extract the package name, handling all edge cases + that the custom regex-based approach might miss. + + Args: + requirement_string: The requirement string to parse + + Returns: + The package name as a string + """ + return canonicalize_name(Requirement(requirement_string).name) diff --git a/src/setuptools_scm/_run_cmd.py b/src/setuptools_scm/_run_cmd.py index 5d5ec15e..2dff6369 100644 --- a/src/setuptools_scm/_run_cmd.py +++ b/src/setuptools_scm/_run_cmd.py @@ -81,6 +81,16 @@ def parse_success( return parse(self.stdout) +KEEP_GIT_ENV = ( + "GIT_CEILING_DIRECTORIES", + "GIT_EXEC_PATH", + "GIT_SSH", + "GIT_SSH_COMMAND", + "GIT_AUTHOR_DATE", + "GIT_COMMITTER_DATE", +) + + def no_git_env(env: Mapping[str, str]) -> dict[str, str]: # adapted from pre-commit # Too many bugs dealing with environment variables and GIT: @@ -95,11 +105,7 @@ def no_git_env(env: Mapping[str, str]) -> dict[str, str]: if k.startswith("GIT_"): log.debug("%s: %s", k, v) return { - k: v - for k, v in env.items() - if not k.startswith("GIT_") - or k - in ("GIT_CEILING_DIRECTORIES", "GIT_EXEC_PATH", "GIT_SSH", "GIT_SSH_COMMAND") + k: v for k, v in env.items() if not k.startswith("GIT_") or k in KEEP_GIT_ENV } diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py index b655c76f..4f8874fb 100644 --- a/src/setuptools_scm/_types.py +++ b/src/setuptools_scm/_types.py @@ -5,10 +5,13 @@ from typing import TYPE_CHECKING from typing import Callable from typing import List +from typing import Protocol from typing import Sequence from typing import Tuple from typing import Union +from setuptools import Distribution + if TYPE_CHECKING: import sys @@ -18,6 +21,8 @@ from typing_extensions import TypeAlias from . import version + from ._integration.pyproject_reading import PyProjectData + from ._integration.toml import InvalidTomlError PathT: TypeAlias = Union["os.PathLike[str]", str] @@ -26,3 +31,31 @@ VERSION_SCHEME: TypeAlias = Union[str, Callable[["version.ScmVersion"], str]] VERSION_SCHEMES: TypeAlias = Union[List[str], Tuple[str, ...], VERSION_SCHEME] SCMVERSION: TypeAlias = "version.ScmVersion" + +# Git pre-parse function types +GIT_PRE_PARSE: TypeAlias = Union[str, None] + +# Testing injection types for configuration reading +GivenPyProjectResult: TypeAlias = Union[ + "PyProjectData", "InvalidTomlError", FileNotFoundError, None +] + + +class VersionInferenceApplicable(Protocol): + """A result object from version inference decision that can be applied to a dist.""" + + def apply(self, dist: Distribution) -> None: # pragma: no cover - structural type + ... + + +class GetVersionInferenceConfig(Protocol): + """Callable protocol for the decision function used by integration points.""" + + def __call__( + self, + dist_name: str | None, + current_version: str | None, + pyproject_data: PyProjectData, + overrides: dict[str, object] | None = None, + ) -> VersionInferenceApplicable: # pragma: no cover - structural type + ... diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index 5be2f89d..966ab69c 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -2,7 +2,6 @@ import dataclasses import logging -import operator import os import re import shlex @@ -12,6 +11,7 @@ from datetime import date from datetime import datetime from datetime import timezone +from enum import Enum from os.path import samefile from pathlib import Path from typing import TYPE_CHECKING @@ -26,6 +26,7 @@ from ._run_cmd import run as _run from .integration import data_from_mime from .scm_workdir import Workdir +from .scm_workdir import get_latest_file_mtime from .version import ScmVersion from .version import meta from .version import tag_to_version @@ -46,11 +47,21 @@ "--dirty", "--tags", "--long", + "--abbrev=40", "--match", "*[0-9]*", ] +class GitPreParse(Enum): + """Available git pre-parse functions""" + + WARN_ON_SHALLOW = "warn_on_shallow" + FAIL_ON_SHALLOW = "fail_on_shallow" + FETCH_ON_SHALLOW = "fetch_on_shallow" + FAIL_ON_MISSING_SUBMODULES = "fail_on_missing_submodules" + + def run_git( args: Sequence[str | os.PathLike[str]], repo: Path, @@ -82,11 +93,9 @@ def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdir | None: real_wd = os.fspath(wd) else: str_wd = os.fspath(wd) - assert str_wd.replace("\\", "/").endswith(real_wd) - # In windows wd contains ``\`` which should be replaced by ``/`` - # for this assertion to work. Length of string isn't changed by replace - # ``\\`` is just and escape for `\` - real_wd = str_wd[: -len(real_wd)] + from ._compat import strip_path_suffix + + real_wd = strip_path_suffix(str_wd, real_wd) log.debug("real root %s", real_wd) if not samefile(real_wd, wd): return None @@ -123,7 +132,13 @@ def parse_timestamp(timestamp_text: str) -> date | None: return None if sys.version_info < (3, 11) and timestamp_text.endswith("Z"): timestamp_text = timestamp_text[:-1] + "+00:00" - return datetime.fromisoformat(timestamp_text).date() + + # Convert to UTC to ensure consistent date regardless of local timezone + dt = datetime.fromisoformat(timestamp_text) + log.debug("dt: %s", dt) + dt_utc = dt.astimezone(timezone.utc).date() + log.debug("dt utc: %s", dt_utc) + return dt_utc res = run_git( [ @@ -138,6 +153,28 @@ def parse_timestamp(timestamp_text: str) -> date | None: error_msg="logging the iso date for head failed", ) + def get_dirty_tag_date(self) -> date | None: + """Get the latest modification time of changed files in the working directory. + + Returns the date of the most recently modified file that has changes, + or None if no files are changed or if an error occurs. + """ + if not self.is_dirty(): + return None + + try: + # Get list of changed files + changed_files_res = run_git(["diff", "--name-only"], self.path) + if changed_files_res.returncode != 0: + return None + + changed_files = changed_files_res.stdout.strip().split("\n") + return get_latest_file_mtime(changed_files, self.path) + + except Exception as e: + log.debug("Failed to get dirty tag date: %s", e) + return None + def is_shallow(self) -> bool: return self.path.joinpath(".git/shallow").is_file() @@ -145,12 +182,10 @@ def fetch_shallow(self) -> None: run_git(["fetch", "--unshallow"], self.path, check=True, timeout=240) def node(self) -> str | None: - unsafe_short_node = operator.itemgetter(slice(7)) - return run_git( ["rev-parse", "--verify", "--quiet", "HEAD"], self.path ).parse_success( - parse=unsafe_short_node, + parse=str, ) def count_all_nodes(self) -> int: @@ -182,6 +217,65 @@ def fail_on_shallow(wd: GitWorkdir) -> None: ) +def fail_on_missing_submodules(wd: GitWorkdir) -> None: + """ + Fail if submodules are defined but not initialized/cloned. + + This pre_parse function checks if there are submodules defined in .gitmodules + but not properly initialized (cloned). This helps prevent packaging incomplete + projects when submodules are required for a complete build. + """ + gitmodules_path = wd.path / ".gitmodules" + if not gitmodules_path.exists(): + # No submodules defined, nothing to check + return + + # Get submodule status - lines starting with '-' indicate uninitialized submodules + status_result = run_git(["submodule", "status"], wd.path) + if status_result.returncode != 0: + # Command failed, might not be in a git repo or other error + log.debug("Failed to check submodule status: %s", status_result.stderr) + return + + status_lines = ( + status_result.stdout.strip().split("\n") if status_result.stdout.strip() else [] + ) + uninitialized_submodules = [] + + for line in status_lines: + line = line.strip() + if line.startswith("-"): + # Extract submodule path (everything after the commit hash) + parts = line.split() + if len(parts) >= 2: + submodule_path = parts[1] + uninitialized_submodules.append(submodule_path) + + # If .gitmodules exists but git submodule status returns nothing, + # it means submodules are defined but not properly set up (common after cloning without --recurse-submodules) + if not status_lines and gitmodules_path.exists(): + raise ValueError( + f"Submodules are defined in .gitmodules but not initialized in {wd.path}. " + f"Please run 'git submodule update --init --recursive' to initialize them." + ) + + if uninitialized_submodules: + submodule_list = ", ".join(uninitialized_submodules) + raise ValueError( + f"Submodules are not initialized in {wd.path}: {submodule_list}. " + f"Please run 'git submodule update --init --recursive' to initialize them." + ) + + +# Mapping from enum items to actual pre_parse functions +_GIT_PRE_PARSE_FUNCTIONS: dict[GitPreParse, Callable[[GitWorkdir], None]] = { + GitPreParse.WARN_ON_SHALLOW: warn_on_shallow, + GitPreParse.FAIL_ON_SHALLOW: fail_on_shallow, + GitPreParse.FETCH_ON_SHALLOW: fetch_on_shallow, + GitPreParse.FAIL_ON_MISSING_SUBMODULES: fail_on_missing_submodules, +} + + def get_working_directory(config: Configuration, root: _t.PathT) -> GitWorkdir | None: """ Return the working directory (``GitWorkdir``). @@ -204,16 +298,26 @@ def parse( root: _t.PathT, config: Configuration, describe_command: str | list[str] | None = None, - pre_parse: Callable[[GitWorkdir], None] = warn_on_shallow, + pre_parse: Callable[[GitWorkdir], None] | None = None, ) -> ScmVersion | None: """ - :param pre_parse: experimental pre_parse action, may change at any time + :param pre_parse: experimental pre_parse action, may change at any time. + Takes precedence over config.git_pre_parse if provided. """ _require_command("git") wd = get_working_directory(config, root) if wd: + # Use function parameter first, then config setting, then default + if pre_parse is not None: + effective_pre_parse = pre_parse + else: + # config.scm.git.pre_parse is always a GitPreParse enum instance + effective_pre_parse = _GIT_PRE_PARSE_FUNCTIONS.get( + config.scm.git.pre_parse, warn_on_shallow + ) + return _git_parse_inner( - config, wd, describe_command=describe_command, pre_parse=pre_parse + config, wd, describe_command=describe_command, pre_parse=effective_pre_parse ) else: return None @@ -224,8 +328,8 @@ def version_from_describe( config: Configuration, describe_command: _t.CMD_TYPE | None, ) -> ScmVersion | None: - if config.git_describe_command is not None: - describe_command = config.git_describe_command + if config.scm.git.describe_command is not None: + describe_command = config.scm.git.describe_command if describe_command is not None: if isinstance(describe_command, str): @@ -258,7 +362,7 @@ def _git_parse_inner( if version is None: # If 'git git_describe_command' failed, try to get the information otherwise. - tag = config.version_cls("0.0") + tag = config.version_cls(config.fallback_version or "0.0") node = wd.node() if node is None: distance = 0 @@ -271,7 +375,20 @@ def _git_parse_inner( tag=tag, distance=distance, dirty=dirty, node=node, config=config ) branch = wd.get_branch() - node_date = wd.get_head_date() or datetime.now(timezone.utc).date() + node_date = wd.get_head_date() + + # If we can't get node_date from HEAD (e.g., no commits yet), + # and the working directory is dirty, try to use the latest + # modification time of changed files instead of current time + if node_date is None and wd.is_dirty(): + dirty_date = wd.get_dirty_tag_date() + if dirty_date is not None: + node_date = dirty_date + + # Final fallback to current time + if node_date is None: + node_date = datetime.now(timezone.utc).date() + return dataclasses.replace(version, branch=branch, node_date=node_date) diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py index d8307c78..42320516 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -6,11 +6,13 @@ from pathlib import Path from typing import TYPE_CHECKING +from typing import Any from . import Configuration from ._version_cls import Version from .integration import data_from_mime from .scm_workdir import Workdir +from .scm_workdir import get_latest_file_mtime from .version import ScmVersion from .version import meta from .version import tag_to_version @@ -18,62 +20,140 @@ if TYPE_CHECKING: from . import _types as _t +from ._run_cmd import CompletedProcess from ._run_cmd import require_command as _require_command from ._run_cmd import run as _run log = logging.getLogger(__name__) +def _get_hg_command() -> str: + """Get the hg command from environment, allowing runtime configuration.""" + return os.environ.get("SETUPTOOLS_SCM_HG_COMMAND", "hg") + + +def run_hg(args: list[str], cwd: _t.PathT, **kwargs: Any) -> CompletedProcess: + """Run mercurial command with the configured hg executable.""" + cmd = [_get_hg_command(), *args] + return _run(cmd, cwd=cwd, **kwargs) + + class HgWorkdir(Workdir): @classmethod def from_potential_worktree(cls, wd: _t.PathT) -> HgWorkdir | None: - res = _run(["hg", "root"], wd) + res = run_hg(["root"], wd) if res.returncode: return None return cls(Path(res.stdout)) def get_meta(self, config: Configuration) -> ScmVersion | None: - node: str - tags_str: str - node_date_str: str - node, tags_str, node_date_str = self.hg_log( - ".", "{node}\n{tag}\n{date|shortdate}" - ).split("\n") - # TODO: support bookmarks and topics (but nowadays bookmarks are # mainly used to emulate Git branches, which is already supported with # the dedicated class GitWorkdirHgClient) - branch, dirty_str, dirty_date = _run( - ["hg", "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"], + node_info = self._get_node_info() + if node_info is None: + return None + + node, tags_str, node_date_str = node_info + branch_info = self._get_branch_info() + branch, dirty, dirty_date = branch_info + + # Determine the appropriate node date + node_date = self._get_node_date(dirty, node_date_str, dirty_date) + + # Handle initial/empty repository + if self._is_initial_node(node): + return self._create_initial_meta(config, dirty, branch, node_date) + + node = "h" + node + tags = self._parse_tags(tags_str) + + # Try to get version from current tags + tag_version = self._get_version_from_tags(tags, config) + if tag_version: + return meta(tag_version, dirty=dirty, branch=branch, config=config) + + # Fall back to distance-based versioning + return self._get_distance_based_version(config, dirty, branch, node, node_date) + + def _get_node_info(self) -> tuple[str, str, str] | None: + """Get node, tags, and date information from mercurial log.""" + try: + node, tags_str, node_date_str = self.hg_log( + ".", "{node}\n{tag}\n{date|shortdate}" + ).split("\n") + return node, tags_str, node_date_str + except ValueError: + log.exception("Failed to get node info") + return None + + def _get_branch_info(self) -> tuple[str, bool, str]: + """Get branch name, dirty status, and dirty date.""" + branch, dirty_str, dirty_date = run_hg( + ["id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"], cwd=self.path, check=True, ).stdout.split("\n") dirty = bool(int(dirty_str)) - node_date = datetime.date.fromisoformat(dirty_date if dirty else node_date_str) - - if node == "0" * len(node): - log.debug("initial node %s", self.path) - return meta( - Version("0.0"), - config=config, - dirty=dirty, - branch=branch, - node_date=node_date, - ) - - node = "h" + node[:7] + return branch, dirty, dirty_date + + def _get_node_date( + self, dirty: bool, node_date_str: str, dirty_date: str + ) -> datetime.date: + """Get the appropriate node date, preferring file modification times for dirty repos.""" + if dirty: + file_mod_date = self.get_dirty_tag_date() + if file_mod_date is not None: + return file_mod_date + # Fall back to hg id date for dirty repos + return datetime.date.fromisoformat(dirty_date) + else: + return datetime.date.fromisoformat(node_date_str) + + def _is_initial_node(self, node: str) -> bool: + """Check if this is an initial/empty repository node.""" + return node == "0" * len(node) + + def _create_initial_meta( + self, config: Configuration, dirty: bool, branch: str, node_date: datetime.date + ) -> ScmVersion: + """Create metadata for initial/empty repository.""" + log.debug("initial node %s", self.path) + return meta( + Version("0.0"), + config=config, + dirty=dirty, + branch=branch, + node_date=node_date, + ) + def _parse_tags(self, tags_str: str) -> list[str]: + """Parse and filter tags from mercurial output.""" tags = tags_str.split() if "tip" in tags: # tip is not a real tag tags.remove("tip") + return tags + def _get_version_from_tags( + self, tags: list[str], config: Configuration + ) -> Version | None: + """Try to get a version from the current tags.""" if tags: tag = tag_to_version(tags[0], config) - if tag: - return meta(tag, dirty=dirty, branch=branch, config=config) + return tag + return None + def _get_distance_based_version( + self, + config: Configuration, + dirty: bool, + branch: str, + node: str, + node_date: datetime.date, + ) -> ScmVersion | None: + """Get version based on distance from latest tag.""" try: tag_str = self.get_latest_normalizable_tag() if tag_str is None: @@ -85,8 +165,13 @@ def get_meta(self, config: Configuration) -> ScmVersion | None: tag = Version("0.0") dist += 1 else: - tag = tag_to_version(tag_str, config=config) - assert tag is not None + maybe_tag = tag_to_version(tag_str, config=config) + if maybe_tag is None: + # If tag conversion fails, treat as no tag found + tag = Version("0.0") + dist += 1 + else: + tag = maybe_tag if self.check_changes_since_tag(tag_str) or dirty: return meta( @@ -104,13 +189,12 @@ def get_meta(self, config: Configuration) -> ScmVersion | None: except ValueError: # unpacking failed, old hg log.exception("error") - - return None + return None def hg_log(self, revset: str, template: str) -> str: - cmd = ["hg", "log", "-r", revset, "-T", template] - - return _run(cmd, cwd=self.path, check=True).stdout + return run_hg( + ["log", "-r", revset, "-T", template], cwd=self.path, check=True + ).stdout def get_latest_normalizable_tag(self) -> str | None: # Gets all tags containing a '.' (see #229) from oldest to newest @@ -142,11 +226,43 @@ def check_changes_since_tag(self, tag: str | None) -> bool: return bool(self.hg_log(revset, ".")) + def get_dirty_tag_date(self) -> datetime.date | None: + """Get the latest modification time of changed files in the working directory. + + Returns the date of the most recently modified file that has changes, + or None if no files are changed or if an error occurs. + """ + try: + # Check if working directory is dirty first + res = run_hg(["id", "-T", "{dirty}"], cwd=self.path) + if res.returncode != 0 or not bool(res.stdout): + return None + + # Get list of changed files using hg status + status_res = run_hg(["status", "-m", "-a", "-r"], cwd=self.path) + if status_res.returncode != 0: + return None + + changed_files = [] + for line in status_res.stdout.strip().split("\n"): + if line and len(line) > 2: + # Format is "M filename" or "A filename" etc. + filepath = line[2:] # Skip status char and space + changed_files.append(filepath) + + return get_latest_file_mtime(changed_files, self.path) + + except Exception as e: + log.debug("Failed to get dirty tag date: %s", e) + + return None + def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None: - _require_command("hg") + hg_cmd = _get_hg_command() + _require_command(hg_cmd) if os.path.exists(os.path.join(root, ".hg/git")): - res = _run(["hg", "path"], root) + res = run_hg(["path"], root) if not res.returncode: for line in res.stdout.split("\n"): if line.startswith("default ="): @@ -169,7 +285,7 @@ def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None: def archival_to_version(data: dict[str, str], config: Configuration) -> ScmVersion: log.debug("data %s", data) - node = data.get("node", "")[:12] + node = data.get("node", "") if node: node = "h" + node if "tag" in data: diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py index 9cab6f45..3e91b20f 100644 --- a/src/setuptools_scm/hg_git.py +++ b/src/setuptools_scm/hg_git.py @@ -9,10 +9,10 @@ from . import _types as _t from ._run_cmd import CompletedProcess as _CompletedProcess -from ._run_cmd import require_command -from ._run_cmd import run as _run from .git import GitWorkdir from .hg import HgWorkdir +from .hg import run_hg +from .scm_workdir import get_latest_file_mtime log = logging.getLogger(__name__) @@ -24,31 +24,57 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir): - COMMAND = "hg" - @classmethod def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdirHgClient | None: - require_command("hg") - res = _run(["hg", "root"], cwd=wd).parse_success(parse=Path) + res = run_hg(["root"], cwd=wd).parse_success(parse=Path) if res is None: return None return cls(res) def is_dirty(self) -> bool: - res = _run(["hg", "id", "-T", "{dirty}"], cwd=self.path, check=True) + res = run_hg(["id", "-T", "{dirty}"], cwd=self.path, check=True) return bool(res.stdout) def get_branch(self) -> str | None: - res = _run(["hg", "id", "-T", "{bookmarks}"], cwd=self.path) + res = run_hg(["id", "-T", "{bookmarks}"], cwd=self.path) if res.returncode: log.info("branch err %s", res) return None return res.stdout def get_head_date(self) -> date | None: - return _run('hg log -r . -T "{shortdate(date)}"', cwd=self.path).parse_success( - parse=date.fromisoformat, error_msg="head date err" - ) + return run_hg( + ["log", "-r", ".", "-T", "{shortdate(date)}"], cwd=self.path + ).parse_success(parse=date.fromisoformat, error_msg="head date err") + + def get_dirty_tag_date(self) -> date | None: + """Get the latest modification time of changed files in the working directory. + + Returns the date of the most recently modified file that has changes, + or None if no files are changed or if an error occurs. + """ + if not self.is_dirty(): + return None + + try: + # Get list of changed files using hg status + status_res = run_hg(["status", "-m", "-a", "-r"], cwd=self.path) + if status_res.returncode != 0: + return None + + changed_files = [] + for line in status_res.stdout.strip().split("\n"): + if line and len(line) > 2: + # Format is "M filename" or "A filename" etc. + filepath = line[2:] # Skip status char and space + changed_files.append(filepath) + + return get_latest_file_mtime(changed_files, self.path) + + except Exception as e: + log.debug("Failed to get dirty tag date: %s", e) + + return None def is_shallow(self) -> bool: return False @@ -57,7 +83,7 @@ def fetch_shallow(self) -> None: pass def get_hg_node(self) -> str | None: - res = _run('hg log -r . -T "{node}"', cwd=self.path) + res = run_hg(["log", "-r", ".", "-T", "{node}"], cwd=self.path) if res.returncode: return None else: @@ -81,7 +107,7 @@ def node(self) -> str | None: if git_node is None: # trying again after hg -> git - _run(["hg", "gexport"], cwd=self.path) + run_hg(["gexport"], cwd=self.path) git_node = self._hg2git(hg_node) if git_node is None: @@ -93,10 +119,10 @@ def node(self) -> str | None: return hg_node - return git_node[:7] + return git_node def count_all_nodes(self) -> int: - res = _run(["hg", "log", "-r", "ancestors(.)", "-T", "."], cwd=self.path) + res = run_hg(["log", "-r", "ancestors(.)", "-T", "."], cwd=self.path) return len(res.stdout) def default_describe(self) -> _CompletedProcess: @@ -106,9 +132,8 @@ def default_describe(self) -> _CompletedProcess: `git describe --dirty --tags --long --match *[0-9]*` """ - res = _run( + res = run_hg( [ - "hg", "log", "-r", "(reverse(ancestors(.)) and tag(r're:v?[0-9].*'))", @@ -136,7 +161,7 @@ def default_describe(self) -> _CompletedProcess: logging.warning("tag not found hg=%s git=%s", hg_tags, git_tags) return _FAKE_GIT_DESCRIBE_ERROR - res = _run(["hg", "log", "-r", f"'{tag}'::.", "-T", "."], cwd=self.path) + res = run_hg(["log", "-r", f"'{tag}'::.", "-T", "."], cwd=self.path) if res.returncode: return _FAKE_GIT_DESCRIBE_ERROR distance = len(res.stdout) - 1 diff --git a/changelog.d/.keep b/src/setuptools_scm/py.typed similarity index 100% rename from changelog.d/.keep rename to src/setuptools_scm/py.typed diff --git a/src/setuptools_scm/scm_workdir.py b/src/setuptools_scm/scm_workdir.py index 9879549d..b3ca7aa8 100644 --- a/src/setuptools_scm/scm_workdir.py +++ b/src/setuptools_scm/scm_workdir.py @@ -1,11 +1,50 @@ from __future__ import annotations +import logging + from dataclasses import dataclass +from datetime import date +from datetime import datetime +from datetime import timezone from pathlib import Path from ._config import Configuration from .version import ScmVersion +log = logging.getLogger(__name__) + + +def get_latest_file_mtime(changed_files: list[str], base_path: Path) -> date | None: + """Get the latest modification time of the given files. + + Args: + changed_files: List of relative file paths + base_path: Base directory path to resolve relative paths + + Returns: + The date of the most recently modified file, or None if no valid files found + """ + if not changed_files or changed_files == [""]: + return None + + latest_mtime = 0.0 + for filepath in changed_files: + full_path = base_path / filepath + try: + file_stat = full_path.stat() + latest_mtime = max(latest_mtime, file_stat.st_mtime) + except OSError: + # File might not exist or be accessible, skip it + log.debug("Failed to get mtime for %s", full_path) + continue + + if latest_mtime > 0: + # Convert to UTC date + dt = datetime.fromtimestamp(latest_mtime, timezone.utc) + return dt.date() + + return None + @dataclass() class Workdir: diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index 29803fcd..77c26dc9 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -16,6 +16,7 @@ from . import _entrypoints from . import _modify_version +from ._node_utils import _format_node_for_output if TYPE_CHECKING: import sys @@ -62,15 +63,21 @@ def _parse_version_tag( log.debug( "key %s data %s, %s, %r", key, match.groupdict(), match.groups(), full ) - result = _TagDict( - version=match.group(key), - prefix=full[: match.start(key)], - suffix=full[match.end(key) :], - ) - log.debug("tag %r parsed to %r", tag, result) - assert result["version"] - return result + if version := match.group(key): + result = _TagDict( + version=version, + prefix=full[: match.start(key)], + suffix=full[match.end(key) :], + ) + + log.debug("tag %r parsed to %r", tag, result) + return result + + raise ValueError( + f'The tag_regex "{config.tag_regex.pattern}" matched tag "{tag}", ' + "however the matched group has no value." + ) else: log.debug("tag %r did not parse", tag) @@ -104,11 +111,31 @@ def tag_to_version( version_str = tag_dict["version"] log.debug("version pre parse %s", version_str) - if suffix := tag_dict.get("suffix", ""): - warnings.warn(f"tag {tag!r} will be stripped of its suffix {suffix!r}") + # Try to create version from base version first + try: + version: _VersionT = config.version_cls(version_str) + log.debug("version=%r", version) + except Exception: + warnings.warn( + f"tag {tag!r} will be stripped of its suffix {tag_dict.get('suffix', '')!r}" + ) + # Fall back to trying without any suffix + version = config.version_cls(version_str) + log.debug("version=%r", version) + return version - version: _VersionT = config.version_cls(version_str) - log.debug("version=%r", version) + # If base version is valid, check if we can preserve the suffix + if suffix := tag_dict.get("suffix", ""): + log.debug("tag %r includes local build data %r, preserving it", tag, suffix) + # Try creating version with suffix - if it fails, we'll use the base version + try: + version_with_suffix = config.version_cls(version_str + suffix) + log.debug("version with suffix=%r", version_with_suffix) + return version_with_suffix + except Exception: + warnings.warn(f"tag {tag!r} will be stripped of its suffix {suffix!r}") + # Return the base version without suffix + return version return version @@ -125,8 +152,8 @@ def _source_epoch_or_utc_now() -> datetime: class ScmVersion: """represents a parsed version from scm""" - tag: _v.Version | _v.NonNormalizedVersion | str - """the related tag or preformatted version string""" + tag: _v.Version | _v.NonNormalizedVersion + """the related tag or preformatted version""" config: _config.Configuration """the configuration used to parse the version""" distance: int = 0 @@ -152,6 +179,11 @@ def exact(self) -> bool: """returns true checked out exactly on a tag and no local changes apply""" return self.distance == 0 and not self.dirty + @property + def short_node(self) -> str | None: + """Return the node formatted for output.""" + return _format_node_for_output(self.node) + def __repr__(self) -> str: return ( f" str: time=self.time, tag=self.tag, distance=self.distance, - node=self.node, + node=_format_node_for_output(self.node), dirty=self.dirty, branch=self.branch, node_date=self.node_date, @@ -191,9 +223,16 @@ def format_next_version( def _parse_tag( tag: _VersionT | str, preformatted: bool, config: _config.Configuration -) -> _VersionT | str: +) -> _VersionT: if preformatted: - return tag + # For preformatted versions, tag should already be validated as a version object + # String validation is handled in meta function before calling this + if isinstance(tag, str): + # This should not happen with enhanced meta, but kept for safety + return _v.NonNormalizedVersion(tag) + else: + # Already a version object (including test mocks), return as-is + return tag elif not isinstance(tag, config.version_cls): version = tag_to_version(tag, config) assert version is not None @@ -212,11 +251,21 @@ def meta( branch: str | None = None, config: _config.Configuration, node_date: date | None = None, + time: datetime | None = None, ) -> ScmVersion: - parsed_version = _parse_tag(tag, preformatted, config) + parsed_version: _VersionT + # Enhanced string validation for preformatted versions + if preformatted and isinstance(tag, str): + # Validate PEP 440 compliance using NonNormalizedVersion + # Let validation errors bubble up to the caller + parsed_version = _v.NonNormalizedVersion(tag) + else: + # Use existing _parse_tag logic for non-preformatted or already validated inputs + parsed_version = _parse_tag(tag, preformatted, config) + log.info("version %s -> %s", tag, parsed_version) assert parsed_version is not None, f"Can't parse version {tag}" - return ScmVersion( + scm_version = ScmVersion( parsed_version, distance=distance, node=node, @@ -226,6 +275,9 @@ def meta( config=config, node_date=node_date, ) + if time is not None: + scm_version = dataclasses.replace(scm_version, time=time) + return scm_version def guess_next_version(tag_version: ScmVersion) -> str: @@ -365,7 +417,11 @@ def guess_next_date_ver( head_date = node_date or today # compute patch if match is None: - tag_date = today + # For legacy non-date tags, always use patch=0 (treat as "other day") + # Use yesterday to ensure tag_date != head_date + from datetime import timedelta + + tag_date = head_date - timedelta(days=1) else: tag_date = ( datetime.strptime(match.group("date"), date_fmt) @@ -373,11 +429,13 @@ def guess_next_date_ver( .date() ) if tag_date == head_date: - patch = "0" if match is None else (match.group("patch") or "0") - patch = int(patch) + 1 + assert match is not None + # Same day as existing date tag - increment patch + patch = int(match.group("patch") or "0") + 1 else: + # Different day or legacy non-date tag - use patch 0 if tag_date > head_date and match is not None: - # warn on future times + # warn on future times (only for actual date tags, not legacy) warnings.warn( f"your previous tag ({tag_date}) is ahead your node date ({head_date})" ) @@ -433,19 +491,93 @@ def postrelease_version(version: ScmVersion) -> str: return version.format_with("{tag}.post{distance}") +def _combine_version_with_local_parts( + main_version: str, *local_parts: str | None +) -> str: + """ + Combine a main version with multiple local parts into a valid PEP 440 version string. + Handles deduplication of local parts to avoid adding the same local data twice. + + Args: + main_version: The main version string (e.g., "1.2.0", "1.2.dev3") + *local_parts: Variable number of local version parts, can be None or empty + + Returns: + A valid PEP 440 version string + + Examples: + _combine_version_with_local_parts("1.2.0", "build.123", "d20090213") -> "1.2.0+build.123.d20090213" + _combine_version_with_local_parts("1.2.0", "build.123", None) -> "1.2.0+build.123" + _combine_version_with_local_parts("1.2.0+build.123", "d20090213") -> "1.2.0+build.123.d20090213" + _combine_version_with_local_parts("1.2.0+build.123", "build.123") -> "1.2.0+build.123" # no duplication + _combine_version_with_local_parts("1.2.0", None, None) -> "1.2.0" + """ + # Split main version into base and existing local parts + if "+" in main_version: + main_part, existing_local = main_version.split("+", 1) + all_local_parts = existing_local.split(".") + else: + main_part = main_version + all_local_parts = [] + + # Process each new local part + for part in local_parts: + if not part or not part.strip(): + continue + + # Strip any leading + and split into segments + clean_part = part.strip("+") + if not clean_part: + continue + + # Split multi-part local identifiers (e.g., "build.123" -> ["build", "123"]) + part_segments = clean_part.split(".") + + # Add each segment if not already present + for segment in part_segments: + if segment and segment not in all_local_parts: + all_local_parts.append(segment) + + # Return combined result + if all_local_parts: + return main_part + "+" + ".".join(all_local_parts) + else: + return main_part + + def format_version(version: ScmVersion) -> str: log.debug("scm version %s", version) log.debug("config %s", version.config) if version.preformatted: - assert isinstance(version.tag, str) - return version.tag + return str(version.tag) + + # Extract original tag's local data for later combination + original_local = "" + if hasattr(version.tag, "local") and version.tag.local is not None: + original_local = str(version.tag.local) + + # Create a patched ScmVersion with only the base version (no local data) for version schemes + from dataclasses import replace + + # Extract the base version (public part) from the tag using config's version_cls + base_version_str = str(version.tag.public) + base_tag = version.config.version_cls(base_version_str) + version_for_scheme = replace(version, tag=base_tag) + main_version = _entrypoints._call_version_scheme( - version, "setuptools_scm.version_scheme", version.config.version_scheme, None + version_for_scheme, + "setuptools_scm.version_scheme", + version.config.version_scheme, ) log.debug("version %s", main_version) assert main_version is not None + local_version = _entrypoints._call_version_scheme( version, "setuptools_scm.local_scheme", version.config.local_scheme, "+unknown" ) log.debug("local_version %s", local_version) - return main_version + local_version + + # Combine main version with original local data and new local scheme data + return _combine_version_with_local_parts( + str(main_version), original_local, local_version + ) diff --git a/testing/INTEGRATION_MIGRATION_PLAN.md b/testing/INTEGRATION_MIGRATION_PLAN.md new file mode 100644 index 00000000..432a05b1 --- /dev/null +++ b/testing/INTEGRATION_MIGRATION_PLAN.md @@ -0,0 +1,92 @@ +## Setuptools integration test migration plan + +Purpose: streamline/simplify integration codepaths and make tests faster and easier to write by preferring unit-level inference over setuptools-driven E2E where possible. + +Reference helper for unit tests: + +```python +from setuptools_scm._integration.pyproject_reading import PyProjectData +from setuptools_scm._integration.version_inference import infer_version_string + +version = infer_version_string( + dist_name="pkg", + pyproject_data=PyProjectData.for_testing(project_present=True, section_present=True, project_name="pkg"), + overrides={"fallback_version": "1.2.3"}, +) +``` + +### Completed +- [x] Introduced `infer_version_string` pure helper to compute versions without a `Distribution` or `setup.py`. + +### Migration candidates (replace E2E/Distribution-hook tests with unit inference) +- [ ] `testing/test_integration.py::test_pyproject_support` + - Proposed unit: `test_infer_fallback_version_from_pyproject` + - Notes: Use `PyProjectData.for_testing(..., section_present=True, project_present=True)` + overrides `{fallback_version: "12.34"}`. + +- [ ] `testing/test_integration.py::test_setuptools_version_keyword_ensures_regex` + - Proposed unit: `test_infer_tag_regex_from_overrides` + - Notes: Create repo/tag in `wd`, call `infer_version_string(..., overrides={"tag_regex": "(1.0)"})`. + +- [ ] `testing/test_basic_api.py::test_parentdir_prefix` + - Proposed unit: `test_infer_parentdir_prefix_version` + - Notes: Use directory name prefix and `{parentdir_prefix_version: "projectname-"}`. + +- [ ] `testing/test_basic_api.py::test_fallback` + - Proposed unit: `test_infer_fallback_version` + - Notes: `{fallback_version: "12.34"}`. + +- [ ] `testing/test_basic_api.py::test_empty_pretend_version` + - Proposed unit: `test_infer_with_empty_pretend_uses_fallback` + - Notes: Set `SETUPTOOLS_SCM_PRETEND_VERSION=""`, infer with fallback. + +- [ ] `testing/test_basic_api.py::test_empty_pretend_version_named` + - Proposed unit: `test_infer_with_empty_named_pretend_uses_fallback` + - Notes: Use named pretend env var and fallback. + +- [ ] `testing/test_regressions.py::test_use_scm_version_callable` + - Proposed unit: `test_infer_with_callable_version_scheme` + - Notes: Pass callable via `overrides={"version_scheme": callable}` to `infer_version_string`. + +- [ ] `testing/test_git.py::test_root_relative_to` + - Proposed unit: `test_configuration_absolute_root_resolution` + - Notes: Assert `Configuration.absolute_root` behavior or use `Configuration.from_data(..., root/relative_to)`; avoid `setup.py`. + +- [ ] `testing/test_git.py::test_root_search_parent_directories` + - Proposed unit: `test_configuration_search_parent_directories` + - Notes: Prefer `Configuration(search_parent_directories=True)` + direct `_get_version` or `infer_version_string`. + +### Tests to keep as integration/E2E +- `testing/test_integration.py::test_integration_function_call_order` + - Validates precedence/ordering between `infer_version` and `version_keyword` hooks on `Distribution`. + +- `testing/test_integration.py::test_distribution_provides_extras` + - Verifies installed distribution metadata (extras exposure). + +- `testing/test_integration.py::test_git_archival_plugin_ignored` + - Entry point filtering behavior. + +- `testing/test_git.py::test_git_version_unnormalized_setuptools` (parameterized) + - Asserts difference between file write (`write_to` non-normalized) vs setuptools-normalized dist metadata. Requires setuptools behavior; not reproducible by pure helper. + +- Maintain a minimal smoke test to ensure `setup.py --version` works end-to-end (one per major path). + +### Already covered by unit-level decision tests (no action) +- `testing/test_version_inference.py` suite + - Exercises `get_version_inference_config` across configuration matrices using `PyProjectData.for_testing`. + +### New unit tests to add (pure inference) +- [ ] `test_infer_local_scheme_no_local_version` + - Use `PyProjectData.for_testing(section_present=True, project_present=True, local_scheme="no-local-version")`. + +- [ ] `test_infer_with_env_pretend_version_and_metadata` + - Set pretend version + metadata env vars; assert combined result via `infer_version_string`. + +- [ ] `test_infer_respects_nested_scm_git_config` + - Provide nested TOML-equivalent via `overrides={"scm": {"git": {"pre_parse": "fail_on_missing_submodules"}}}`. + +### Notes and pitfalls +- Some behaviors are specific to setuptools (normalization of dist metadata vs written file contents) and should remain integration tests. +- Prefer `PyProjectData.for_testing(...)` to avoid file I/O in new unit tests. +- For tests that assert version-file writing, call `infer_version_string(..., force_write_version_files=True)` and set `write_to`/`version_file` in overrides. + + diff --git a/testing/conftest.py b/testing/conftest.py index 09b69c1a..de1d9900 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -2,6 +2,7 @@ import contextlib import os +import shutil import sys from pathlib import Path @@ -37,7 +38,15 @@ def pytest_report_header() -> list[str]: for pkg in VERSION_PKGS: pkg_version = version(pkg) path = __import__(pkg).__file__ - res.append(f"{pkg} version {pkg_version} from {path!r}") + if path and "site-packages" in path: + # Replace everything up to and including site-packages with site:: + parts = path.split("site-packages", 1) + if len(parts) > 1: + path = "site:." + parts[1] + elif path and str(Path.cwd()) in path: + # Replace current working directory with CWD:: + path = path.replace(str(Path.cwd()), "CWD:.") + res.append(f"{pkg} version {pkg_version} from {path}") return res @@ -86,6 +95,14 @@ def wd(tmp_path: Path) -> WorkDir: return WorkDir(target_wd) +@pytest.fixture(scope="session") +def hg_exe() -> str: + hg = shutil.which("hg") + if hg is None: + pytest.skip("hg executable not found") + return hg + + @pytest.fixture def repositories_hg_git(tmp_path: Path) -> tuple[WorkDir, WorkDir]: tmp_path = tmp_path.resolve() diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py index 76239841..7847b352 100644 --- a/testing/test_basic_api.py +++ b/testing/test_basic_api.py @@ -23,7 +23,7 @@ template = """\ __version__ = version = {version!r} __version_tuple__ = version_tuple = {version_tuple!r} -__sha__ = {scm_version.node!r} +__sha__ = {scm_version.short_node!r} """ @@ -55,7 +55,9 @@ def assert_root(monkeypatch: pytest.MonkeyPatch, expected_root: str) -> None: def assertion(config: Configuration) -> ScmVersion: assert config.absolute_root == expected_root - return ScmVersion("1.0", config=config) + from packaging.version import Version + + return ScmVersion(Version("1.0"), config=config) monkeypatch.setattr(setuptools_scm._get_version_impl, "parse_version", assertion) @@ -104,7 +106,7 @@ def test_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def test_empty_pretend_version(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") + # monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") monkeypatch.setenv("SETUPTOOLS_SCM_PRETEND_VERSION", "") p = tmp_path / "sub/package" p.mkdir(parents=True) @@ -184,17 +186,30 @@ def read(name: str) -> str: scm_version = meta("1.0", distance=42, config=c) dump_version(tmp_path, version, "first.py", scm_version=scm_version) lines = read("first.py").splitlines() - assert lines[-2:] == [ + assert lines[-4:] == [ "__version__ = version = '1.0.dev42'", "__version_tuple__ = version_tuple = (1, 0, 'dev42')", + "", + "__commit_id__ = commit_id = None", + ] + + version = "1.0.1" + scm_version = meta("1.0.1", node="g4ac9d2c", config=c) + dump_version(tmp_path, version, "second.py", scm_version=scm_version) + lines = read("second.py").splitlines() + assert lines[-4:] == [ + "__version__ = version = '1.0.1'", + "__version_tuple__ = version_tuple = (1, 0, 1)", + "", + "__commit_id__ = commit_id = 'g4ac9d2c'", ] version = "1.0.1+g4ac9d2c" scm_version = meta("1.0.1", node="g4ac9d2c", config=c) dump_version( - tmp_path, version, "second.py", scm_version=scm_version, template=template + tmp_path, version, "third.py", scm_version=scm_version, template=template ) - lines = read("second.py").splitlines() + lines = read("third.py").splitlines() assert "__version__ = version = '1.0.1+g4ac9d2c'" in lines assert "__version_tuple__ = version_tuple = (1, 0, 1, 'g4ac9d2c')" in lines assert "__sha__ = 'g4ac9d2c'" in lines @@ -204,9 +219,9 @@ def read(name: str) -> str: "1.2.3", node="gb366d8b", distance=18, node_date=date(2021, 4, 15), config=c ) dump_version( - tmp_path, version, "third.py", scm_version=scm_version, template=template + tmp_path, version, "fourth.py", scm_version=scm_version, template=template ) - lines = read("third.py").splitlines() + lines = read("fourth.py").splitlines() assert "__version__ = version = '1.2.3.dev18+gb366d8b.d20210415'" in lines assert ( "__version_tuple__ = version_tuple = (1, 2, 3, 'dev18', 'gb366d8b.d20210415')" @@ -216,7 +231,7 @@ def read(name: str) -> str: import ast - ast.parse(read("third.py")) + ast.parse(read("fourth.py")) def test_parse_plain_fails(recwarn: pytest.WarningsRecorder) -> None: @@ -237,6 +252,18 @@ def __init__(self, tag_str: str) -> None: def __repr__(self) -> str: return f"hello,{self.version}" + @property + def public(self) -> str: + """The public portion of the version (without local part).""" + return self.version.split("+")[0] + + @property + def local(self) -> str | None: + """The local version segment.""" + if "+" in self.version: + return self.version.split("+", 1)[1] + return None + # you can not use normalize=False and version_cls at the same time with pytest.raises( ValueError, diff --git a/testing/test_better_root_errors.py b/testing/test_better_root_errors.py new file mode 100644 index 00000000..0ba964cc --- /dev/null +++ b/testing/test_better_root_errors.py @@ -0,0 +1,202 @@ +""" +Tests for better error messages when relative_to is not set. + +This addresses the issue #279 where error messages should be more helpful +when setuptools-scm fails to detect a version but a repository exists +in a parent directory. +""" + +from __future__ import annotations + +import pytest + +from setuptools_scm import Configuration +from setuptools_scm import get_version +from setuptools_scm._get_version_impl import _find_scm_in_parents +from setuptools_scm._get_version_impl import _version_missing +from testing.wd_wrapper import WorkDir + + +def setup_git_repo(wd: WorkDir) -> WorkDir: + """Set up a git repository for testing.""" + wd("git init") + wd("git config user.email test@example.com") + wd('git config user.name "a test"') + wd.add_command = "git add ." + wd.commit_command = "git commit -m test-{reason}" + return wd + + +def setup_hg_repo(wd: WorkDir) -> WorkDir: + """Set up a mercurial repository for testing.""" + try: + wd("hg init") + wd.add_command = "hg add ." + wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' + return wd + except Exception: + pytest.skip("hg not available") + + +def test_find_scm_in_parents_finds_git(wd: WorkDir) -> None: + """Test that _find_scm_in_parents correctly finds git repositories in parent directories.""" + # Set up git repo in root + setup_git_repo(wd) + + # Create a subdirectory structure + subdir = wd.cwd / "subproject" / "nested" + subdir.mkdir(parents=True) + + # Test from the nested subdirectory + config = Configuration(root=str(subdir)) + found_scm = _find_scm_in_parents(config) + + assert found_scm == wd.cwd + + +def test_find_scm_in_parents_finds_hg(wd: WorkDir) -> None: + """Test that _find_scm_in_parents correctly finds mercurial repositories in parent directories.""" + # Set up hg repo in root + setup_hg_repo(wd) + + # Create a subdirectory structure + subdir = wd.cwd / "subproject" / "nested" + subdir.mkdir(parents=True) + + # Test from the nested subdirectory + config = Configuration(root=str(subdir)) + found_scm = _find_scm_in_parents(config) + + assert found_scm == wd.cwd + + +def test_find_scm_in_parents_returns_none(wd: WorkDir) -> None: + """Test that _find_scm_in_parents returns None when no SCM repository is found.""" + # Don't initialize any SCM, just create subdirectories + subdir = wd.cwd / "project" / "nested" + subdir.mkdir(parents=True) + + config = Configuration(root=str(subdir)) + found_scm = _find_scm_in_parents(config) + + assert found_scm is None + + +def test_version_missing_with_scm_in_parent(wd: WorkDir) -> None: + """Test that _version_missing provides helpful error message when SCM is found in parent.""" + # Set up git repo in root + setup_git_repo(wd) + + # Create a subdirectory structure + subdir = wd.cwd / "subproject" / "nested" + subdir.mkdir(parents=True) + + # Test error message when relative_to is not set + config = Configuration(root=str(subdir), relative_to=None) + + with pytest.raises(LookupError) as exc_info: + _version_missing(config) + + error_message = str(exc_info.value) + + # Check that the error message mentions the parent repository + assert f"repository was found in a parent directory: {wd.cwd}" in error_message + assert "relative_to" in error_message + assert "search_parent_directories = true" in error_message + assert "setuptools_scm.get_version(relative_to=__file__)" in error_message + + +def test_version_missing_no_scm_found(wd: WorkDir) -> None: + """Test that _version_missing provides standard error message when no SCM is found anywhere.""" + # Don't initialize any SCM, just create subdirectories + subdir = wd.cwd / "project" / "nested" + subdir.mkdir(parents=True) + + config = Configuration(root=str(subdir), relative_to=None) + + with pytest.raises(LookupError) as exc_info: + _version_missing(config) + + error_message = str(exc_info.value) + + # Check that it falls back to the standard error message + assert ( + "Make sure you're either building from a fully intact git repository" + in error_message + ) + assert "repository was found in a parent directory" not in error_message + + +def test_version_missing_with_relative_to_set(wd: WorkDir) -> None: + """Test that when relative_to is set, we don't search parents for error messages.""" + # Set up git repo in root + setup_git_repo(wd) + + # Create a subdirectory structure + subdir = wd.cwd / "subproject" / "nested" + subdir.mkdir(parents=True) + + # Create a dummy file to use as relative_to + dummy_file = subdir / "setup.py" + dummy_file.write_text("# dummy file", encoding="utf-8") + + # Test error message when relative_to IS set + config = Configuration(root=str(subdir), relative_to=str(dummy_file)) + + with pytest.raises(LookupError) as exc_info: + _version_missing(config) + + error_message = str(exc_info.value) + + # Should not mention parent directory when relative_to is set + assert "repository was found in a parent directory" not in error_message + assert ( + "Make sure you're either building from a fully intact git repository" + in error_message + ) + + +def test_search_parent_directories_works_as_suggested( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that the suggested search_parent_directories=True solution actually works.""" + # Set up git repo + setup_git_repo(wd) + wd.commit_testfile() # Make sure there's a commit for version detection + + # Create a subdirectory + subdir = wd.cwd / "subproject" + subdir.mkdir() + + # Change to the subdirectory + monkeypatch.chdir(subdir) + + # This should work with search_parent_directories=True + version = get_version(search_parent_directories=True) + assert version is not None + assert "0.1.dev" in version + + +def test_integration_better_error_from_nested_directory( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Integration test: get_version from nested directory should give helpful error.""" + # Set up git repo + setup_git_repo(wd) + + # Create a subdirectory + subdir = wd.cwd / "subproject" + subdir.mkdir() + + # Change to the subdirectory + monkeypatch.chdir(subdir) + + # Try to get version without any configuration + with pytest.raises(LookupError) as exc_info: + get_version() + + error_message = str(exc_info.value) + + # Should suggest helpful solutions + assert f"repository was found in a parent directory: {wd.cwd}" in error_message + assert "search_parent_directories = true" in error_message diff --git a/testing/test_cli.py b/testing/test_cli.py index 050fe031..ffdcebd2 100644 --- a/testing/test_cli.py +++ b/testing/test_cli.py @@ -80,3 +80,161 @@ def test_cli_force_version_files( assert version_file.exists() assert output[:5] in version_file.read_text("utf-8") + + +def test_cli_create_archival_file_stable( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test creating stable .git_archival.txt file.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + archival_file = wd.cwd / ".git_archival.txt" + assert not archival_file.exists() + + result = main(["create-archival-file", "--stable"]) + assert result == 0 + assert archival_file.exists() + + content = archival_file.read_text("utf-8") + expected_lines = [ + "node: $Format:%H$", + "node-date: $Format:%cI$", + "describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$", + ] + for line in expected_lines: + assert line in content + + # Stable version should not contain ref-names + assert "ref-names" not in content + + +def test_cli_create_archival_file_full( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test creating full .git_archival.txt file with branch information.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + archival_file = wd.cwd / ".git_archival.txt" + assert not archival_file.exists() + + result = main(["create-archival-file", "--full"]) + assert result == 0 + assert archival_file.exists() + + content = archival_file.read_text("utf-8") + expected_lines = [ + "node: $Format:%H$", + "node-date: $Format:%cI$", + "describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$", + "ref-names: $Format:%D$", + ] + for line in expected_lines: + assert line in content + + # Full version should contain warning comment + assert "WARNING" in content + assert "unstable" in content + + +def test_cli_create_archival_file_exists_no_force( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that existing .git_archival.txt file prevents creation without --force.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + archival_file = wd.cwd / ".git_archival.txt" + archival_file.write_text("existing content", encoding="utf-8") + + # Should fail without --force + result = main(["create-archival-file", "--stable"]) + assert result == 1 + + # Content should be unchanged + assert archival_file.read_text("utf-8") == "existing content" + + +def test_cli_create_archival_file_exists_with_force( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that --force overwrites existing .git_archival.txt file.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + archival_file = wd.cwd / ".git_archival.txt" + archival_file.write_text("existing content", encoding="utf-8") + + # Should succeed with --force + result = main(["create-archival-file", "--stable", "--force"]) + assert result == 0 + + # Content should be updated + content = archival_file.read_text("utf-8") + assert "existing content" not in content + assert "node: $Format:%H$" in content + + +def test_cli_create_archival_file_requires_stable_or_full( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that create-archival-file requires either --stable or --full.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + # Should fail without --stable or --full + with pytest.raises(SystemExit): + main(["create-archival-file"]) + + +def test_cli_create_archival_file_mutually_exclusive( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that --stable and --full are mutually exclusive.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + # Should fail with both --stable and --full + with pytest.raises(SystemExit): + main(["create-archival-file", "--stable", "--full"]) + + +def test_cli_create_archival_file_existing_gitattributes( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test behavior when .gitattributes already has export-subst configuration.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + # Create .gitattributes with export-subst configuration + gitattributes_file = wd.cwd / ".gitattributes" + gitattributes_file.write_text(".git_archival.txt export-subst\n", encoding="utf-8") + + result = main(["create-archival-file", "--stable"]) + assert result == 0 + + archival_file = wd.cwd / ".git_archival.txt" + assert archival_file.exists() + + +def test_cli_create_archival_file_no_gitattributes( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test behavior when .gitattributes doesn't exist or lacks export-subst.""" + wd.commit_testfile() + wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) + monkeypatch.chdir(wd.cwd) + + result = main(["create-archival-file", "--stable"]) + assert result == 0 + + archival_file = wd.cwd / ".git_archival.txt" + assert archival_file.exists() diff --git a/testing/test_compat.py b/testing/test_compat.py new file mode 100644 index 00000000..3cd52771 --- /dev/null +++ b/testing/test_compat.py @@ -0,0 +1,73 @@ +"""Test compatibility utilities.""" + +from __future__ import annotations + +import pytest + +from setuptools_scm._compat import normalize_path_for_assertion +from setuptools_scm._compat import strip_path_suffix + + +def test_normalize_path_for_assertion() -> None: + """Test path normalization for assertions.""" + # Unix-style paths should remain unchanged + assert normalize_path_for_assertion("/path/to/file") == "/path/to/file" + + # Windows-style paths should be normalized + assert normalize_path_for_assertion(r"C:\path\to\file") == "C:/path/to/file" + assert normalize_path_for_assertion(r"path\to\file") == "path/to/file" + + # Mixed paths should be normalized + assert normalize_path_for_assertion(r"C:\path/to\file") == "C:/path/to/file" + + # Already normalized paths should remain unchanged + assert normalize_path_for_assertion("path/to/file") == "path/to/file" + + +def test_strip_path_suffix_success() -> None: + """Test successful path suffix stripping.""" + # Unix-style paths + assert strip_path_suffix("/home/user/project", "project") == "/home/user/" + assert ( + strip_path_suffix("/home/user/project/subdir", "project/subdir") + == "/home/user/" + ) + + # Windows-style paths + assert ( + strip_path_suffix("C:\\Users\\user\\project", "project") == "C:\\Users\\user\\" + ) + assert ( + strip_path_suffix("C:\\Users\\user\\project\\subdir", "project/subdir") + == "C:\\Users\\user\\" + ) + + # Mixed paths should work due to normalization + assert ( + strip_path_suffix("C:\\Users\\user\\project", "project") == "C:\\Users\\user\\" + ) + assert strip_path_suffix("/home/user/project", "project") == "/home/user/" + + # Edge cases + assert strip_path_suffix("project", "project") == "" + assert strip_path_suffix("/project", "project") == "/" + + +def test_strip_path_suffix_failure() -> None: + """Test failed path suffix stripping.""" + with pytest.raises(AssertionError, match="Path assertion failed"): + strip_path_suffix("/home/user/project", "other") + + with pytest.raises(AssertionError, match="Custom error"): + strip_path_suffix("/home/user/project", "other", "Custom error") + + +def test_integration_example() -> None: + """Test the integration pattern used in the codebase.""" + # Simulate the pattern used in git.py and _file_finders/git.py + full_path = r"C:\\Users\\user\\project\\subdir" + suffix = "subdir" + + # Now this is a single operation + prefix = strip_path_suffix(full_path, suffix) + assert prefix == r"C:\\Users\\user\\project\\" diff --git a/testing/test_config.py b/testing/test_config.py index b456f700..d0f06bd6 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -98,3 +98,23 @@ def test_config_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> No assert pristine.root != overridden.root assert pristine.fallback_root != overridden.fallback_root + + +@pytest.mark.parametrize( + "tag_regex", + [ + r".*", + r"(.+)(.+)", + r"((.*))", + ], +) +def test_config_bad_regex(tag_regex: str) -> None: + with pytest.raises( + ValueError, + match=( + f"Expected tag_regex '{re.escape(tag_regex)}' to contain a single match" + " group or a group named 'version' to identify the version part of any" + " tag." + ), + ): + Configuration(tag_regex=re.compile(tag_regex)) diff --git a/testing/test_deprecation.py b/testing/test_deprecation.py new file mode 100644 index 00000000..fa6e5aaf --- /dev/null +++ b/testing/test_deprecation.py @@ -0,0 +1,24 @@ +"""Test deprecation warnings and their exact text.""" + +from pathlib import Path + +import pytest + +from setuptools_scm._integration.deprecation import warn_dynamic_version + + +def test_warn_dynamic_version_full_text() -> None: + """Test the complete warning text for warn_dynamic_version function.""" + test_path = Path("test_file.toml") + expected_warning = ( + f"{test_path}: at [test.section]\n" + "test_expression is forcing setuptools to override the version setuptools-scm did already set\n" + "When using setuptools-scm it's invalid to use setuptools dynamic version as well, please remove it.\n" + "Setuptools-scm is responsible for setting the version, forcing setuptools to override creates errors." + ) + + with pytest.warns(UserWarning) as warning_info: # noqa: PT030 + warn_dynamic_version(test_path, "test.section", "test_expression") + + assert len(warning_info) == 1 + assert str(warning_info[0].message) == expected_warning diff --git a/testing/test_file_finder.py b/testing/test_file_finder.py index 5902d8e5..7d31e7d1 100644 --- a/testing/test_file_finder.py +++ b/testing/test_file_finder.py @@ -245,3 +245,33 @@ def test_archive( os.link("data/datafile", datalink) assert set(find_files()) == _sep({archive_file, "data/datafile", "data/datalink"}) + + +@pytest.fixture +def hg_wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> WorkDir: + try: + wd("hg init") + except OSError: + pytest.skip("hg executable not found") + (wd.cwd / "file").touch() + wd("hg add file") + monkeypatch.chdir(wd.cwd) + return wd + + +def test_hg_gone(hg_wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("PATH", str(hg_wd.cwd / "not-existing")) + assert set(find_files()) == set() + + +def test_hg_command_from_env( + hg_wd: WorkDir, + monkeypatch: pytest.MonkeyPatch, + request: pytest.FixtureRequest, + hg_exe: str, +) -> None: + with monkeypatch.context() as m: + m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe) + m.setenv("PATH", str(hg_wd.cwd / "not-existing")) + # No module reloading needed - runtime configuration works immediately + assert set(find_files()) == {"file"} diff --git a/testing/test_functions.py b/testing/test_functions.py index d6c4e711..b6b8a59e 100644 --- a/testing/test_functions.py +++ b/testing/test_functions.py @@ -47,6 +47,18 @@ def test_next_tag(tag: str, expected: str) -> None: "distance-dirty": meta("1.1", distance=3, dirty=True, config=c), } +# Versions with build metadata in the tag +VERSIONS_WITH_BUILD_METADATA = { + "exact-build": meta("1.1+build.123", distance=0, dirty=False, config=c), + "dirty-build": meta("1.1+build.123", distance=0, dirty=True, config=c), + "distance-clean-build": meta("1.1+build.123", distance=3, dirty=False, config=c), + "distance-dirty-build": meta("1.1+build.123", distance=3, dirty=True, config=c), + "exact-ci": meta("2.0.0+ci.456", distance=0, dirty=False, config=c), + "dirty-ci": meta("2.0.0+ci.456", distance=0, dirty=True, config=c), + "distance-clean-ci": meta("2.0.0+ci.456", distance=2, dirty=False, config=c), + "distance-dirty-ci": meta("2.0.0+ci.456", distance=2, dirty=True, config=c), +} + @pytest.mark.parametrize( ("version", "version_scheme", "local_scheme", "expected"), @@ -77,6 +89,96 @@ def test_format_version( assert format_version(configured_version) == expected +@pytest.mark.parametrize( + ("version", "version_scheme", "local_scheme", "expected"), + [ + # Exact matches should preserve build metadata from tag + ("exact-build", "guess-next-dev", "node-and-date", "1.1+build.123"), + ("exact-build", "guess-next-dev", "no-local-version", "1.1+build.123"), + ("exact-ci", "guess-next-dev", "node-and-date", "2.0.0+ci.456"), + ("exact-ci", "guess-next-dev", "no-local-version", "2.0.0+ci.456"), + # Dirty exact matches - version scheme treats dirty as non-exact, build metadata preserved + ( + "dirty-build", + "guess-next-dev", + "node-and-date", + "1.2.dev0+build.123.d20090213", + ), + ("dirty-build", "guess-next-dev", "no-local-version", "1.2.dev0+build.123"), + ("dirty-ci", "guess-next-dev", "node-and-date", "2.0.1.dev0+ci.456.d20090213"), + # Distance cases - build metadata should be preserved and combined with SCM data + ( + "distance-clean-build", + "guess-next-dev", + "node-and-date", + "1.2.dev3+build.123", + ), + ( + "distance-clean-build", + "guess-next-dev", + "no-local-version", + "1.2.dev3+build.123", + ), + ("distance-clean-ci", "guess-next-dev", "node-and-date", "2.0.1.dev2+ci.456"), + # Distance + dirty cases - build metadata should be preserved and combined with SCM data + ( + "distance-dirty-build", + "guess-next-dev", + "node-and-date", + "1.2.dev3+build.123.d20090213", + ), + ( + "distance-dirty-ci", + "guess-next-dev", + "node-and-date", + "2.0.1.dev2+ci.456.d20090213", + ), + # Post-release scheme tests + ("exact-build", "post-release", "node-and-date", "1.1+build.123"), + ( + "dirty-build", + "post-release", + "node-and-date", + "1.1.post0+build.123.d20090213", + ), + ( + "distance-clean-build", + "post-release", + "node-and-date", + "1.1.post3+build.123", + ), + ( + "distance-dirty-build", + "post-release", + "node-and-date", + "1.1.post3+build.123.d20090213", + ), + ], +) +def test_format_version_with_build_metadata( + version: str, version_scheme: str, local_scheme: str, expected: str +) -> None: + """Test format_version with tags that contain build metadata.""" + from dataclasses import replace + + from packaging.version import Version + + scm_version = VERSIONS_WITH_BUILD_METADATA[version] + configured_version = replace( + scm_version, + config=replace( + scm_version.config, version_scheme=version_scheme, local_scheme=local_scheme + ), + ) + result = format_version(configured_version) + + # Validate result is a valid PEP 440 version + parsed = Version(result) + assert str(parsed) == result, f"Result should be valid PEP 440: {result}" + + assert result == expected, f"Expected {expected}, got {result}" + + def test_dump_version_doesnt_bail_on_value_error(tmp_path: Path) -> None: write_to = "VERSION" version = str(VERSIONS["exact"].tag) @@ -191,3 +293,61 @@ def test_has_command_logs_stderr(caplog: pytest.LogCaptureFixture) -> None: def test_tag_to_version(tag: str, expected_version: str) -> None: version = str(tag_to_version(tag, c)) assert version == expected_version + + +def test_write_version_to_path_deprecation_warning_none(tmp_path: Path) -> None: + """Test that write_version_to_path warns when scm_version=None is passed.""" + from setuptools_scm._integration.dump_version import write_version_to_path + + target_file = tmp_path / "version.py" + + # This should raise a deprecation warning when scm_version=None is explicitly passed + with pytest.warns( + DeprecationWarning, match="write_version_to_path called without scm_version" + ): + write_version_to_path( + target=target_file, + template=None, # Use default template + version="1.2.3", + scm_version=None, # Explicitly passing None should warn + ) + + # Verify the file was created and contains the expected content + assert target_file.exists() + content = target_file.read_text(encoding="utf-8") + + # Check that the version is correctly formatted + assert "__version__ = version = '1.2.3'" in content + assert "__version_tuple__ = version_tuple = (1, 2, 3)" in content + + # Check that commit_id is set to None when scm_version is None + assert "__commit_id__ = commit_id = None" in content + + +def test_write_version_to_path_deprecation_warning_missing(tmp_path: Path) -> None: + """Test that write_version_to_path warns when scm_version parameter is not provided.""" + from setuptools_scm._integration.dump_version import write_version_to_path + + target_file = tmp_path / "version.py" + + # This should raise a deprecation warning when scm_version is not provided + with pytest.warns( + DeprecationWarning, match="write_version_to_path called without scm_version" + ): + write_version_to_path( + target=target_file, + template=None, # Use default template + version="1.2.3", + # scm_version not provided - should warn + ) + + # Verify the file was created and contains the expected content + assert target_file.exists() + content = target_file.read_text(encoding="utf-8") + + # Check that the version is correctly formatted + assert "__version__ = version = '1.2.3'" in content + assert "__version_tuple__ = version_tuple = (1, 2, 3)" in content + + # Check that commit_id is set to None when scm_version is None + assert "__commit_id__ = commit_id = None" in content diff --git a/testing/test_git.py b/testing/test_git.py index 9186b1a6..31cac7a3 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -39,15 +39,22 @@ ) -@pytest.fixture(name="wd") -def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> WorkDir: - debug_mode.disable() - monkeypatch.delenv("HOME", raising=False) +def setup_git_wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch | None = None) -> WorkDir: + """Set up a WorkDir with git initialized and configured for testing.""" + if monkeypatch: + monkeypatch.delenv("HOME", raising=False) wd("git init") wd("git config user.email test@example.com") wd('git config user.name "a test"') wd.add_command = "git add ." wd.commit_command = "git commit -m test-{reason}" + return wd + + +@pytest.fixture(name="wd") +def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> WorkDir: + debug_mode.disable() + setup_git_wd(wd, monkeypatch) debug_mode.enable() return wd @@ -208,16 +215,26 @@ def test_version_from_git(wd: WorkDir) -> None: setup(use_scm_version={'normalize': False, 'write_to': 'VERSION.txt'}) """, "with_created_class": """ - from setuptools import setup +from setuptools import setup - class MyVersion: - def __init__(self, tag_str: str): - self.version = tag_str +class MyVersion: + def __init__(self, tag_str: str): + self.version = tag_str - def __repr__(self): - return self.version + def __repr__(self): + return self.version - setup(use_scm_version={'version_cls': MyVersion, 'write_to': 'VERSION.txt'}) + @property + def public(self): + return self.version.split('+')[0] + + @property + def local(self): + if '+' in self.version: + return self.version.split('+', 1)[1] + return None + +setup(use_scm_version={'version_cls': MyVersion, 'write_to': 'VERSION.txt'}) """, "with_named_import": """ from setuptools import setup @@ -428,7 +445,11 @@ def test_not_matching_tags(wd: WorkDir) -> None: wd.commit_testfile() assert wd.get_version( tag_regex=r"^apache-arrow-([\.0-9]+)$", - git_describe_command="git describe --dirty --tags --long --exclude *js* ", + scm={ + "git": { + "describe_command": "git describe --dirty --tags --long --exclude *js* " + } + }, ).startswith("0.11.2") @@ -515,6 +536,36 @@ def test_git_getdate_git_2_45_0_plus( assert git_wd.get_head_date() == date(2024, 4, 30) +def test_git_getdate_timezone_consistency( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that get_head_date returns consistent UTC dates regardless of local timezone. + + This test forces a git commit with a timestamp that represents a time + after midnight in a positive timezone offset but still the previous day in UTC. + This is the exact scenario that was causing test failures in issue #1145. + """ + # Create a timestamp that's problematic: + # - In Europe/Berlin (UTC+2): 2025-06-12 00:30:00 (June 12th) + # - In UTC: 2025-06-11 22:30:00 (June 11th) + problematic_timestamp = "2025-06-12T00:30:00+02:00" + + # Force git to use this specific timestamp for the commit + monkeypatch.setenv("GIT_AUTHOR_DATE", problematic_timestamp) + monkeypatch.setenv("GIT_COMMITTER_DATE", problematic_timestamp) + + wd.commit_testfile() + + git_wd = git.GitWorkdir(wd.cwd) + result_date = git_wd.get_head_date() + + # The correct behavior is to return the UTC date (2025-06-11) + # If the bug is present, it would return the timezone-local date (2025-06-12) + expected_utc_date = date(2025, 6, 11) + + assert result_date == expected_utc_date + + @pytest.fixture def signed_commit_wd(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> WorkDir: if not has_command("gpg", args=["--version"], warn=False): @@ -593,3 +644,220 @@ def test_git_archival_from_unfiltered() -> None: ): version = archival_to_version({"node": "$Format:%H$"}, config=config) assert version is None + + +def test_fail_on_missing_submodules_no_gitmodules(wd: WorkDir) -> None: + """Test that fail_on_missing_submodules does nothing when no .gitmodules exists.""" + wd.commit_testfile() + # Should not raise any exception + git.fail_on_missing_submodules(git.GitWorkdir(wd.cwd)) + + +def test_fail_on_missing_submodules_with_initialized_submodules(wd: WorkDir) -> None: + """Test that fail_on_missing_submodules passes when submodules are initialized.""" + # Create a submodule directory and .gitmodules file + submodule_dir = wd.cwd / "external" + submodule_dir.mkdir() + + # Initialize a git repo in the submodule directory + wd(["git", "-C", str(submodule_dir), "init"]) + wd(["git", "-C", str(submodule_dir), "config", "user.email", "test@example.com"]) + wd(["git", "-C", str(submodule_dir), "config", "user.name", "Test User"]) + + # Create a commit in the submodule + test_file = submodule_dir / "test.txt" + test_file.write_text("test content", encoding="utf-8") + wd(["git", "-C", str(submodule_dir), "add", "test.txt"]) + wd(["git", "-C", str(submodule_dir), "commit", "-m", "Initial commit"]) + + # Add it as a submodule to the main repo + wd(["git", "submodule", "add", str(submodule_dir), "external"]) + wd.commit_testfile() + + # Should not raise any exception since the submodule is initialized + git.fail_on_missing_submodules(git.GitWorkdir(wd.cwd)) + + +def test_fail_on_missing_submodules_with_uninitialized_submodules( + tmp_path: Path, +) -> None: + """Test that fail_on_missing_submodules fails when submodules are not initialized.""" + # Create a test repository with a .gitmodules file but no actual submodule + test_repo = tmp_path / "test_repo" + test_repo.mkdir() + test_wd = setup_git_wd(WorkDir(test_repo)) + + # Create a fake .gitmodules file (this simulates what happens after cloning without --recurse-submodules) + gitmodules_content = """[submodule "external"] + path = external + url = https://example.com/external.git +""" + test_wd.write(".gitmodules", gitmodules_content) + test_wd.add_and_commit("Add-submodule-reference") + + # Should raise ValueError for uninitialized submodules + with pytest.raises( + ValueError, match=r"Submodules are defined in \.gitmodules but not initialized" + ): + git.fail_on_missing_submodules(git.GitWorkdir(test_repo)) + + +def test_git_pre_parse_config_integration(wd: WorkDir) -> None: + """Test that git_pre_parse configuration is used by the parse function.""" + wd.commit_testfile() + + # Test with default (None) - should use warn_on_shallow + config = Configuration() + result = git.parse(str(wd.cwd), config) + assert result is not None + + # Test with explicit configuration + from setuptools_scm._config import GitConfiguration + from setuptools_scm._config import ScmConfiguration + + config_with_pre_parse = Configuration( + scm=ScmConfiguration( + git=GitConfiguration(pre_parse=git.GitPreParse.WARN_ON_SHALLOW) + ) + ) + result = git.parse(str(wd.cwd), config_with_pre_parse) + assert result is not None + + # Test with different pre_parse value + config_fail_shallow = Configuration( + scm=ScmConfiguration( + git=GitConfiguration(pre_parse=git.GitPreParse.FAIL_ON_MISSING_SUBMODULES) + ) + ) + result = git.parse(str(wd.cwd), config_fail_shallow) + assert result is not None + + +def test_nested_scm_git_config_from_toml(tmp_path: Path) -> None: + """Test that nested SCM git configuration is properly parsed from TOML.""" + # Create a test pyproject.toml with nested SCM configuration + pyproject_path = tmp_path / "pyproject.toml" + pyproject_content = """ +[tool.setuptools_scm.scm.git] +pre_parse = "fail_on_missing_submodules" +""" + pyproject_path.write_text(pyproject_content, encoding="utf-8") + + # Parse the configuration from file + config = Configuration.from_file(pyproject_path) + + # Verify the nested configuration was parsed correctly and converted to enum + assert config.scm.git.pre_parse == git.GitPreParse.FAIL_ON_MISSING_SUBMODULES + + +def test_nested_scm_git_config_from_data() -> None: + """Test that nested SCM git configuration parsing works correctly with from_data.""" + # Test configuration parsing directly without file I/O + config_data = {"scm": {"git": {"pre_parse": "fail_on_missing_submodules"}}} + + # Parse the configuration data + config = Configuration.from_data(relative_to=".", data=config_data) + + # Verify the nested configuration was parsed correctly and converted to enum + assert config.scm.git.pre_parse == git.GitPreParse.FAIL_ON_MISSING_SUBMODULES + + +def test_invalid_git_pre_parse_raises_error() -> None: + """Test that invalid git pre_parse values raise a helpful ValueError.""" + # Test configuration parsing directly without file I/O + invalid_config_data = {"scm": {"git": {"pre_parse": "invalid_function"}}} + + # Parse the configuration data - should raise ValueError + with pytest.raises( + ValueError, match="Invalid git pre_parse function 'invalid_function'" + ): + Configuration.from_data(relative_to=".", data=invalid_config_data) + + +def test_git_describe_command_backward_compatibility() -> None: + """Test backward compatibility for git_describe_command configuration.""" + # Test old configuration style still works with deprecation warning + old_config_data = { + "git_describe_command": "git describe --dirty --tags --long --exclude *js*" + } + + with pytest.warns(DeprecationWarning, match=r"git_describe_command.*deprecated"): + config = Configuration.from_data(relative_to=".", data=old_config_data) + + # Verify it was migrated to the new location + assert ( + config.scm.git.describe_command + == "git describe --dirty --tags --long --exclude *js*" + ) + + +def test_git_describe_command_from_data_conflict() -> None: + """Test that specifying both old and new configuration in from_data raises ValueError.""" + # Both old and new configuration specified - should raise ValueError + mixed_config_data = { + "git_describe_command": "old command", + "scm": {"git": {"describe_command": "new command"}}, + } + + # The Configuration constructor should handle the conflict detection + with pytest.warns(DeprecationWarning, match=r"git_describe_command.*deprecated"): + with pytest.raises( + ValueError, match=r"Cannot specify both.*git_describe_command" + ): + Configuration.from_data(relative_to=".", data=mixed_config_data) + + +def test_git_describe_command_init_argument_deprecation() -> None: + """Test that passing git_describe_command as init argument issues deprecation warning.""" + # Test init argument + with pytest.warns(DeprecationWarning, match=r"git_describe_command.*deprecated"): + config = Configuration(git_describe_command="test command") + + # Verify the value was migrated to the new location + assert config.scm.git.describe_command == "test command" + + +def test_git_describe_command_init_conflict() -> None: + """Test that specifying both old and new configuration raises ValueError.""" + from setuptools_scm._config import GitConfiguration + from setuptools_scm._config import ScmConfiguration + + # Both old init arg and new configuration specified - should raise ValueError + with pytest.warns(DeprecationWarning, match=r"git_describe_command.*deprecated"): + with pytest.raises( + ValueError, match=r"Cannot specify both.*git_describe_command" + ): + Configuration( + git_describe_command="old command", + scm=ScmConfiguration( + git=GitConfiguration(describe_command="new command") + ), + ) + + +def test_git_no_commits_uses_fallback_version(wd: WorkDir) -> None: + """Test that when git describe fails (no commits), fallback_version is used instead of 0.0.""" + # Reinitialize as empty repo to remove any existing commits + wd("rm -rf .git") + wd("git init") + wd("git config user.email test@example.com") + wd('git config user.name "a test"') + + # Test with fallback_version set - should use the fallback instead of "0.0" + config = Configuration(fallback_version="1.2.3") + version = git.parse(str(wd.cwd), config) + + # Should get a version starting with the fallback version + assert version is not None + assert str(version.tag) == "1.2.3" + assert version.distance == 0 + assert version.dirty is True # No commits means dirty + + # Test without fallback_version - should default to "0.0" + config_no_fallback = Configuration() + version_no_fallback = git.parse(str(wd.cwd), config_no_fallback) + + assert version_no_fallback is not None + assert str(version_no_fallback.tag) == "0.0" + assert version_no_fallback.distance == 0 + assert version_no_fallback.dirty is True diff --git a/testing/test_hg_git.py b/testing/test_hg_git.py index 9527cb02..1c9101a6 100644 --- a/testing/test_hg_git.py +++ b/testing/test_hg_git.py @@ -2,8 +2,11 @@ import pytest +from setuptools_scm import Configuration +from setuptools_scm._run_cmd import CommandNotFoundError from setuptools_scm._run_cmd import has_command from setuptools_scm._run_cmd import run +from setuptools_scm.hg import parse from testing.wd_wrapper import WorkDir @@ -81,3 +84,31 @@ def test_base(repositories_hg_git: tuple[WorkDir, WorkDir]) -> None: wd("hg pull -u") assert wd_git.get_version() == "17.33.0rc0" assert wd.get_version() == "17.33.0rc0" + + +def test_hg_gone( + repositories_hg_git: tuple[WorkDir, WorkDir], monkeypatch: pytest.MonkeyPatch +) -> None: + wd = repositories_hg_git[0] + monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) + config = Configuration() + wd.write("pyproject.toml", "[tool.setuptools_scm]") + with pytest.raises(CommandNotFoundError, match=r"hg"): + parse(wd.cwd, config=config) + + assert wd.get_version(fallback_version="1.0") == "1.0" + + +def test_hg_command_from_env( + repositories_hg_git: tuple[WorkDir, WorkDir], + monkeypatch: pytest.MonkeyPatch, + request: pytest.FixtureRequest, + hg_exe: str, +) -> None: + wd = repositories_hg_git[0] + with monkeypatch.context() as m: + m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe) + m.setenv("PATH", str(wd.cwd / "not-existing")) + # No module reloading needed - runtime configuration works immediately + wd.write("pyproject.toml", "[tool.setuptools_scm]") + assert wd.get_version().startswith("0.1.dev0+") diff --git a/testing/test_integration.py b/testing/test_integration.py index ba1cdb67..e85b5bba 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -1,16 +1,28 @@ from __future__ import annotations import importlib.metadata -import os +import logging +import re import subprocess import sys import textwrap from pathlib import Path +from typing import TYPE_CHECKING +from typing import Any import pytest -import setuptools_scm._integration.setuptools +from packaging.version import Version + +from setuptools_scm._integration import setuptools as setuptools_integration +from setuptools_scm._integration.pyproject_reading import PyProjectData +from setuptools_scm._integration.setup_cfg import SetuptoolsBasicData +from setuptools_scm._integration.setup_cfg import read_setup_cfg +from setuptools_scm._requirement_cls import extract_package_name + +if TYPE_CHECKING: + import setuptools from setuptools_scm import Configuration from setuptools_scm._integration.setuptools import _warn_on_old_setuptools @@ -34,8 +46,6 @@ def wd(wd: WorkDir) -> WorkDir: def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - if sys.version_info < (3, 11): - pytest.importorskip("tomli") monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") pkg = tmp_path / "package" pkg.mkdir() @@ -60,95 +70,6 @@ def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> N assert res.stdout == "12.34" -PYPROJECT_FILES = { - "setup.py": "[tool.setuptools_scm]", - "setup.cfg": "[tool.setuptools_scm]", - "pyproject tool.setuptools_scm": ( - "[tool.setuptools_scm]\ndist_name='setuptools_scm_example'" - ), - "pyproject.project": ( - "[project]\nname='setuptools_scm_example'\n" - "dynamic=['version']\n[tool.setuptools_scm]" - ), -} - -SETUP_PY_PLAIN = "__import__('setuptools').setup()" -SETUP_PY_WITH_NAME = "__import__('setuptools').setup(name='setuptools_scm_example')" - -SETUP_PY_FILES = { - "setup.py": SETUP_PY_WITH_NAME, - "setup.cfg": SETUP_PY_PLAIN, - "pyproject tool.setuptools_scm": SETUP_PY_PLAIN, - "pyproject.project": SETUP_PY_PLAIN, -} - -SETUP_CFG_FILES = { - "setup.py": "", - "setup.cfg": "[metadata]\nname=setuptools_scm_example", - "pyproject tool.setuptools_scm": "", - "pyproject.project": "", -} - -with_metadata_in = pytest.mark.parametrize( - "metadata_in", - ["setup.py", "setup.cfg", "pyproject tool.setuptools_scm", "pyproject.project"], -) - - -@with_metadata_in -def test_pyproject_support_with_git(wd: WorkDir, metadata_in: str) -> None: - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - wd.write("pyproject.toml", PYPROJECT_FILES[metadata_in]) - wd.write("setup.py", SETUP_PY_FILES[metadata_in]) - wd.write("setup.cfg", SETUP_CFG_FILES[metadata_in]) - res = wd([sys.executable, "setup.py", "--version"]) - assert res.endswith("0.1.dev0+d20090213") - - -@pytest.mark.parametrize("use_scm_version", ["True", "{}", "lambda: {}"]) -def test_pyproject_missing_setup_hook_works(wd: WorkDir, use_scm_version: str) -> None: - wd.write( - "setup.py", - f"""__import__('setuptools').setup( - name="example-scm-unique", - use_scm_version={use_scm_version}, - )""", - ) - wd.write( - "pyproject.toml", - textwrap.dedent( - """ - [build-system] - requires=["setuptools", "setuptools_scm"] - build-backend = "setuptools.build_meta" - [tool] - """ - ), - ) - - res = subprocess.run( - [sys.executable, "setup.py", "--version"], - cwd=wd.cwd, - check=True, - stdout=subprocess.PIPE, - encoding="utf-8", - ) - stripped = res.stdout.strip() - assert stripped.endswith("0.1.dev0+d20090213") - - res_build = subprocess.run( - [sys.executable, "-m", "build", "-nxw"], - env={k: v for k, v in os.environ.items() if k != "SETUPTOOLS_SCM_DEBUG"}, - cwd=wd.cwd, - ) - import pprint - - pprint.pprint(res_build) - wheel: Path = next(wd.cwd.joinpath("dist").iterdir()) - assert "0.1.dev0+d20090213" in str(wheel) - - def test_pretend_version(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: monkeypatch.setenv(PRETEND_KEY, "1.0.0") @@ -156,18 +77,6 @@ def test_pretend_version(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: assert wd.get_version(dist_name="ignored") == "1.0.0" -@with_metadata_in -def test_pretend_version_named_pyproject_integration( - monkeypatch: pytest.MonkeyPatch, wd: WorkDir, metadata_in: str -) -> None: - test_pyproject_support_with_git(wd, metadata_in) - monkeypatch.setenv( - PRETEND_KEY_NAMED.format(name="setuptools_scm_example".upper()), "3.2.1" - ) - res = wd([sys.executable, "setup.py", "--version"]) - assert res.endswith("3.2.1") - - def test_pretend_version_named(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0") monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test2".upper()), "2.0.0") @@ -183,14 +92,319 @@ def test_pretend_version_name_takes_precedence( assert wd.get_version(dist_name="test") == "1.0.0" -def test_pretend_version_accepts_bad_string( +def test_pretend_version_rejects_invalid_string( monkeypatch: pytest.MonkeyPatch, wd: WorkDir ) -> None: + """Test that invalid pretend versions raise errors and bubble up.""" monkeypatch.setenv(PRETEND_KEY, "dummy") - wd.write("setup.py", SETUP_PY_PLAIN) - assert wd.get_version(write_to="test.py") == "dummy" - pyver = wd([sys.executable, "setup.py", "--version"]) - assert pyver == "0.0.0" + + # With strict validation, invalid pretend versions should raise errors + with pytest.raises(Exception, match=r".*dummy.*"): + wd.get_version(write_to="test.py") + + +def test_pretend_metadata_with_version( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir +) -> None: + """Test pretend metadata overrides work with pretend version.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + monkeypatch.setenv(PRETEND_KEY, "1.2.3.dev4+g1337beef") + monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="g1337beef", distance=4}') + + version = wd.get_version() + assert version == "1.2.3.dev4+g1337beef" + + # Test version file template functionality + wd("mkdir -p src") + version_file_content = """ +version = '{version}' +major = {version_tuple[0]} +minor = {version_tuple[1]} +patch = {version_tuple[2]} +commit_hash = '{scm_version.short_node}' +num_commit = {scm_version.distance} +""" # noqa: RUF027 + # Use write_to with template to create version file + version = wd.get_version( + write_to="src/version.py", write_to_template=version_file_content + ) + + content = (wd.cwd / "src/version.py").read_text(encoding="utf-8") + assert "commit_hash = 'g1337beef'" in content + assert "num_commit = 4" in content + + +def test_pretend_metadata_named(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: + """Test pretend metadata with named package support.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY_NAMED + + monkeypatch.setenv( + PRETEND_KEY_NAMED.format(name="test".upper()), "1.2.3.dev5+gabcdef12" + ) + monkeypatch.setenv( + PRETEND_METADATA_KEY_NAMED.format(name="test".upper()), + '{node="gabcdef12", distance=5, dirty=true}', + ) + + version = wd.get_version(dist_name="test") + assert version == "1.2.3.dev5+gabcdef12" + + +def test_pretend_metadata_without_version_warns( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture +) -> None: + """Test that pretend metadata without any base version logs a warning.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + # Only set metadata, no version - but there will be a git repo so there will be a base version + # Let's create an empty git repo without commits to truly have no base version + monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="g1234567", distance=2}') + + with caplog.at_level(logging.WARNING): + version = wd.get_version() + assert version is not None + + # In this case, metadata was applied to a fallback version, so no warning about missing base + + +def test_pretend_metadata_with_scm_version( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that pretend metadata works with actual SCM-detected version.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + # Set up a git repo with a tag so we have a base version + wd("git init") + wd("git config user.name test") + wd("git config user.email test@example.com") + wd.write("file.txt", "content") + wd("git add file.txt") + wd("git commit -m 'initial'") + wd("git tag v1.0.0") + + # Now add metadata overrides + monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="gcustom123", distance=7}') + + # Test that the metadata gets applied to the actual SCM version + version = wd.get_version() + # The version becomes 1.0.1.dev7+gcustom123 due to version scheme and metadata overrides + assert "1.0.1.dev7+gcustom123" == version + + # Test version file to see if metadata was applied + wd("mkdir -p src") + version_file_content = """ +version = '{version}' +commit_hash = '{scm_version.short_node}' +num_commit = {scm_version.distance} +""" # noqa: RUF027 + version = wd.get_version( + write_to="src/version.py", write_to_template=version_file_content + ) + + content = (wd.cwd / "src/version.py").read_text(encoding="utf-8") + assert "commit_hash = 'gcustom123'" in content + assert "num_commit = 7" in content + + +def test_pretend_metadata_type_conversion( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir +) -> None: + """Test that pretend metadata properly uses TOML native types.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + monkeypatch.setenv(PRETEND_KEY, "2.0.0") + monkeypatch.setenv( + PRETEND_METADATA_KEY, + '{distance=10, dirty=true, node="gfedcba98", branch="feature-branch"}', + ) + + version = wd.get_version() + # The version should be formatted properly with the metadata + assert "2.0.0" in version + + +def test_pretend_metadata_invalid_fields_filtered( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture +) -> None: + """Test that invalid metadata fields are filtered out with a warning.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + monkeypatch.setenv(PRETEND_KEY, "1.0.0") + monkeypatch.setenv( + PRETEND_METADATA_KEY, + '{node="g123456", distance=3, invalid_field="should_be_ignored", another_bad_field=42}', + ) + + with caplog.at_level(logging.WARNING): + version = wd.get_version() + assert version == "1.0.0" + + assert "Invalid metadata fields in pretend metadata" in caplog.text + assert "invalid_field" in caplog.text + assert "another_bad_field" in caplog.text + + +def test_pretend_metadata_date_parsing( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir +) -> None: + """Test that TOML date values work in pretend metadata.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + monkeypatch.setenv(PRETEND_KEY, "1.5.0") + monkeypatch.setenv( + PRETEND_METADATA_KEY, '{node="g987654", distance=7, node_date=2024-01-15}' + ) + + version = wd.get_version() + assert version == "1.5.0" + + +def test_pretend_metadata_invalid_toml_error( + monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture +) -> None: + """Test that invalid TOML in pretend metadata logs an error.""" + from setuptools_scm._overrides import PRETEND_METADATA_KEY + + monkeypatch.setenv(PRETEND_KEY, "1.0.0") + monkeypatch.setenv(PRETEND_METADATA_KEY, "{invalid toml syntax here}") + + with caplog.at_level(logging.ERROR): + version = wd.get_version() + # Should fall back to basic pretend version + assert version == "1.0.0" + + assert "Failed to parse pretend metadata" in caplog.text + + +def test_git_tag_with_local_build_data_preserved(wd: WorkDir) -> None: + """Test that git tags containing local build data are preserved in final version.""" + wd.commit_testfile() + + # Create a git tag that includes local build data + # This simulates a CI system that creates tags with build metadata + wd("git tag 1.0.0+build.123") + + # The version should preserve the build metadata from the tag + version = wd.get_version() + + # Validate it's a proper PEP 440 version + parsed_version = Version(version) + assert str(parsed_version) == version, ( + f"Version should parse correctly as PEP 440: {version}" + ) + + assert version == "1.0.0+build.123", ( + f"Expected build metadata preserved, got {version}" + ) + + # Validate the local part is correct + assert parsed_version.local == "build.123", ( + f"Expected local part 'build.123', got {parsed_version.local}" + ) + + +def test_git_tag_with_commit_hash_preserved(wd: WorkDir) -> None: + """Test that git tags with commit hash data are preserved.""" + wd.commit_testfile() + + # Create a git tag that includes commit hash metadata + wd("git tag 2.0.0+sha.abcd1234") + + # The version should preserve the commit hash from the tag + version = wd.get_version() + + # Validate it's a proper PEP 440 version + parsed_version = Version(version) + assert str(parsed_version) == version, ( + f"Version should parse correctly as PEP 440: {version}" + ) + + assert version == "2.0.0+sha.abcd1234" + + # Validate the local part is correct + assert parsed_version.local == "sha.abcd1234", ( + f"Expected local part 'sha.abcd1234', got {parsed_version.local}" + ) + + +def test_git_tag_with_local_build_data_preserved_dirty_workdir(wd: WorkDir) -> None: + """Test that git tags with local build data are preserved even with dirty working directory.""" + wd.commit_testfile() + + # Create a git tag that includes local build data + wd("git tag 1.5.0+build.456") + + # Make working directory dirty + wd.write("modified_file.txt", "some changes") + + # The version should preserve the build metadata from the tag + # even when working directory is dirty + version = wd.get_version() + + # Validate it's a proper PEP 440 version + parsed_version = Version(version) + assert str(parsed_version) == version, ( + f"Version should parse correctly as PEP 440: {version}" + ) + + assert version == "1.5.0+build.456", ( + f"Expected build metadata preserved with dirty workdir, got {version}" + ) + + # Validate the local part is correct + assert parsed_version.local == "build.456", ( + f"Expected local part 'build.456', got {parsed_version.local}" + ) + + +def test_git_tag_with_local_build_data_preserved_with_distance(wd: WorkDir) -> None: + """Test that git tags with local build data are preserved with distance.""" + wd.commit_testfile() + + # Create a git tag that includes local build data + wd("git tag 3.0.0+ci.789") + + # Add another commit after the tag to create distance + wd.commit_testfile("after-tag") + + # The version should use version scheme for distance but preserve original tag's build data + version = wd.get_version() + + # Validate it's a proper PEP 440 version + parsed_version = Version(version) + assert str(parsed_version) == version, ( + f"Version should parse correctly as PEP 440: {version}" + ) + + # Tag local data should be preserved and combined with SCM data + assert version.startswith("3.0.1.dev1"), ( + f"Expected dev version with distance, got {version}" + ) + + # Use regex to validate the version format with both tag build data and SCM node data + # Expected format: 3.0.1.dev1+ci.789.g + version_pattern = r"^3\.0\.1\.dev1\+ci\.789\.g[a-f0-9]+$" + assert re.match(version_pattern, version), ( + f"Version should match pattern {version_pattern}, got {version}" + ) + + # The original tag's local data (+ci.789) should be preserved and combined with SCM data + assert "+ci.789" in version, f"Tag local data should be preserved, got {version}" + + # Validate the local part contains both tag and SCM node information + assert parsed_version.local is not None, ( + f"Expected local version part, got {parsed_version.local}" + ) + assert "ci.789" in parsed_version.local, ( + f"Expected local part to contain tag data 'ci.789', got {parsed_version.local}" + ) + assert "g" in parsed_version.local, ( + f"Expected local part to contain SCM node data 'g...', got {parsed_version.local}" + ) + + # Note: This test verifies that local build data from tags is preserved and combined + # with SCM data when there's distance, which is the desired behavior for issue 1019. def testwarn_on_broken_setuptools() -> None: @@ -205,7 +419,7 @@ def test_distribution_provides_extras() -> None: dist = distribution("setuptools_scm") pe: list[str] = dist.metadata.get_all("Provides-Extra", []) - assert sorted(pe) == ["docs", "rich", "test", "toml"] + assert sorted(pe) == ["rich", "simple", "toml"] @pytest.mark.issue(760) @@ -221,9 +435,89 @@ def test_unicode_in_setup_cfg(tmp_path: Path) -> None: ), encoding="utf-8", ) - name = setuptools_scm._integration.setuptools.read_dist_name_from_setup_cfg(cfg) + from setuptools_scm._integration.setup_cfg import read_setup_cfg + + name = read_setup_cfg(cfg).name assert name == "configparser" + # also ensure we can parse a version if present (legacy projects) + cfg.write_text( + textwrap.dedent( + """ + [metadata] + name = configparser + version = 1.2.3 + """ + ), + encoding="utf-8", + ) + + data = read_setup_cfg(cfg) + assert isinstance(data, SetuptoolsBasicData) + assert data.name == "configparser" + assert data.version == "1.2.3" + + +@pytest.mark.issue(1216) +def test_setup_cfg_dynamic_version_warns_and_ignores(tmp_path: Path) -> None: + cfg = tmp_path / "setup.cfg" + cfg.write_text( + textwrap.dedent( + """ + [metadata] + name = example-broken + version = attr: example_broken.__version__ + """ + ), + encoding="utf-8", + ) + + with pytest.warns( + UserWarning, + match=r"setup\.cfg: at \[metadata\]", + ): + legacy_data = read_setup_cfg(cfg) + + assert legacy_data.version is None + + +def test_setup_cfg_version_prevents_inference_version_keyword( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + # Legacy project setup - we construct the data directly since files are not read anyway + monkeypatch.chdir(tmp_path) + + dist = create_clean_distribution("legacy-proj") + + # Using keyword should detect an existing version via legacy data and avoid inferring + from setuptools_scm._integration import setuptools as setuptools_integration + from setuptools_scm._integration.pyproject_reading import PyProjectData + from setuptools_scm._integration.setup_cfg import SetuptoolsBasicData + + # Construct PyProjectData directly without requiring build backend inference + pyproject_data = PyProjectData.for_testing( + is_required=False, # setuptools-scm not required + section_present=False, # no [tool.setuptools_scm] section + project_present=False, # no [project] section + ) + + # Construct legacy data with version from setup.cfg + legacy_data = SetuptoolsBasicData( + path=tmp_path / "setup.cfg", name="legacy-proj", version="0.9.0" + ) + + with pytest.warns(UserWarning, match="version of legacy-proj already set"): + setuptools_integration.version_keyword( + dist, + "use_scm_version", + True, + _given_pyproject_data=pyproject_data, + _given_legacy_data=legacy_data, + ) + + # setuptools_scm should not set a version when setup.cfg already provided one + assert dist.metadata.version is None + def test_setuptools_version_keyword_ensures_regex( wd: WorkDir, @@ -232,12 +526,12 @@ def test_setuptools_version_keyword_ensures_regex( wd.commit_testfile("test") wd("git tag 1.0") monkeypatch.chdir(wd.cwd) - import setuptools - from setuptools_scm._integration.setuptools import version_keyword - - dist = setuptools.Distribution({"name": "test"}) - version_keyword(dist, "use_scm_version", {"tag_regex": "(1.0)"}) + dist = create_clean_distribution("test") + setuptools_integration.version_keyword( + dist, "use_scm_version", {"tag_regex": "(1.0)"} + ) + assert dist.metadata.version == "1.0" @pytest.mark.parametrize( @@ -256,3 +550,166 @@ def test_git_archival_plugin_ignored(tmp_path: Path, ep_name: str) -> None: found = list(iter_matching_entrypoints(tmp_path, config=c, entrypoint=ep_name)) imports = [item.value for item in found] assert "setuptools_scm_git_archive:parse" not in imports + + +@pytest.mark.parametrize("base_name", ["setuptools_scm", "setuptools-scm"]) +@pytest.mark.parametrize( + "requirements", + ["", ">=8", "[toml]>=7", "~=9.0", "[rich,toml]>=8"], + ids=["empty", "version", "extras", "fuzzy", "multiple-extras"], +) +def test_extract_package_name(base_name: str, requirements: str) -> None: + """Test the _extract_package_name helper function""" + assert extract_package_name(f"{base_name}{requirements}") == "setuptools-scm" + + +# Helper function for creating and managing distribution objects +def create_clean_distribution(name: str) -> setuptools.Distribution: + """Create a clean distribution object without any setuptools_scm effects. + + This function creates a new setuptools Distribution and ensures it's completely + clean from any previous setuptools_scm version inference effects, including: + - Clearing any existing version + - Removing the _setuptools_scm_version_set_by_infer flag + """ + import setuptools + + dist = setuptools.Distribution({"name": name}) + + # Clean all setuptools_scm effects + dist.metadata.version = None + if hasattr(dist, "_setuptools_scm_version_set_by_infer"): + delattr(dist, "_setuptools_scm_version_set_by_infer") + + return dist + + +def version_keyword_default( + dist: setuptools.Distribution, pyproject_data: PyProjectData | None = None +) -> None: + """Helper to call version_keyword with default config and return the result.""" + + setuptools_integration.version_keyword( + dist, "use_scm_version", True, _given_pyproject_data=pyproject_data + ) + + +def version_keyword_calver( + dist: setuptools.Distribution, pyproject_data: PyProjectData | None = None +) -> None: + """Helper to call version_keyword with calver-by-date scheme and return the result.""" + + setuptools_integration.version_keyword( + dist, + "use_scm_version", + {"version_scheme": "calver-by-date"}, + _given_pyproject_data=pyproject_data, + ) + + +def infer_version_with_data( + dist: setuptools.Distribution, pyproject_data: PyProjectData | None = None +) -> None: + """Helper to call infer_version with pyproject data.""" + + setuptools_integration.infer_version(dist, _given_pyproject_data=pyproject_data) + + +@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/1022") +@pytest.mark.filterwarnings("ignore:version of .* already set:UserWarning") +@pytest.mark.filterwarnings( + "ignore:.* does not correspond to a valid versioning date.*:UserWarning" +) +@pytest.mark.parametrize( + ("first_integration", "second_integration", "expected_final_version"), + [ + # infer_version and version_keyword can be called in either order + (infer_version_with_data, version_keyword_default, "1.0.1.dev1"), + (infer_version_with_data, version_keyword_calver, "9.2.13.0.dev1"), + (version_keyword_default, infer_version_with_data, "1.0.1.dev1"), + (version_keyword_calver, infer_version_with_data, "9.2.13.0.dev1"), + ], +) +def test_integration_function_call_order( + wd: WorkDir, + monkeypatch: pytest.MonkeyPatch, + first_integration: Any, + second_integration: Any, + expected_final_version: str, +) -> None: + """Test that integration functions can be called in any order. + + version_keyword should always win when it specifies configuration, but currently doesn't. + Some tests will fail, showing the bug. + """ + # Set up controlled environment for deterministic versions + monkeypatch.setenv("SOURCE_DATE_EPOCH", "1234567890") # 2009-02-13T23:31:30+00:00 + # Override node_date to get consistent calver versions + monkeypatch.setenv( + "SETUPTOOLS_SCM_PRETEND_METADATA_FOR_TEST_CALL_ORDER", "{node_date=2009-02-13}" + ) + + # Set up a git repository with a tag and known commit hash + wd.commit_testfile("test") + wd("git tag 1.0.0") + wd.commit_testfile("test2") # Add another commit to get distance + monkeypatch.chdir(wd.cwd) + + # Create PyProjectData with equivalent configuration - no file I/O! + project_name = "test-call-order" + pyproject_data = PyProjectData.for_testing( + project_name=project_name, + has_dynamic_version=True, + project_present=True, + section_present=True, + local_scheme="no-local-version", + ) + + dist = create_clean_distribution(project_name) + + # Call both integration functions in order with direct data injection + first_integration(dist, pyproject_data) + second_integration(dist, pyproject_data) + + # Get the final version directly from the distribution + final_version = dist.metadata.version + + # Assert the final version matches expectation + # Some tests will fail here, demonstrating the bug where version_keyword doesn't override + assert final_version == expected_final_version, ( + f"Expected version '{expected_final_version}' but got '{final_version}'" + ) + + +@pytest.mark.issue("xmlsec-regression") +def test_xmlsec_download_regression( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that pip download works for xmlsec package without causing setuptools_scm regression. + + This test ensures that downloading and building xmlsec from source doesn't fail + due to setuptools_scm issues when using --no-build-isolation. + """ + # Set up environment with setuptools_scm debug enabled + monkeypatch.setenv("SETUPTOOLS_SCM_DEBUG", "1") + monkeypatch.setenv("COLUMNS", "150") + + # Run pip download command with no-binary and no-build-isolation + try: + subprocess.run( + [ + *(sys.executable, "-m", "pip", "download"), + *("--no-binary", "xmlsec"), + "--no-build-isolation", + "-v", + "xmlsec==1.3.16", + ], + cwd=tmp_path, + timeout=300, + check=True, + ) + except subprocess.CalledProcessError as e: + pytest.fail(f"pip download failed: {e}", pytrace=False) + + # The success of the subprocess.run call above means the regression is fixed. + # pip download succeeded without setuptools_scm causing version conflicts. diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index 57073716..3e15aae8 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -31,12 +31,12 @@ def wd(wd: WorkDir) -> WorkDir: archival_mapping = { "1.0": {"tag": "1.0"}, - "1.1.0.dev3+h000000000000": { + "1.1.0.dev3+h0000000000": { "latesttag": "1.0", "latesttagdistance": "3", "node": "0" * 20, }, - "1.0.1.dev3+h000000000000": { + "1.0.1.dev3+h0000000000": { "latesttag": "1.0.0", "latesttagdistance": "3", "branch": "1.0", @@ -67,6 +67,36 @@ def test_hg_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: assert wd.get_version(fallback_version="1.0") == "1.0" +def test_hg_command_from_env( + wd: WorkDir, + monkeypatch: pytest.MonkeyPatch, + request: pytest.FixtureRequest, + hg_exe: str, +) -> None: + wd.write("pyproject.toml", "[tool.setuptools_scm]") + # Need to commit something first for versioning to work + wd.commit_testfile() + + monkeypatch.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe) + monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) + version = wd.get_version() + assert version.startswith("0.1.dev1+") + + +def test_hg_command_from_env_is_invalid( + wd: WorkDir, monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest +) -> None: + with monkeypatch.context() as m: + m.setenv("SETUPTOOLS_SCM_HG_COMMAND", str(wd.cwd / "not-existing")) + # No module reloading needed - runtime configuration works immediately + config = Configuration() + wd.write("pyproject.toml", "[tool.setuptools_scm]") + with pytest.raises(CommandNotFoundError, match=r"test.*hg.*not-existing"): + parse(wd.cwd, config=config) + + assert wd.get_version(fallback_version="1.0") == "1.0" + + def test_find_files_stop_at_root_hg( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -133,7 +163,7 @@ def test_version_from_archival(wd: WorkDir) -> None: """, ) - assert wd.get_version() == "0.2.dev3+h000000000000" + assert wd.get_version() == "0.2.dev3+h0000000000" @pytest.mark.issue("#72") diff --git a/testing/test_overrides.py b/testing/test_overrides.py new file mode 100644 index 00000000..afba5339 --- /dev/null +++ b/testing/test_overrides.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +import logging + +import pytest + +from setuptools_scm._overrides import _find_close_env_var_matches +from setuptools_scm._overrides import _search_env_vars_with_prefix +from setuptools_scm._overrides import read_named_env + + +class TestSearchEnvVarsWithPrefix: + """Test the _search_env_vars_with_prefix helper function.""" + + def test_exact_match(self) -> None: + """Test finding exact normalized matches.""" + env = {"SETUPTOOLS_SCM_TEST_FOR_MY_PACKAGE": "value1"} + + matches = _search_env_vars_with_prefix( + "SETUPTOOLS_SCM_TEST_FOR_", "my-package", env + ) + + assert len(matches) == 1 + assert matches[0] == ("SETUPTOOLS_SCM_TEST_FOR_MY_PACKAGE", "value1") + + def test_multiple_normalizations(self) -> None: + """Test finding various normalization patterns.""" + # Set up different normalization patterns + env = { + "SETUPTOOLS_SCM_TEST_FOR_MY_AWESOME_PKG": "value1", + "SETUPTOOLS_SCM_TEST_FOR_MYAWESOMEPKG": "value2", + "SETUPTOOLS_SCM_TEST_FOR_MY_AWESOME-PKG": "value3", # duplicate + } + + matches = _search_env_vars_with_prefix( + "SETUPTOOLS_SCM_TEST_FOR_", "my-awesome.pkg", env + ) + + # Should find the variants that match our normalization patterns + assert len(matches) >= 1 + env_vars = [var for var, _ in matches] + assert "SETUPTOOLS_SCM_TEST_FOR_MY_AWESOME_PKG" in env_vars + + def test_no_matches(self) -> None: + """Test when no matches are found.""" + # Set up unrelated env vars + env = { + "OTHER_VAR": "value", + "SETUPTOOLS_SCM_OTHER_FOR_SOMETHING": "value", + } + + matches = _search_env_vars_with_prefix( + "SETUPTOOLS_SCM_TEST_FOR_", "nonexistent", env + ) + + assert len(matches) == 0 + + def test_case_variations(self) -> None: + """Test that case variations are handled.""" + env = {"SETUPTOOLS_SCM_TEST_FOR_MYPACKAGE": "value1"} + + matches = _search_env_vars_with_prefix( + "SETUPTOOLS_SCM_TEST_FOR_", "MyPackage", env + ) + + assert len(matches) == 1 + assert matches[0][1] == "value1" + + +class TestFindCloseEnvVarMatches: + """Test the _find_close_env_var_matches helper function.""" + + def test_close_matches(self) -> None: + """Test finding close matches for potential typos.""" + env = { + "SETUPTOOLS_SCM_TEST_FOR_MY_PACKAG": "typo1", # missing 'e' + "SETUPTOOLS_SCM_TEST_FOR_MY_PAKAGE": "typo2", # 'c' -> 'k' + "SETUPTOOLS_SCM_TEST_FOR_OTHER_PKG": "unrelated", + } + + close_matches = _find_close_env_var_matches( + "SETUPTOOLS_SCM_TEST_FOR_", "MY_PACKAGE", env + ) + + # Should find the close matches but not the unrelated one + assert "SETUPTOOLS_SCM_TEST_FOR_MY_PACKAG" in close_matches + assert "SETUPTOOLS_SCM_TEST_FOR_MY_PAKAGE" in close_matches + assert "SETUPTOOLS_SCM_TEST_FOR_OTHER_PKG" not in close_matches + + def test_threshold(self) -> None: + """Test that threshold filtering works.""" + env = {"SETUPTOOLS_SCM_TEST_FOR_COMPLETELY_DIFFERENT": "unrelated"} + + close_matches = _find_close_env_var_matches( + "SETUPTOOLS_SCM_TEST_FOR_", "MY_PACKAGE", env, threshold=0.8 + ) + + # With high threshold, completely different string shouldn't match + assert len(close_matches) == 0 + + def test_no_close_matches(self) -> None: + """Test when no close matches exist.""" + env: dict[str, str] = {} + close_matches = _find_close_env_var_matches( + "SETUPTOOLS_SCM_TEST_FOR_", "MY_PACKAGE", env + ) + + assert len(close_matches) == 0 + + +class TestReadNamedEnvEnhanced: + """Test the enhanced read_named_env function.""" + + def test_standard_behavior_unchanged(self) -> None: + """Test that standard behavior still works.""" + # Generic env var + env = {"SETUPTOOLS_SCM_TEST": "generic_value"} + assert read_named_env(name="TEST", dist_name=None, env=env) == "generic_value" + + # Dist-specific env var (standard normalization) + env = {"SETUPTOOLS_SCM_TEST_FOR_MY_PACKAGE": "specific_value"} + assert ( + read_named_env(name="TEST", dist_name="my-package", env=env) + == "specific_value" + ) + + def test_alternative_normalization_found( + self, caplog: pytest.LogCaptureFixture + ) -> None: + """Test finding alternative normalizations with warnings.""" + # Set up an alternative normalization pattern (user uses dots instead of canonical hyphens) + env = {"SETUPTOOLS_SCM_TEST_FOR_MY.PACKAGE": "alt_value"} + + with caplog.at_level(logging.WARNING): + result = read_named_env(name="TEST", dist_name="my.package", env=env) + + assert result == "alt_value" + assert "Found environment variable" in caplog.text + assert "but expected" in caplog.text + + def test_multiple_alternatives_warning( + self, caplog: pytest.LogCaptureFixture + ) -> None: + """Test warning when multiple alternative normalizations exist.""" + # Set up multiple alternatives that represent the same canonical package name + # but use different normalizations in the env var + env = { + "SETUPTOOLS_SCM_TEST_FOR_MY.PACKAGE": "alt1", # dots instead of hyphens + "SETUPTOOLS_SCM_TEST_FOR_MY-PACKAGE": "alt2", # dashes instead of underscores + "SETUPTOOLS_SCM_TEST_FOR_my.package": "alt3", # lowercase + } + + with caplog.at_level(logging.WARNING): + result = read_named_env(name="TEST", dist_name="my.package", env=env) + + assert result in ["alt1", "alt2", "alt3"] # Should use one of them + assert "Multiple alternative environment variables found" in caplog.text + + def test_typo_suggestions(self, caplog: pytest.LogCaptureFixture) -> None: + """Test suggestions for potential typos.""" + # Set up a close but not exact match + env = {"SETUPTOOLS_SCM_TEST_FOR_MY_PACKAG": "typo_value"} + + with caplog.at_level(logging.WARNING): + result = read_named_env(name="TEST", dist_name="my-package", env=env) + + # Should return None (generic fallback) but warn about close matches + assert result is None + assert "Did you mean one of these?" in caplog.text + assert "SETUPTOOLS_SCM_TEST_FOR_MY_PACKAG" in caplog.text + + def test_fallback_to_generic(self) -> None: + """Test fallback to generic env var when dist-specific not found.""" + env = {"SETUPTOOLS_SCM_TEST": "generic_fallback"} + + result = read_named_env(name="TEST", dist_name="nonexistent-package", env=env) + + assert result == "generic_fallback" + + def test_no_generic_fallback(self) -> None: + """Test behavior when neither dist-specific nor generic env vars exist.""" + env: dict[str, str] = {} + result = read_named_env(name="TEST", dist_name="some-package", env=env) + + assert result is None + + def test_dist_specific_overrides_generic(self) -> None: + """Test that dist-specific env vars override generic ones.""" + env = { + "SETUPTOOLS_SCM_TEST": "generic", + "SETUPTOOLS_SCM_TEST_FOR_MY_PACKAGE": "specific", + } + + result = read_named_env(name="TEST", dist_name="my-package", env=env) + + assert result == "specific" + + def test_custom_tool_prefix(self) -> None: + """Test that custom tool prefixes work.""" + env = {"CUSTOM_TOOL_TEST_FOR_MY_PACKAGE": "custom_value"} + + result = read_named_env( + tool="CUSTOM_TOOL", name="TEST", dist_name="my-package", env=env + ) + + assert result == "custom_value" + + def test_complex_dist_name_normalization( + self, caplog: pytest.LogCaptureFixture + ) -> None: + """Test complex dist name normalization scenarios.""" + # User uses a non-canonical format (keeping underscores instead of canonical hyphens) + # The canonical form of "complex.dist-name_with.dots" is "complex-dist-name-with-dots" + # which becomes "COMPLEX_DIST_NAME_WITH_DOTS" as env var + # But user set it with mixed format: + env = {"SETUPTOOLS_SCM_TEST_FOR_COMPLEX.DIST_NAME_WITH.DOTS": "value"} + + with caplog.at_level(logging.WARNING): + result = read_named_env( + name="TEST", dist_name="complex.dist-name_with.dots", env=env + ) + + assert result == "value" + assert "Found environment variable" in caplog.text + + def test_lowercase_environment_variable( + self, caplog: pytest.LogCaptureFixture + ) -> None: + """Test that lowercase environment variables are found as alternatives.""" + env = {"SETUPTOOLS_SCM_TEST_FOR_my.package": "lowercase_value"} + + with caplog.at_level(logging.WARNING): + result = read_named_env(name="TEST", dist_name="my.package", env=env) + + assert result == "lowercase_value" + assert "Found environment variable" in caplog.text + assert "but expected" in caplog.text + + def test_edge_case_empty_dist_name(self) -> None: + """Test edge case with empty dist name.""" + env = {"SETUPTOOLS_SCM_TEST": "generic"} + + result = read_named_env(name="TEST", dist_name="", env=env) + + # Should still try dist-specific lookup but fall back to generic + assert result == "generic" diff --git a/testing/test_pyproject_reading.py b/testing/test_pyproject_reading.py new file mode 100644 index 00000000..2a1fa89b --- /dev/null +++ b/testing/test_pyproject_reading.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +from pathlib import Path +from unittest.mock import Mock + +import pytest + +from setuptools_scm._integration.pyproject_reading import has_build_package_with_extra +from setuptools_scm._integration.pyproject_reading import read_pyproject + + +class TestPyProjectReading: + """Test the pyproject reading functionality.""" + + def test_read_pyproject_missing_file_raises(self, tmp_path: Path) -> None: + """Test that read_pyproject raises FileNotFoundError when file is missing.""" + with pytest.raises(FileNotFoundError): + read_pyproject(path=tmp_path / "nonexistent.toml") + + def test_read_pyproject_existing_file(self, tmp_path: Path) -> None: + """Test that read_pyproject reads existing files correctly.""" + # Create a simple pyproject.toml + pyproject_content = """ +[build-system] +requires = ["setuptools>=80", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" + +[project] +name = "test-package" +dynamic = ["version"] + +[tool.setuptools_scm] +""" + pyproject_file = tmp_path / "pyproject.toml" + pyproject_file.write_text(pyproject_content, encoding="utf-8") + + result = read_pyproject(path=pyproject_file) + + assert result.path == pyproject_file + assert result.tool_name == "setuptools_scm" + assert result.is_required is True + assert result.section_present is True + assert result.project_present is True + assert result.project.get("name") == "test-package" + + +class TestBuildPackageWithExtra: + """Test the has_build_package_with_extra function.""" + + def test_has_simple_extra(self) -> None: + """Test that simple extra is detected correctly.""" + requires = ["setuptools-scm[simple]"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is True + ) + + def test_has_no_simple_extra(self) -> None: + """Test that missing simple extra is detected correctly.""" + requires = ["setuptools-scm"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is False + ) + + def test_has_different_extra(self) -> None: + """Test that different extra is not detected as simple.""" + requires = ["setuptools-scm[toml]"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is False + ) + + def test_has_multiple_extras_including_simple(self) -> None: + """Test that simple extra is detected when multiple extras are present.""" + requires = ["setuptools-scm[simple,toml]"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is True + ) + + def test_different_package_with_simple_extra(self) -> None: + """Test that simple extra on different package is not detected.""" + requires = ["other-package[simple]"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is False + ) + + def test_version_specifier_with_extra(self) -> None: + """Test that version specifiers work correctly with extras.""" + requires = ["setuptools-scm[simple]>=8.0"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is True + ) + + def test_complex_requirement_with_extra(self) -> None: + """Test that complex requirements with extras work correctly.""" + requires = ["setuptools-scm[simple]>=8.0,<9.0"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is True + ) + + def test_empty_requires_list(self) -> None: + """Test that empty requires list returns False.""" + requires: list[str] = [] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is False + ) + + def test_invalid_requirement_string(self) -> None: + """Test that invalid requirement strings are handled gracefully.""" + requires = ["invalid requirement string"] + assert ( + has_build_package_with_extra(requires, "setuptools-scm", "simple") is False + ) + + +def test_read_pyproject_with_given_definition(monkeypatch: pytest.MonkeyPatch) -> None: + """Test that read_pyproject reads existing files correctly.""" + monkeypatch.setattr( + "setuptools_scm._integration.pyproject_reading.read_toml_content", + Mock(side_effect=FileNotFoundError("this test should not read")), + ) + + res = read_pyproject( + _given_definition={ + "build-system": {"requires": ["setuptools-scm[simple]"]}, + "project": {"name": "test-package", "dynamic": ["version"]}, + } + ) + + assert res.should_infer() + + +def test_read_pyproject_with_setuptools_dynamic_version_warns() -> None: + """Test that warning is issued when version inference is enabled.""" + with pytest.warns( + UserWarning, + match=r"pyproject\.toml: at \[tool\.setuptools\.dynamic\]", + ): + pyproject_data = read_pyproject( + _given_definition={ + "build-system": {"requires": ["setuptools-scm[simple]"]}, + "project": {"name": "test-package", "dynamic": ["version"]}, + "tool": { + "setuptools": { + "dynamic": {"version": {"attr": "test_package.__version__"}} + } + }, + } + ) + assert pyproject_data.project_version is None + + +def test_read_pyproject_with_setuptools_dynamic_version_no_warn_when_file_finder_only() -> ( + None +): + """Test that no warning is issued when only file finder is used (no version inference).""" + # When setuptools-scm is used only for file finding (no [tool.setuptools_scm] section, + # no [simple] extra, version not in dynamic), it's valid to use tool.setuptools.dynamic.version + import warnings + + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + pyproject_data = read_pyproject( + _given_definition={ + "build-system": {"requires": ["setuptools-scm"]}, + "project": {"name": "test-package", "version": "1.0.0"}, + "tool": { + "setuptools": { + "dynamic": {"version": {"attr": "test_package.__version__"}} + } + }, + } + ) + + # Filter to check for the dynamic version warning specifically + relevant_warnings = [ + w for w in warning_list if "tool.setuptools.dynamic" in str(w.message) + ] + assert len(relevant_warnings) == 0, ( + "Should not warn about tool.setuptools.dynamic when only using file finder" + ) + assert pyproject_data.project_version == "1.0.0" + assert not pyproject_data.should_infer() diff --git a/testing/test_regressions.py b/testing/test_regressions.py index 21f52711..326d62b8 100644 --- a/testing/test_regressions.py +++ b/testing/test_regressions.py @@ -101,6 +101,84 @@ def test_case_mismatch_on_windows_git(tmp_path: Path) -> None: assert res is not None +@pytest.mark.skipif(sys.platform != "win32", reason="this bug is only valid on windows") +def test_case_mismatch_nested_dir_windows_git(tmp_path: Path) -> None: + """Test case where we have a nested directory with different casing""" + # Create git repo in my_repo + repo_path = tmp_path / "my_repo" + repo_path.mkdir() + run("git init", repo_path) + + # Create a nested directory with specific casing + nested_dir = repo_path / "CasedDir" + nested_dir.mkdir() + + # Create a pyproject.toml in the nested directory + (nested_dir / "pyproject.toml").write_text( + """ +[build-system] +requires = ["setuptools>=64", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "test-project" +dynamic = ["version"] + +[tool.setuptools_scm] +""", + encoding="utf-8", + ) + + # Add and commit the file + run("git add .", repo_path) + run("git commit -m 'Initial commit'", repo_path) + + # Now try to parse from the nested directory with lowercase path + # This simulates: cd my_repo/caseddir (lowercase) when actual dir is CasedDir + lowercase_nested_path = str(nested_dir).replace("CasedDir", "caseddir") + + # This should trigger the assertion error in _git_toplevel + try: + res = parse(lowercase_nested_path, Configuration()) + # If we get here without assertion error, the bug is already fixed or not triggered + print(f"Parse succeeded with result: {res}") + except AssertionError as e: + print(f"AssertionError caught as expected: {e}") + # Re-raise so the test fails, showing we reproduced the bug + raise + + +def test_case_mismatch_force_assertion_failure(tmp_path: Path) -> None: + """Force the assertion failure by directly calling _git_toplevel with mismatched paths""" + from setuptools_scm._file_finders.git import _git_toplevel + + # Create git repo structure + repo_path = tmp_path / "my_repo" + repo_path.mkdir() + run("git init", repo_path) + + # Create nested directory + nested_dir = repo_path / "CasedDir" + nested_dir.mkdir() + + # Add and commit something to make it a valid repo + (nested_dir / "test.txt").write_text("test", encoding="utf-8") + run("git add .", repo_path) + run("git commit -m 'Initial commit'", repo_path) + + # Now call _git_toplevel with a path that has different casing + # This should cause the assertion to fail + lowercase_nested_path = str(nested_dir).replace("CasedDir", "caseddir") + + try: + result = _git_toplevel(lowercase_nested_path) + print(f"_git_toplevel returned: {result}") + # If no assertion error, either the bug is fixed or we didn't trigger it properly + except AssertionError as e: + print(f"AssertionError as expected: {e}") + raise # Let the test fail to show we reproduced the issue + + def test_entrypoints_load() -> None: d = distribution("setuptools-scm") diff --git a/testing/test_version.py b/testing/test_version.py index 32a65c0d..e63c9493 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -1,8 +1,12 @@ from __future__ import annotations +import re + from dataclasses import replace from datetime import date +from datetime import datetime from datetime import timedelta +from datetime import timezone from typing import Any import pytest @@ -67,7 +71,27 @@ def test_next_semver(version: ScmVersion, expected_next: str) -> None: def test_next_semver_bad_tag() -> None: - version = meta("1.0.0-foo", preformatted=True, config=c) + # Create a mock version class that represents an invalid version for testing error handling + from typing import cast + + from setuptools_scm._version_cls import _VersionT + + class BrokenVersionForTest: + """A mock version that behaves like a string but passes type checking.""" + + def __init__(self, version_str: str): + self._version_str = version_str + + def __str__(self) -> str: + return self._version_str + + def __repr__(self) -> str: + return f"BrokenVersionForTest({self._version_str!r})" + + # Cast to the expected type to avoid type checking issues + broken_tag = cast(_VersionT, BrokenVersionForTest("1.0.0-foo")) + version = meta(broken_tag, preformatted=True, config=c) + with pytest.raises( ValueError, match=r"1\.0\.0-foo.* can't be parsed as numeric version" ): @@ -221,6 +245,17 @@ def test_tag_regex1(tag: str, expected: str) -> None: assert result.tag.public == expected +def test_regex_match_but_no_version() -> None: + with pytest.raises( + ValueError, + match=( + r'The tag_regex "\(\?P\)\.\*" matched tag "v1",' + " however the matched group has no value" + ), + ): + meta("v1", config=replace(c, tag_regex=re.compile(r"(?P).*"))) + + @pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/471") def test_version_bump_bad() -> None: class YikesVersion: @@ -269,11 +304,14 @@ def test_custom_version_schemes() -> None: assert custom_computed == no_guess_dev_version(version) +# Fixed time for consistent test behavior across timezone boundaries +# This prevents issue #687 where tests failed around midnight in non-UTC timezones +_TEST_TIME = datetime(2023, 12, 15, 12, 0, 0, tzinfo=timezone.utc) + + def date_offset(base_date: date | None = None, days_offset: int = 0) -> date: if base_date is None: - from setuptools_scm.version import _source_epoch_or_utc_now - - base_date = _source_epoch_or_utc_now().date() + base_date = _TEST_TIME.date() return base_date - timedelta(days=days_offset) @@ -304,12 +342,23 @@ def date_to_str( id="leading 0s", ), pytest.param( - meta(date_to_str(days_offset=3), config=c_non_normalize, dirty=True), + meta( + date_to_str(days_offset=3), + config=c_non_normalize, + dirty=True, + time=_TEST_TIME, + ), date_to_str() + ".0.dev0", id="dirty other day", ), pytest.param( - meta(date_to_str(), config=c_non_normalize, distance=2, branch="default"), + meta( + date_to_str(), + config=c_non_normalize, + distance=2, + branch="default", + time=_TEST_TIME, + ), date_to_str() + ".1.dev2", id="normal branch", ), @@ -382,8 +431,8 @@ def test_calver_by_date(version: ScmVersion, expected_next: str) -> None: [ pytest.param(meta("1.0.0", config=c), "1.0.0", id="SemVer exact stays"), pytest.param( - meta("1.0.0", config=c_non_normalize, dirty=True), - "09.02.13.1.dev0", + meta("1.0.0", config=c_non_normalize, dirty=True, time=_TEST_TIME), + "23.12.15.0.dev0", id="SemVer dirty is replaced by date", marks=pytest.mark.filterwarnings("ignore:.*legacy version.*:UserWarning"), ), @@ -397,7 +446,12 @@ def test_calver_by_date_semver(version: ScmVersion, expected_next: str) -> None: def test_calver_by_date_future_warning() -> None: with pytest.warns(UserWarning, match="your previous tag*"): calver_by_date( - meta(date_to_str(days_offset=-2), config=c_non_normalize, distance=2) + meta( + date_to_str(days_offset=-2), + config=c_non_normalize, + distance=2, + time=_TEST_TIME, + ) ) @@ -437,8 +491,54 @@ def __str__(self) -> str: def __repr__(self) -> str: return f"MyVersion" + @property + def public(self) -> str: + """The public portion of the version (without local part).""" + return self.tag.split("+")[0] + + @property + def local(self) -> str | None: + """The local version segment.""" + if "+" in self.tag: + return self.tag.split("+", 1)[1] + return None + config = Configuration(version_cls=MyVersion) # type: ignore[arg-type] scm_version = meta("1.0.0-foo", config=config) assert isinstance(scm_version.tag, MyVersion) assert str(scm_version.tag) == "Custom 1.0.0-foo" + + +@pytest.mark.parametrize("config_key", ["version_scheme", "local_scheme"]) +def test_no_matching_entrypoints(config_key: str) -> None: + version = meta( + "1.0", + config=replace(c, **{config_key: "nonexistent"}), # type: ignore[arg-type] + ) + with pytest.raises( + ValueError, + match=( + r'Couldn\'t find any implementations for entrypoint "setuptools_scm\..*?"' + ' with value "nonexistent"' + ), + ): + format_version(version) + + +def test_all_entrypoints_return_none() -> None: + version = meta( + "1.0", + config=replace( + c, + version_scheme=lambda v: None, # type: ignore[arg-type,return-value] + ), + ) + with pytest.raises( + ValueError, + match=( + 'None of the "setuptools_scm.version_scheme" entrypoints matching' + r" .*? returned a value." + ), + ): + format_version(version) diff --git a/testing/test_version_inference.py b/testing/test_version_inference.py new file mode 100644 index 00000000..967ab768 --- /dev/null +++ b/testing/test_version_inference.py @@ -0,0 +1,255 @@ +from __future__ import annotations + +from types import SimpleNamespace +from typing import Any + +import pytest + +from setuptools_scm._integration.pyproject_reading import PyProjectData +from setuptools_scm._integration.version_inference import VersionInferenceConfig +from setuptools_scm._integration.version_inference import VersionInferenceNoOp +from setuptools_scm._integration.version_inference import VersionInferenceResult +from setuptools_scm._integration.version_inference import VersionInferenceWarning +from setuptools_scm._integration.version_inference import get_version_inference_config + +# Common test data +PYPROJECT = SimpleNamespace( + DEFAULT=PyProjectData.for_testing( + is_required=True, section_present=True, project_present=True + ), + WITHOUT_TOOL_SECTION=PyProjectData.for_testing( + is_required=True, section_present=False, project_present=True + ), + ONLY_REQUIRED=PyProjectData.for_testing( + is_required=True, section_present=False, project_present=False + ), + WITHOUT_PROJECT=PyProjectData.for_testing( + is_required=True, section_present=True, project_present=False + ), +) + +OVERRIDES = SimpleNamespace( + NOT_GIVEN=None, + EMPTY={}, + CALVER={"version_scheme": "calver"}, + UNRELATED={"key": "value"}, +) + + +WARNING_PACKAGE = VersionInferenceWarning( + message="version of test_package already set", +) +WARNING_NO_PACKAGE = VersionInferenceWarning( + message="version of None already set", +) + +NOOP = VersionInferenceNoOp() + + +def expect_config( + *, + dist_name: str | None = "test_package", + current_version: str | None, + pyproject_data: PyProjectData = PYPROJECT.DEFAULT, + overrides: dict[str, Any] | None = None, + expected: type[VersionInferenceConfig] + | VersionInferenceWarning + | VersionInferenceNoOp, +) -> None: + """Helper to test get_version_inference_config and assert expected result type.""" + __tracebackhide__ = True + result = get_version_inference_config( + dist_name=dist_name, + current_version=current_version, + pyproject_data=pyproject_data, + overrides=overrides, + ) + + expectation: VersionInferenceResult + if expected == VersionInferenceConfig: + expectation = VersionInferenceConfig( + dist_name=dist_name, + pyproject_data=pyproject_data, + overrides=overrides, + ) + else: + assert isinstance(expected, (VersionInferenceNoOp, VersionInferenceWarning)) + expectation = expected + + assert result == expectation + + +infer_implied = pytest.mark.parametrize( + ("overrides", "pyproject_data"), + [ + pytest.param( + OVERRIDES.EMPTY, PYPROJECT.DEFAULT, id="empty_overrides_default_pyproject" + ), + pytest.param( + OVERRIDES.EMPTY, + PYPROJECT.WITHOUT_TOOL_SECTION, + id="empty_overrides_without_tool_section", + ), + pytest.param( + OVERRIDES.NOT_GIVEN, + PYPROJECT.DEFAULT, + id="infer_version_default_pyproject", + ), + ], +) + + +@pytest.mark.parametrize("package_name", ["test_package", None]) +@infer_implied +def test_implied_with_version_warns( + package_name: str | None, + overrides: dict[str, Any] | None, + pyproject_data: PyProjectData, +) -> None: + expect_config( + dist_name=package_name, + current_version="1.0.0", + pyproject_data=pyproject_data, + overrides=overrides, + expected=WARNING_PACKAGE if package_name else WARNING_NO_PACKAGE, + ) + + +@pytest.mark.parametrize("package_name", ["test_package", None]) +@infer_implied +def test_implied_without_version_infers( + package_name: str | None, + overrides: dict[str, Any] | None, + pyproject_data: PyProjectData, +) -> None: + expect_config( + dist_name=package_name, + current_version=None, + pyproject_data=pyproject_data, + overrides=overrides, + expected=VersionInferenceConfig, + ) + + +def test_no_config_no_infer() -> None: + expect_config( + current_version=None, + pyproject_data=PYPROJECT.WITHOUT_TOOL_SECTION, + overrides=OVERRIDES.NOT_GIVEN, + expected=NOOP, + ) + + +class TestVersionInferenceDecision: + """Test the version inference decision logic.""" + + def test_setuptools_scm_required_no_project_section_infer_version(self) -> None: + """We don't infer without tool section even if required: infer_version path.""" + expect_config( + current_version=None, + pyproject_data=PYPROJECT.ONLY_REQUIRED, + overrides=None, + expected=NOOP, + ) + + def test_setuptools_scm_required_no_project_section_version_keyword(self) -> None: + """Test that we DO infer when setuptools-scm is required but no project section and use_scm_version=True.""" + expect_config( + current_version=None, + pyproject_data=PYPROJECT.ONLY_REQUIRED, + overrides=OVERRIDES.EMPTY, + expected=VersionInferenceConfig, + ) + + def test_setuptools_scm_required_no_project_section_version_keyword_with_config( + self, + ) -> None: + """Test that we DO infer when setuptools-scm is required but no project section and use_scm_version={config}.""" + expect_config( + current_version=None, + pyproject_data=PYPROJECT.ONLY_REQUIRED, + overrides=OVERRIDES.CALVER, + expected=VersionInferenceConfig, + ) + + def test_tool_section_present(self) -> None: + """We infer when tool section is present.""" + expect_config( + current_version=None, + pyproject_data=PYPROJECT.WITHOUT_PROJECT, + expected=VersionInferenceConfig, + ) + + def test_simple_extra_with_dynamic_version_infers(self) -> None: + """We infer when setuptools-scm[simple] is in build-system.requires and version is dynamic.""" + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + has_dynamic_version=True, + build_requires=["setuptools-scm[simple]"], + ) + expect_config( + current_version=None, + pyproject_data=pyproject_data, + expected=VersionInferenceConfig, + ) + + def test_simple_extra_without_dynamic_version_no_infer(self) -> None: + """We don't infer when setuptools-scm[simple] is present but version is not dynamic.""" + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + has_dynamic_version=False, + build_requires=["setuptools-scm[simple]"], + ) + expect_config( + current_version=None, + pyproject_data=pyproject_data, + expected=NOOP, + ) + + def test_no_simple_extra_with_dynamic_version_no_infer(self) -> None: + """We don't infer when setuptools-scm (without simple extra) is present even with dynamic version.""" + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + has_dynamic_version=True, + build_requires=["setuptools-scm"], + ) + expect_config( + current_version=None, + pyproject_data=pyproject_data, + expected=NOOP, + ) + + def test_simple_extra_no_project_section_no_infer(self) -> None: + """We don't infer when setuptools-scm[simple] is present but no project section.""" + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=False, + build_requires=["setuptools-scm[simple]"], + ) + expect_config( + current_version=None, + pyproject_data=pyproject_data, + expected=NOOP, + ) + + def test_simple_extra_with_version_warns(self) -> None: + """We warn when setuptools-scm[simple] is present with dynamic version but version is already set.""" + pyproject_data = PyProjectData.for_testing( + is_required=True, + section_present=False, + project_present=True, + has_dynamic_version=True, + build_requires=["setuptools-scm[simple]"], + ) + expect_config( + current_version="1.0.0", + pyproject_data=pyproject_data, + expected=WARNING_PACKAGE, + ) diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py index e1aa6c4f..1f5efbe7 100644 --- a/testing/wd_wrapper.py +++ b/testing/wd_wrapper.py @@ -20,13 +20,13 @@ def __init__(self, cwd: Path) -> None: self.cwd = cwd self.__counter = itertools.count() - def __call__(self, cmd: list[str] | str, **kw: object) -> str: + def __call__(self, cmd: list[str] | str, *, timeout: int = 10, **kw: object) -> str: if kw: assert isinstance(cmd, str), "formatting the command requires text input" cmd = cmd.format(**kw) from setuptools_scm._run_cmd import run - return run(cmd, cwd=self.cwd).stdout + return run(cmd, cwd=self.cwd, timeout=timeout).stdout def write(self, name: str, content: str | bytes) -> Path: path = self.cwd / name diff --git a/tox.ini b/tox.ini index cdb25590..83af3bbe 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,12 @@ ignore=E203,W503 [testenv] usedevelop=True -extras=test +dependency_groups = test +deps = + pytest + pytest-cov + pytest-timeout + pytest-xdist commands= python -X warn_default_encoding -m pytest {posargs} diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..4b145a91 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2128 @@ +version = 1 +revision = 3 +requires-python = ">=3.8" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "argh" +version = "0.30.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/12/353fc6b87bfaf98fe5a0c95e8cba03acd24ab0eea99d1307e1ab7a3809ba/argh-0.30.5.tar.gz", hash = "sha256:b37dfd617a09d19a4a7bcaed0e060b288bc7ac8dfdc0facf886a49a25ff33728", size = 63041, upload-time = "2023-12-25T22:05:32.15Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/51/99d9dfcb588e15b4d9630f98f84d4e766d03b47da52839395057dbbe2df4/argh-0.30.5-py3-none-any.whl", hash = "sha256:3844e955d160f0689a3cdca06a59dfcfbf1fcea70029d67d473f73503341e0d8", size = 44635, upload-time = "2023-12-25T22:05:29.35Z" }, +] + +[[package]] +name = "astunparse" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "python_full_version < '3.9'" }, + { name = "wheel", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +version = "5.7.post1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/df/30/903f35159c87ff1d92aa3fcf8cb52de97632a21e0ae43ed940f5d033e01a/backrefs-5.7.post1.tar.gz", hash = "sha256:8b0f83b770332ee2f1c8244f4e03c77d127a0fa529328e6a0e77fa25bee99678", size = 6582270, upload-time = "2024-06-16T18:38:20.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/bb/47fc255d1060dcfd55b460236380edd8ebfc5b2a42a0799ca90c9fc983e3/backrefs-5.7.post1-py310-none-any.whl", hash = "sha256:c5e3fd8fd185607a7cb1fefe878cfb09c34c0be3c18328f12c574245f1c0287e", size = 380429, upload-time = "2024-06-16T18:38:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/39ef491caef3abae945f5a5fd72830d3b596bfac0630508629283585e213/backrefs-5.7.post1-py311-none-any.whl", hash = "sha256:712ea7e494c5bf3291156e28954dd96d04dc44681d0e5c030adf2623d5606d51", size = 392234, upload-time = "2024-06-16T18:38:12.283Z" }, + { url = "https://files.pythonhosted.org/packages/6a/00/33403f581b732ca70fdebab558e8bbb426a29c34e0c3ed674a479b74beea/backrefs-5.7.post1-py312-none-any.whl", hash = "sha256:a6142201c8293e75bce7577ac29e1a9438c12e730d73a59efdd1b75528d1a6c5", size = 398110, upload-time = "2024-06-16T18:38:14.257Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ea/df0ac74a26838f6588aa012d5d801831448b87d0a7d0aefbbfabbe894870/backrefs-5.7.post1-py38-none-any.whl", hash = "sha256:ec61b1ee0a4bfa24267f6b67d0f8c5ffdc8e0d7dc2f18a2685fd1d8d9187054a", size = 369477, upload-time = "2024-06-16T18:38:16.196Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e8/e43f535c0a17a695e5768670fc855a0e5d52dc0d4135b3915bfa355f65ac/backrefs-5.7.post1-py39-none-any.whl", hash = "sha256:05c04af2bf752bb9a6c9dcebb2aff2fab372d3d9d311f2a138540e307756bd3a", size = 380429, upload-time = "2024-06-16T18:38:18.079Z" }, +] + +[[package]] +name = "backrefs" +version = "5.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, +] + +[[package]] +name = "bracex" +version = "2.5.post1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/57418c4404cd22fe6275b8301ca2b46a8cdaa8157938017a9ae0b3edf363/bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6", size = 26641, upload-time = "2024-09-28T21:41:22.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/02/8db98cdc1a58e0abd6716d5e63244658e6e63513c65f469f34b6f1053fd0/bracex-2.5.post1-py3-none-any.whl", hash = "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6", size = 11558, upload-time = "2024-09-28T21:41:21.016Z" }, +] + +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + +[[package]] +name = "brei" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argh", marker = "python_full_version >= '3.11'" }, + { name = "rich", marker = "python_full_version >= '3.11'" }, + { name = "rich-argparse", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/97/503b9bc095501678cd6e10a940e042d36f2a77ee41b798e93477d142543d/brei-0.2.4.tar.gz", hash = "sha256:fb8a1f191a1c70a81a9df366c530e01938b8082e7198e898d4ea8eb53066fa92", size = 18853, upload-time = "2024-11-25T10:17:11.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/6d/e5c216146c0654cbf7d4e0567a075e3f13f2700bc8812b9ec3a35bf1099b/brei-0.2.4-py3-none-any.whl", hash = "sha256:46967640f1aebe3c698456434a8ff0b2901860c4544778ab763803875e925155", size = 22436, upload-time = "2024-11-25T10:17:10.292Z" }, +] + +[[package]] +name = "build" +version = "1.2.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.10.2'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, +] + +[[package]] +name = "certifi" +version = "2025.7.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fd/f700cfd4ad876def96d2c769d8a32d808b12d1010b6003dc6639157f99ee/charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", size = 198257, upload-time = "2025-05-02T08:33:45.511Z" }, + { url = "https://files.pythonhosted.org/packages/3a/95/6eec4cbbbd119e6a402e3bfd16246785cc52ce64cf21af2ecdf7b3a08e91/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", size = 143453, upload-time = "2025-05-02T08:33:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/d4f913660383b3d93dbe6f687a312ea9f7e89879ae883c4e8942048174d4/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", size = 153130, upload-time = "2025-05-02T08:33:50.568Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/7540141529eabc55bf19cc05cd9b61c2078bebfcdbd3e799af99b777fc28/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", size = 145688, upload-time = "2025-05-02T08:33:52.828Z" }, + { url = "https://files.pythonhosted.org/packages/2e/bb/d76d3d6e340fb0967c43c564101e28a78c9a363ea62f736a68af59ee3683/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", size = 147418, upload-time = "2025-05-02T08:33:54.718Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ef/b7c1f39c0dc3808160c8b72e0209c2479393966313bfebc833533cfff9cc/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", size = 150066, upload-time = "2025-05-02T08:33:56.597Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/4e47cc23d2a4a5eb6ed7d6f0f8cda87d753e2f8abc936d5cf5ad2aae8518/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", size = 144499, upload-time = "2025-05-02T08:33:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/d7/9c/efdf59dd46593cecad0548d36a702683a0bdc056793398a9cd1e1546ad21/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", size = 152954, upload-time = "2025-05-02T08:34:00.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/b3/4e8b73f7299d9aaabd7cd26db4a765f741b8e57df97b034bb8de15609002/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", size = 155876, upload-time = "2025-05-02T08:34:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/53/cb/6fa0ccf941a069adce3edb8a1e430bc80e4929f4d43b5140fdf8628bdf7d/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", size = 153186, upload-time = "2025-05-02T08:34:04.481Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c6/80b93fabc626b75b1665ffe405e28c3cef0aae9237c5c05f15955af4edd8/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", size = 148007, upload-time = "2025-05-02T08:34:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/41/eb/c7367ac326a2628e4f05b5c737c86fe4a8eb3ecc597a4243fc65720b3eeb/charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", size = 97923, upload-time = "2025-05-02T08:34:08.792Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/1c82646582ccf2c757fa6af69b1a3ea88744b8d2b4ab93b7686b2533e023/charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", size = 105020, upload-time = "2025-05-02T08:34:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, + { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, + { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, + { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, + { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "copier" +version = "9.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11'" }, + { name = "dunamai", marker = "python_full_version >= '3.11'" }, + { name = "funcy", marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "jinja2-ansible-filters", marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pathspec", marker = "python_full_version >= '3.11'" }, + { name = "platformdirs", version = "4.3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "plumbum", marker = "python_full_version >= '3.11'" }, + { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "questionary", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/78/1246b4a1ddeb404037f76efedab9b42ada4e6ba56c574d8846e2a6b8f498/copier-9.8.0.tar.gz", hash = "sha256:343ac1eb65e678aa355690d7f19869ef07cabf837f511a87ed452443c085ec58", size = 579353, upload-time = "2025-07-07T18:47:03.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/ed/839c91ff365f24756c90189e07f9de226d2e37cbc03c635f5d16d45d79cb/copier-9.8.0-py3-none-any.whl", hash = "sha256:ca0bee47f198b66cec926c4f1a3aa77f11ee0102624369c10e42ca9058c0a891", size = 55744, upload-time = "2025-07-07T18:47:01.905Z" }, +] + +[[package]] +name = "dunamai" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "python_full_version >= '3.11'" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "entangled-cli" +version = "2.1.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argh", marker = "python_full_version >= '3.11'" }, + { name = "brei", marker = "python_full_version >= '3.11'" }, + { name = "copier", marker = "python_full_version >= '3.11'" }, + { name = "filelock", marker = "python_full_version >= '3.11'" }, + { name = "mawk", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "rich", marker = "python_full_version >= '3.11'" }, + { name = "rich-argparse", marker = "python_full_version >= '3.11'" }, + { name = "tomlkit", marker = "python_full_version >= '3.11'" }, + { name = "watchdog", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/35/568227a7e821e42520f769ca9bee162dfd8ef03e18616d1201577a2e25e0/entangled_cli-2.1.13.tar.gz", hash = "sha256:c17b1e479f1ac9689187a4ed7eb31c066af3458cdc663afffcb0f10051228642", size = 36900, upload-time = "2025-04-24T23:01:11.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/8c/cf9c69a44aa28900ceef8ab17aafc2beefab13d3cab6b771a4e3109e3c2e/entangled_cli-2.1.13-py3-none-any.whl", hash = "sha256:01822e05e393934ec1bc9f867d179581703282352b836b69d5cf654ee8e6e6b7", size = 50175, upload-time = "2025-04-24T23:01:09.981Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "flake8" +version = "5.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version < '3.8.1'" }, + { name = "pycodestyle", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, + { name = "pyflakes", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/00/9808c62b2d529cefc69ce4e4a1ea42c0f855effa55817b7327ec5b75e60a/flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", size = 145862, upload-time = "2022-08-03T23:21:27.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/a0/b881b63a17a59d9d07f5c0cc91a29182c8e8a9aa2bde5b3b2b16519c02f4/flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248", size = 61897, upload-time = "2022-08-03T23:21:25.027Z" }, +] + +[[package]] +name = "flake8" +version = "7.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, + { name = "pycodestyle", version = "2.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, + { name = "pyflakes", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/16/3f2a0bb700ad65ac9663262905a025917c020a3f92f014d2ba8964b4602c/flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd", size = 48119, upload-time = "2025-02-16T18:45:44.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/f8/08d37b2cd89da306e3520bd27f8a85692122b42b56c0c2c3784ff09c022f/flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a", size = 57745, upload-time = "2025-02-16T18:45:42.351Z" }, +] + +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "mccabe", marker = "python_full_version >= '3.9'" }, + { name = "pycodestyle", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyflakes", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + +[[package]] +name = "funcy" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/b8/c6081521ff70afdff55cd9512b2220bbf4fa88804dae51d1b57b4b58ef32/funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb", size = 537931, upload-time = "2023-03-28T06:22:46.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0", size = 30891, upload-time = "2023-03-28T06:22:42.576Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "astunparse", marker = "python_full_version < '3.9'" }, + { name = "colorama", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e9/b2c86ad9d69053e497a24ceb25d661094fb321ab4ed39a8b71793dcbae82/griffe-1.4.0.tar.gz", hash = "sha256:8fccc585896d13f1221035d32c50dec65830c87d23f9adb9b1e6f3d63574f7f5", size = 381028, upload-time = "2024-10-11T12:53:54.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/7c/e9e66869c2e4c9b378474e49c993128ec0131ef4721038b6d06e50538caf/griffe-1.4.0-py3-none-any.whl", hash = "sha256:e589de8b8c137e99a46ec45f9598fc0ac5b6868ce824b24db09c02d117b89bc5", size = 127015, upload-time = "2024-10-11T12:53:52.383Z" }, +] + +[[package]] +name = "griffe" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/72/10c5799440ce6f3001b7913988b50a99d7b156da71fe19be06178d5a2dd5/griffe-1.8.0.tar.gz", hash = "sha256:0b4658443858465c13b2de07ff5e15a1032bc889cfafad738a476b8b97bb28d7", size = 401098, upload-time = "2025-07-22T23:45:54.629Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/c4/a839fcc28bebfa72925d9121c4d39398f77f95bcba0cf26c972a0cfb1de7/griffe-1.8.0-py3-none-any.whl", hash = "sha256:110faa744b2c5c84dd432f4fa9aa3b14805dd9519777dd55e8db214320593b02", size = 132487, upload-time = "2025-07-22T23:45:52.778Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "zipp", version = "3.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "jinja2-ansible-filters" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/27/fa186af4b246eb869ffca8ffa42d92b05abaec08c99329e74d88b2c46ec7/jinja2-ansible-filters-1.3.2.tar.gz", hash = "sha256:07c10cf44d7073f4f01102ca12d9a2dc31b41d47e4c61ed92ef6a6d2669b356b", size = 16945, upload-time = "2022-06-30T14:08:50.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl", hash = "sha256:e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34", size = 18975, upload-time = "2022-06-30T14:08:49.571Z" }, +] + +[[package]] +name = "markdown" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086, upload-time = "2024-08-16T15:55:17.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349, upload-time = "2024-08-16T15:55:16.176Z" }, +] + +[[package]] +name = "markdown" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206, upload-time = "2024-02-02T16:30:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079, upload-time = "2024-02-02T16:30:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620, upload-time = "2024-02-02T16:30:08.31Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818, upload-time = "2024-02-02T16:30:09.577Z" }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493, upload-time = "2024-02-02T16:30:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630, upload-time = "2024-02-02T16:30:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745, upload-time = "2024-02-02T16:30:14.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021, upload-time = "2024-02-02T16:30:16.032Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659, upload-time = "2024-02-02T16:30:17.079Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213, upload-time = "2024-02-02T16:30:18.251Z" }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192, upload-time = "2024-02-02T16:30:57.715Z" }, + { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072, upload-time = "2024-02-02T16:30:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928, upload-time = "2024-02-02T16:30:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106, upload-time = "2024-02-02T16:31:01.582Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781, upload-time = "2024-02-02T16:31:02.71Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518, upload-time = "2024-02-02T16:31:04.392Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669, upload-time = "2024-02-02T16:31:05.53Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933, upload-time = "2024-02-02T16:31:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656, upload-time = "2024-02-02T16:31:07.767Z" }, + { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206, upload-time = "2024-02-02T16:31:08.843Z" }, + { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193, upload-time = "2024-02-02T16:31:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073, upload-time = "2024-02-02T16:31:11.442Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486, upload-time = "2024-02-02T16:31:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685, upload-time = "2024-02-02T16:31:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338, upload-time = "2024-02-02T16:31:14.812Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439, upload-time = "2024-02-02T16:31:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531, upload-time = "2024-02-02T16:31:17.13Z" }, + { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823, upload-time = "2024-02-02T16:31:18.247Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658, upload-time = "2024-02-02T16:31:19.583Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211, upload-time = "2024-02-02T16:31:20.96Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +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" } +wheels = [ + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, +] + +[[package]] +name = "mawk" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/42/0cb0d6d02649f50e2a8918a350152d8d139839cf74befeeeb83668302e48/mawk-0.1.4.tar.gz", hash = "sha256:4e115b2f7eae97406bf2360bafba22efee03d29ff298436a69db506b1535d2f1", size = 7216, upload-time = "2023-06-04T12:53:42.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/29/b4ddddd1ae74ce2651e7eedf1871a6b2240433eeec070622f81b985d5e70/mawk-0.1.4-py3-none-any.whl", hash = "sha256:8ab7ce0808d10769f8aa05af8448046c290af5f529db874b44a8fd56056c4462", size = 7941, upload-time = "2023-06-04T12:53:41.112Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag", version = "0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pyyaml-env-tag", version = "1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/ae/0f1154c614d6a8b8a36fff084e5b82af3a15f7d2060cf0dcdb1c53297a71/mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f", size = 40262, upload-time = "2024-09-01T18:29:18.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f", size = 16522, upload-time = "2024-09-01T18:29:16.605Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" }, +] + +[[package]] +name = "mkdocs-entangled-plugin" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "mkdocs", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/a2/f94a86faae15f76daf39735f4aed443a9fb5af5e568919f9d472cbc222b5/mkdocs_entangled_plugin-0.2.0.tar.gz", hash = "sha256:7f585cc5811fb097aad0435c20929108348b293830e4b8978b55e19f24631908", size = 8672, upload-time = "2023-05-03T23:56:43.771Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/89/c75092d147093ccf6994bacddd58b40d3fa92092d9ec236356f399a4d44a/mkdocs_entangled_plugin-0.2.0-py3-none-any.whl", hash = "sha256:f386631c11e6c19a41f89902f83275c274db8abacf5adac9cff88394ec9e1788", size = 10821, upload-time = "2023-05-03T23:56:42.589Z" }, +] + +[[package]] +name = "mkdocs-entangled-plugin" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "entangled-cli", marker = "python_full_version >= '3.11'" }, + { name = "mawk", marker = "python_full_version >= '3.11'" }, + { name = "mkdocs", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/1b/af0757f0a5a60a636903f520ca6164df314068374f765a072d06b65bad7a/mkdocs_entangled_plugin-0.4.0.tar.gz", hash = "sha256:e78f1ff3b55a3838f6a5d6a46d9280484af4520a4679f9bccd8b638406b026c8", size = 7527, upload-time = "2023-10-14T12:20:40.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/d0/9bf18d41e1e2eda91d67a3493778016001402781dcf9f3c391d94fb50545/mkdocs_entangled_plugin-0.4.0-py3-none-any.whl", hash = "sha256:af4fe84e2218e2c0a805d20161977e34cd56f8af24b4d1a88be4c258176b81bb", size = 9408, upload-time = "2023-10-14T12:20:18.937Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mergedeep" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-include-markdown-plugin" +version = "6.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "mkdocs", marker = "python_full_version < '3.9'" }, + { name = "wcmatch", version = "10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/fe/4bb438d0f58995f81e2616d640f7efe0df9b1f992cba706a9453676c9140/mkdocs_include_markdown_plugin-6.2.2.tar.gz", hash = "sha256:f2bd5026650492a581d2fd44be6c22f90391910d76582b96a34c264f2d17875d", size = 21045, upload-time = "2024-08-10T23:36:41.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/d9/7b2b09b4870a2cd5a80628c74553307205a8474aabe128b66e305b56ac30/mkdocs_include_markdown_plugin-6.2.2-py3-none-any.whl", hash = "sha256:d293950f6499d2944291ca7b9bc4a60e652bbfd3e3a42b564f6cceee268694e7", size = 24643, upload-time = "2024-08-10T23:36:39.736Z" }, +] + +[[package]] +name = "mkdocs-include-markdown-plugin" +version = "7.1.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "mkdocs", marker = "python_full_version >= '3.9'" }, + { name = "wcmatch", version = "10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/17/988d97ac6849b196f54d45ca9c60ca894880c160a512785f03834704b3d9/mkdocs_include_markdown_plugin-7.1.6.tar.gz", hash = "sha256:a0753cb82704c10a287f1e789fc9848f82b6beb8749814b24b03dd9f67816677", size = 23391, upload-time = "2025-06-13T18:25:51.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/a1/6cf1667a05e5f468e1263fcf848772bca8cc9e358cd57ae19a01f92c9f6f/mkdocs_include_markdown_plugin-7.1.6-py3-none-any.whl", hash = "sha256:7975a593514887c18ecb68e11e35c074c5499cfa3e51b18cd16323862e1f7345", size = 27161, upload-time = "2025-06-13T18:25:49.847Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.6.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs", version = "5.7.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "backrefs", version = "5.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions", version = "10.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pymdown-extensions", version = "10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/c1/f804ba2db2ddc2183e900befe7dad64339a34fa935034e1ab405289d0a97/mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5", size = 3951836, upload-time = "2025-07-01T10:14:15.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/30/dda19f0495a9096b64b6b3c07c4bfcff1c76ee0fc521086d53593f18b4c0/mkdocs_material-9.6.15-py3-none-any.whl", hash = "sha256:ac969c94d4fe5eb7c924b6d2f43d7db41159ea91553d18a9afc4780c34f2717a", size = 8716840, upload-time = "2025-07-01T10:14:13.18Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "jinja2", marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs", marker = "python_full_version < '3.9'" }, + { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pymdown-extensions", version = "10.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/170ff04de72227f715d67da32950c7b8434449f3805b2ec3dd1085db4d7c/mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33", size = 92677, upload-time = "2024-09-06T10:26:06.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/cc/8ba127aaee5d1e9046b0d33fa5b3d17da95a9d705d44902792e0569257fd/mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf", size = 29643, upload-time = "2024-09-06T10:26:04.498Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python", version = "1.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2", marker = "python_full_version >= '3.9'" }, + { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs", marker = "python_full_version >= '3.9'" }, + { name = "mkdocs-autorefs", version = "1.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pymdown-extensions", version = "10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python", version = "1.16.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "griffe", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocstrings", version = "0.26.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/ba/534c934cd0a809f51c91332d6ed278782ee4126b8ba8db02c2003f162b47/mkdocstrings_python-1.11.1.tar.gz", hash = "sha256:8824b115c5359304ab0b5378a91f6202324a849e1da907a3485b59208b797322", size = 166890, upload-time = "2024-09-03T17:20:54.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f2/2a2c48fda645ac6bbe73bcc974587a579092b6868e6ff8bc6d177f4db38a/mkdocstrings_python-1.11.1-py3-none-any.whl", hash = "sha256:a21a1c05acef129a618517bb5aae3e33114f569b11588b1e7af3e9d4061a71af", size = 109297, upload-time = "2024-09-03T17:20:52.621Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.16.12" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "griffe", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs-autorefs", version = "1.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocstrings", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, +] + +[[package]] +name = "mypy" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532, upload-time = "2024-10-22T21:55:47.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731, upload-time = "2024-10-22T21:54:54.221Z" }, + { url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276, upload-time = "2024-10-22T21:54:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706, upload-time = "2024-10-22T21:55:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586, upload-time = "2024-10-22T21:55:18.957Z" }, + { url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318, upload-time = "2024-10-22T21:55:13.791Z" }, + { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027, upload-time = "2024-10-22T21:55:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699, upload-time = "2024-10-22T21:55:34.646Z" }, + { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263, upload-time = "2024-10-22T21:54:51.807Z" }, + { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688, upload-time = "2024-10-22T21:55:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811, upload-time = "2024-10-22T21:54:59.152Z" }, + { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900, upload-time = "2024-10-22T21:55:37.103Z" }, + { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818, upload-time = "2024-10-22T21:55:11.513Z" }, + { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275, upload-time = "2024-10-22T21:54:37.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783, upload-time = "2024-10-22T21:55:42.852Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197, upload-time = "2024-10-22T21:54:43.68Z" }, + { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721, upload-time = "2024-10-22T21:54:22.321Z" }, + { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996, upload-time = "2024-10-22T21:54:46.023Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043, upload-time = "2024-10-22T21:55:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996, upload-time = "2024-10-22T21:55:25.811Z" }, + { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709, upload-time = "2024-10-22T21:55:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2a/13e9ad339131c0fba5c70584f639005a47088f5eed77081a3d00479df0ca/mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", size = 10955147, upload-time = "2024-10-22T21:55:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/94/39/02929067dc16b72d78109195cfed349ac4ec85f3d52517ac62b9a5263685/mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", size = 10138373, upload-time = "2024-10-22T21:54:56.889Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cc/066709bb01734e3dbbd1375749f8789bf9693f8b842344fc0cf52109694f/mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", size = 12543621, upload-time = "2024-10-22T21:54:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a2/124df839025348c7b9877d0ce134832a9249968e3ab36bb826bab0e9a1cf/mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", size = 13050348, upload-time = "2024-10-22T21:54:40.801Z" }, + { url = "https://files.pythonhosted.org/packages/45/86/cc94b1e7f7e756a63043cf425c24fb7470013ee1c032180282db75b1b335/mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", size = 9615311, upload-time = "2024-10-22T21:54:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906, upload-time = "2024-10-22T21:55:28.105Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657, upload-time = "2024-10-22T21:55:03.931Z" }, + { url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394, upload-time = "2024-10-22T21:54:49.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591, upload-time = "2024-10-22T21:55:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690, upload-time = "2024-10-22T21:54:28.814Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043, upload-time = "2024-10-22T21:55:16.617Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pip" +version = "25.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/70/53/b309b4a497b09655cb7e07088966881a57d082f48ac3cb54ea729fd2c6cf/pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea", size = 1950850, upload-time = "2025-02-09T17:14:04.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/bc/b7db44f5f39f9d0494071bddae6880eb645970366d0a200022a1a93d57f5/pip-25.0.1-py3-none-any.whl", hash = "sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f", size = 1841526, upload-time = "2025-02-09T17:14:01.463Z" }, +] + +[[package]] +name = "pip" +version = "25.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/59/de/241caa0ca606f2ec5fe0c1f4261b0465df78d786a38da693864a116c37f4/pip-25.1.1.tar.gz", hash = "sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077", size = 1940155, upload-time = "2025-05-02T15:14:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/a2/d40fb2460e883eca5199c62cfc2463fd261f760556ae6290f88488c362c0/pip-25.1.1-py3-none-any.whl", hash = "sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af", size = 1825227, upload-time = "2025-05-02T15:13:59.102Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "plumbum" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "python_full_version >= '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/5d/49ba324ad4ae5b1a4caefafbce7a1648540129344481f2ed4ef6bb68d451/plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219", size = 319083, upload-time = "2024-10-05T05:59:27.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/9d/d03542c93bb3d448406731b80f39c3d5601282f778328c22c77d270f4ed4/plumbum-1.9.0-py3-none-any.whl", hash = "sha256:9fd0d3b0e8d86e4b581af36edf3f3bbe9d1ae15b45b8caab28de1bcb27aaa7f5", size = 127970, upload-time = "2024-10-05T05:59:25.102Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/83/5bcaedba1f47200f0665ceb07bcb00e2be123192742ee0edfb66b600e5fd/pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", size = 102127, upload-time = "2022-08-03T23:13:29.715Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/e4/fc77f1039c34b3612c4867b69cbb2b8a4e569720b1f19b0637002ee03aff/pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b", size = 41493, upload-time = "2022-08-03T23:13:27.416Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232, upload-time = "2024-08-04T20:26:54.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284, upload-time = "2024-08-04T20:26:53.173Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types", marker = "python_full_version >= '3.11'" }, + { name = "pydantic-core", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/53/ea/bbe9095cdd771987d13c82d104a9c8559ae9aec1e29f139e286fd2e9256e/pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", size = 2028677, upload-time = "2025-04-23T18:32:27.227Z" }, + { url = "https://files.pythonhosted.org/packages/49/1d/4ac5ed228078737d457a609013e8f7edc64adc37b91d619ea965758369e5/pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", size = 1864735, upload-time = "2025-04-23T18:32:29.019Z" }, + { url = "https://files.pythonhosted.org/packages/23/9a/2e70d6388d7cda488ae38f57bc2f7b03ee442fbcf0d75d848304ac7e405b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", size = 1898467, upload-time = "2025-04-23T18:32:31.119Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2e/1568934feb43370c1ffb78a77f0baaa5a8b6897513e7a91051af707ffdc4/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", size = 1983041, upload-time = "2025-04-23T18:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/01/1a/1a1118f38ab64eac2f6269eb8c120ab915be30e387bb561e3af904b12499/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", size = 2136503, upload-time = "2025-04-23T18:32:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/5c/da/44754d1d7ae0f22d6d3ce6c6b1486fc07ac2c524ed8f6eca636e2e1ee49b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", size = 2736079, upload-time = "2025-04-23T18:32:37.659Z" }, + { url = "https://files.pythonhosted.org/packages/4d/98/f43cd89172220ec5aa86654967b22d862146bc4d736b1350b4c41e7c9c03/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", size = 2006508, upload-time = "2025-04-23T18:32:39.637Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cc/f77e8e242171d2158309f830f7d5d07e0531b756106f36bc18712dc439df/pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", size = 2113693, upload-time = "2025-04-23T18:32:41.818Z" }, + { url = "https://files.pythonhosted.org/packages/54/7a/7be6a7bd43e0a47c147ba7fbf124fe8aaf1200bc587da925509641113b2d/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", size = 2074224, upload-time = "2025-04-23T18:32:44.033Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/31cf8fadffbb03be1cb520850e00a8490c0927ec456e8293cafda0726184/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", size = 2245403, upload-time = "2025-04-23T18:32:45.836Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8d/bbaf4c6721b668d44f01861f297eb01c9b35f612f6b8e14173cb204e6240/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", size = 2242331, upload-time = "2025-04-23T18:32:47.618Z" }, + { url = "https://files.pythonhosted.org/packages/bb/93/3cc157026bca8f5006250e74515119fcaa6d6858aceee8f67ab6dc548c16/pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", size = 1910571, upload-time = "2025-04-23T18:32:49.401Z" }, + { url = "https://files.pythonhosted.org/packages/5b/90/7edc3b2a0d9f0dda8806c04e511a67b0b7a41d2187e2003673a996fb4310/pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", size = 1956504, upload-time = "2025-04-23T18:32:51.287Z" }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, + { url = "https://files.pythonhosted.org/packages/08/98/dbf3fdfabaf81cda5622154fda78ea9965ac467e3239078e0dcd6df159e7/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", size = 2024034, upload-time = "2025-04-23T18:33:32.843Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/7810aa9256e7f2ccd492590f86b79d370df1e9292f1f80b000b6a75bd2fb/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", size = 1858578, upload-time = "2025-04-23T18:33:34.912Z" }, + { url = "https://files.pythonhosted.org/packages/d8/60/bc06fa9027c7006cc6dd21e48dbf39076dc39d9abbaf718a1604973a9670/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", size = 1892858, upload-time = "2025-04-23T18:33:36.933Z" }, + { url = "https://files.pythonhosted.org/packages/f2/40/9d03997d9518816c68b4dfccb88969756b9146031b61cd37f781c74c9b6a/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", size = 2068498, upload-time = "2025-04-23T18:33:38.997Z" }, + { url = "https://files.pythonhosted.org/packages/d8/62/d490198d05d2d86672dc269f52579cad7261ced64c2df213d5c16e0aecb1/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", size = 2108428, upload-time = "2025-04-23T18:33:41.18Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ec/4cd215534fd10b8549015f12ea650a1a973da20ce46430b68fc3185573e8/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", size = 2069854, upload-time = "2025-04-23T18:33:43.446Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1a/abbd63d47e1d9b0d632fee6bb15785d0889c8a6e0a6c3b5a8e28ac1ec5d2/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", size = 2237859, upload-time = "2025-04-23T18:33:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/80/1c/fa883643429908b1c90598fd2642af8839efd1d835b65af1f75fba4d94fe/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", size = 2239059, upload-time = "2025-04-23T18:33:47.735Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661, upload-time = "2025-04-23T18:33:49.995Z" }, +] + +[[package]] +name = "pyflakes" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/07/92/f0cb5381f752e89a598dd2850941e7f570ac3cb8ea4a344854de486db152/pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3", size = 66388, upload-time = "2022-07-30T17:29:05.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/13/63178f59f74e53acc2165aee4b002619a3cfa7eeaeac989a9eb41edf364e/pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", size = 66116, upload-time = "2022-07-30T17:29:04.179Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788, upload-time = "2024-01-05T00:28:47.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.15" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pyyaml", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/92/a7296491dbf5585b3a987f3f3fc87af0e632121ff3e490c14b5f2d2b4eb5/pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7", size = 852320, upload-time = "2025-04-27T23:48:29.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/d1/c54e608505776ce4e7966d03358ae635cfd51dff1da6ee421c090dbc797b/pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f", size = 265845, upload-time = "2025-04-27T23:48:27.359Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyyaml", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/0a/c06b542ac108bfc73200677309cd9188a3a01b127a63f20cadc18d873d88/pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de", size = 853197, upload-time = "2025-06-21T17:56:36.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/d4/10bb14004d3c792811e05e21b5e5dcae805aacb739bd12a0540967b99592/pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2", size = 266143, upload-time = "2025-06-21T17:56:35.356Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, + { name = "iniconfig", marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "iniconfig", marker = "python_full_version >= '3.9'" }, + { name = "packaging", marker = "python_full_version >= '3.9'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pygments", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/75/20/6cd04d636a4c83458ecbb7c8220c13786a1a80d3f5fb568df39310e73e98/pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c", size = 8766775, upload-time = "2025-07-14T20:12:55.029Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6c/94c10268bae5d0d0c6509bdfb5aa08882d11a9ccdf89ff1cde59a6161afb/pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd", size = 9594743, upload-time = "2025-07-14T20:12:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218, upload-time = "2024-08-06T20:33:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067, upload-time = "2024-08-06T20:33:07.879Z" }, + { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812, upload-time = "2024-08-06T20:33:12.542Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531, upload-time = "2024-08-06T20:33:14.391Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820, upload-time = "2024-08-06T20:33:16.586Z" }, + { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514, upload-time = "2024-08-06T20:33:22.414Z" }, + { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702, upload-time = "2024-08-06T20:33:23.813Z" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "pyyaml", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631, upload-time = "2020-11-12T02:38:26.239Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911, upload-time = "2020-11-12T02:38:24.638Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "pyyaml", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "questionary" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/b8/d16eb579277f3de9e56e5ad25280fab52fc5774117fb70362e8c2e016559/questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587", size = 26775, upload-time = "2024-12-29T11:49:17.802Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/3f/11dd4cd4f39e05128bfd20138faea57bec56f9ffba6185d276e3107ba5b2/questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec", size = 36747, upload-time = "2024-12-29T11:49:16.734Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, +] + +[[package]] +name = "rich-argparse" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/a6/34460d81e5534f6d2fc8e8d91ff99a5835fdca53578eac89e4f37b3a7c6d/rich_argparse-1.7.1.tar.gz", hash = "sha256:d7a493cde94043e41ea68fb43a74405fa178de981bf7b800f7a3bd02ac5c27be", size = 38094, upload-time = "2025-05-25T20:20:35.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/f6/5fc0574af5379606ffd57a4b68ed88f9b415eb222047fe023aefcc00a648/rich_argparse-1.7.1-py3-none-any.whl", hash = "sha256:a8650b42e4a4ff72127837632fba6b7da40784842f08d7395eb67a9cbd7b4bf9", size = 25357, upload-time = "2025-05-25T20:20:33.793Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, + { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, + { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, + { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, + { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, + { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, + { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" }, + { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, +] + +[[package]] +name = "setuptools" +version = "75.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/01/771ea46cce201dd42cff043a5eea929d1c030fb3d1c2ee2729d02ca7814c/setuptools-75.3.2.tar.gz", hash = "sha256:3c1383e1038b68556a382c1e8ded8887cd20141b0eb5708a6c8d277de49364f5", size = 1354489, upload-time = "2025-03-12T00:02:19.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/65/3f0dba35760d902849d39d38c0a72767794b1963227b69a587f8a336d08c/setuptools-75.3.2-py3-none-any.whl", hash = "sha256:90ab613b6583fc02d5369cbca13ea26ea0e182d1df2d943ee9cbe81d4c61add9", size = 1251198, upload-time = "2025-03-12T00:02:17.554Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "setuptools-scm" +source = { editable = "." } +dependencies = [ + { name = "packaging" }, + { name = "setuptools", version = "75.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "setuptools", version = "80.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] + +[package.optional-dependencies] +rich = [ + { name = "rich" }, +] + +[package.dev-dependencies] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-entangled-plugin", version = "0.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "mkdocs-entangled-plugin", version = "0.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "mkdocs-include-markdown-plugin", version = "6.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs-include-markdown-plugin", version = "7.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", version = "0.26.1", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version < '3.9'" }, + { name = "mkdocstrings", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version >= '3.9'" }, + { name = "pygments" }, +] +test = [ + { name = "build" }, + { name = "flake8", version = "5.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, + { name = "flake8", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, + { name = "flake8", version = "7.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "griffe", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "griffe", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mypy" }, + { name = "pip", version = "25.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pip", version = "25.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest-timeout" }, + { name = "rich" }, + { name = "ruff" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "wheel" }, +] + +[package.metadata] +requires-dist = [ + { name = "packaging", specifier = ">=20" }, + { name = "rich", marker = "extra == 'rich'" }, + { name = "setuptools" }, + { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +provides-extras = ["rich", "simple", "toml"] + +[package.metadata.requires-dev] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-entangled-plugin" }, + { name = "mkdocs-include-markdown-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extras = ["python"] }, + { name = "pygments" }, +] +test = [ + { name = "build" }, + { name = "flake8" }, + { name = "griffe" }, + { name = "mypy", specifier = "~=1.13.0" }, + { name = "pip" }, + { name = "pytest" }, + { name = "pytest-timeout" }, + { name = "rich" }, + { name = "ruff" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "wheel" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, +] + +[[package]] +name = "tomlkit" +version = "0.12.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/ab/18f4c8f2bec75eb1a7aebcc52cdb02ab04fd39ff7025bb1b1c7846cc45b8/tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c", size = 191420, upload-time = "2024-05-08T13:50:19.363Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/6d/b5406752c4e4ba86692b22fab0afed8b48f16bdde8f92e1d852976b61dc6/tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f", size = 37685, upload-time = "2024-05-08T13:50:17.343Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "watchdog" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/a6/d6ef450393dac5734c63c40a131f66808d2e6f59f6165ab38c98fbe4e6ec/watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9", size = 124593, upload-time = "2023-03-20T09:21:11.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/fd/58b82550ebe4883bb2a5e1b6c14d8702b5ce0f36c58470bba51dc777df46/watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41", size = 100697, upload-time = "2023-03-20T09:20:25.047Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/42f47ffdfadff4c41b89c54163f323f875eb963bf90088e477c43b8f7b15/watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397", size = 91219, upload-time = "2023-03-20T09:20:26.864Z" }, + { url = "https://files.pythonhosted.org/packages/9b/39/30bb3c2e4f8e89b5c60e98589acf5c5a001cb0efde249aa05d748d1734a2/watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96", size = 91756, upload-time = "2023-03-20T09:20:28.309Z" }, + { url = "https://files.pythonhosted.org/packages/00/9e/a9711f35f1ad6571e92dc2e955e7de9dfac21a1b33e9cd212f066a60a387/watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae", size = 100700, upload-time = "2023-03-20T09:20:29.847Z" }, + { url = "https://files.pythonhosted.org/packages/84/ab/67001e62603bf2ea35ace40023f7c74f61e8b047160d6bb078373cec1a67/watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9", size = 91251, upload-time = "2023-03-20T09:20:31.892Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/d419fdbd3051b42b0a8091ddf78f70540b6d9d277a84845f7c5955f9de92/watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7", size = 91753, upload-time = "2023-03-20T09:20:33.337Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6e/7ca8ed16928d7b11da69372f55c64a09dce649d2b24b03f7063cd8683c4b/watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f", size = 100655, upload-time = "2023-03-20T09:20:37.473Z" }, + { url = "https://files.pythonhosted.org/packages/2e/54/48527f3aea4f7ed331072352fee034a7f3d6ec7a2ed873681738b2586498/watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc", size = 91216, upload-time = "2023-03-20T09:20:39.793Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/3a3ce6dd01807ff918aec3bbcabc92ed1a7edc5bb2266c720bb39fec1bec/watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3", size = 91752, upload-time = "2023-03-20T09:20:41.395Z" }, + { url = "https://files.pythonhosted.org/packages/75/fe/d9a37d8df76878853f68dd665ec6d2c7a984645de460164cb880a93ffe6b/watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3", size = 100653, upload-time = "2023-03-20T09:20:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/94/ce/70c65a6c4b0330129c402624d42f67ce82d6a0ba2036de67628aeffda3c1/watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0", size = 91247, upload-time = "2023-03-20T09:20:45.157Z" }, + { url = "https://files.pythonhosted.org/packages/51/b9/444a984b1667013bac41b31b45d9718e069cc7502a43a924896806605d83/watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8", size = 91753, upload-time = "2023-03-20T09:20:46.913Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/bef1c6f6ac18041234a9f3e8bc995d611e255c44f10433bfaf255968c269/watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346", size = 90419, upload-time = "2023-03-20T09:20:50.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/65/9e36a3c821d47a22e54a8fc73681586b2d26e82d24ea3af63acf2ef78f97/watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64", size = 90428, upload-time = "2023-03-20T09:20:52.216Z" }, + { url = "https://files.pythonhosted.org/packages/92/28/631872d7fbc45527037060db8c838b47a129a6c09d2297d6dddcfa283cf2/watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a", size = 82049, upload-time = "2023-03-20T09:20:53.951Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a2/4e3230bdc1fb878b152a2c66aa941732776f4545bd68135d490591d66713/watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44", size = 82049, upload-time = "2023-03-20T09:20:55.583Z" }, + { url = "https://files.pythonhosted.org/packages/21/72/46fd174352cd88b9157ade77e3b8835125d4b1e5186fc7f1e8c44664e029/watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a", size = 82052, upload-time = "2023-03-20T09:20:57.124Z" }, + { url = "https://files.pythonhosted.org/packages/74/3c/e4b77f4f069aca2b6e35925db7a1aa6cb600dcb52fc3e962284640ca37f3/watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709", size = 82050, upload-time = "2023-03-20T09:20:58.864Z" }, + { url = "https://files.pythonhosted.org/packages/71/3a/b12740f4f60861240d57b42a2ac6ac0a2821db506c4435f7872c1fad867d/watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83", size = 82050, upload-time = "2023-03-20T09:21:00.452Z" }, + { url = "https://files.pythonhosted.org/packages/40/1b/4e6d3e0f587587931f590531b4ed08070d71a9efb35541d792a68d8ee593/watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d", size = 82049, upload-time = "2023-03-20T09:21:01.979Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f0/456948b865ab259784f774154e7d65844fa9757522fdb11533fbf8ae7aca/watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33", size = 82051, upload-time = "2023-03-20T09:21:03.67Z" }, + { url = "https://files.pythonhosted.org/packages/55/0d/bfc2a0d425b12444a2dc245a934c065bbb7bd9833fff071cba79c21bb76e/watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f", size = 82038, upload-time = "2023-03-20T09:21:05.492Z" }, + { url = "https://files.pythonhosted.org/packages/9b/6e/ce8d124d03cd3f2941365d9c81d62e3afe43f2dc7e6e86274fa9c2ec2d5b/watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c", size = 82040, upload-time = "2023-03-20T09:21:07.609Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/cd0337069c468f22ef256e768ece74c78b511092f1004ab260268e1af4a9/watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759", size = 82040, upload-time = "2023-03-20T09:21:09.178Z" }, +] + +[[package]] +name = "wcmatch" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "bracex", version = "2.5.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/ab/b3a52228538ccb983653c446c1656eddf1d5303b9cb8b9aef6a91299f862/wcmatch-10.0.tar.gz", hash = "sha256:e72f0de09bba6a04e0de70937b0cf06e55f36f37b3deb422dfaf854b867b840a", size = 115578, upload-time = "2024-09-26T18:39:52.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/df/4ee467ab39cc1de4b852c212c1ed3becfec2e486a51ac1ce0091f85f38d7/wcmatch-10.0-py3-none-any.whl", hash = "sha256:0dd927072d03c0a6527a20d2e6ad5ba8d0380e60870c383bc533b71744df7b7a", size = 39347, upload-time = "2024-09-26T18:39:51.002Z" }, +] + +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "bracex", version = "2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199, upload-time = "2024-09-13T13:44:16.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200, upload-time = "2024-09-13T13:44:14.38Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +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" } +wheels = [ + { 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" }, +]