diff --git a/.cursor/rules/test-running.mdc b/.cursor/rules/test-running.mdc
deleted file mode 100644
index 201f9af6..00000000
--- a/.cursor/rules/test-running.mdc
+++ /dev/null
@@ -1,12 +0,0 @@
----
-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/README.md b/.github/workflows/README.md
new file mode 100644
index 00000000..43da3962
--- /dev/null
+++ b/.github/workflows/README.md
@@ -0,0 +1,150 @@
+# Reusable Workflows for Towncrier-based Releases
+
+This directory contains reusable GitHub Actions workflows that other projects can use to implement the same towncrier-based release process.
+
+## Available Reusable Workflows
+
+### `reusable-towncrier-release.yml`
+
+Determines the next version using the `towncrier-fragments` version scheme and builds the changelog.
+
+**Inputs:**
+- `project_name` (required): Name of the project (used for labeling and tag prefix)
+- `project_directory` (required): Directory containing the project (relative to repository root)
+
+**Outputs:**
+- `version`: The determined next version
+- `has_fragments`: Whether fragments were found
+
+**Behavior:**
+- ✅ Strict validation - workflow fails if changelog fragments or version data is missing
+- ✅ No fallback values - ensures data integrity for releases
+- ✅ Clear error messages to guide troubleshooting
+
+**Example usage:**
+
+```yaml
+jobs:
+ determine-version:
+ uses: pypa/setuptools-scm/.github/workflows/reusable-towncrier-release.yml@main
+ with:
+ project_name: my-project
+ project_directory: ./
+```
+
+## Using These Workflows in Your Project
+
+### Prerequisites
+
+1. **Add vcs-versioning dependency** to your project
+2. **Configure towncrier** in your `pyproject.toml`:
+
+```toml
+[tool.towncrier]
+directory = "changelog.d"
+filename = "CHANGELOG.md"
+start_string = "\n"
+template = "changelog.d/template.md"
+title_format = "## {version} ({project_date})"
+
+[[tool.towncrier.type]]
+directory = "removal"
+name = "Removed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "feature"
+name = "Added"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "bugfix"
+name = "Fixed"
+showcontent = true
+```
+
+3. **Create changelog structure**:
+ - `changelog.d/` directory
+ - `changelog.d/template.md` (towncrier template)
+ - `CHANGELOG.md` with the start marker
+
+4. **Add the version scheme entry point** (if using vcs-versioning):
+
+The `towncrier-fragments` version scheme is provided by vcs-versioning 0.2.0+.
+
+### Complete Example Workflow
+
+```yaml
+name: Create Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ create_release:
+ description: 'Create release'
+ required: true
+ type: boolean
+ default: false
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ determine-version:
+ uses: pypa/setuptools-scm/.github/workflows/reusable-towncrier-release.yml@main
+ with:
+ project_name: my-project
+ project_directory: ./
+
+ create-release-pr:
+ needs: determine-version
+ if: needs.determine-version.outputs.has_fragments == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Download changelog artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: changelog-my-project
+
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@v7
+ with:
+ commit-message: "Release v${{ needs.determine-version.outputs.version }}"
+ branch: release-${{ needs.determine-version.outputs.version }}
+ title: "Release v${{ needs.determine-version.outputs.version }}"
+ labels: release:my-project
+ body: |
+ Automated release PR for version ${{ needs.determine-version.outputs.version }}
+```
+
+## Architecture
+
+The workflow system is designed with these principles:
+
+1. **Version scheme is single source of truth** - No version calculation in scripts
+2. **Reusable components** - Other projects can use the same workflows
+3. **Manual approval** - Release PRs must be reviewed and merged
+4. **Project-prefixed tags** - Enable monorepo releases (`project-vX.Y.Z`)
+5. **Automated but controlled** - Automation with human approval gates
+6. **Fail fast** - No fallback values; workflows fail explicitly if required data is missing
+7. **No custom scripts** - Uses PR title parsing and built-in tools only
+
+## Version Bump Logic
+
+The `towncrier-fragments` version scheme determines bumps based on fragment types:
+
+| Fragment Type | Version Bump | Example |
+|---------------|--------------|---------|
+| `removal` | Major (X.0.0) | Breaking changes |
+| `feature`, `deprecation` | Minor (0.X.0) | New features |
+| `bugfix`, `doc`, `misc` | Patch (0.0.X) | Bug fixes |
+
+## Support
+
+For issues or questions about these workflows:
+- Open an issue at https://github.com/pypa/setuptools-scm/issues
+- See full documentation in [CONTRIBUTING.md](../../CONTRIBUTING.md)
+
diff --git a/.github/workflows/api-check.yml b/.github/workflows/api-check.yml
index 4db2526b..b39747ae 100644
--- a/.github/workflows/api-check.yml
+++ b/.github/workflows/api-check.yml
@@ -28,52 +28,22 @@ jobs:
with:
python-version: '3.11'
- - name: Install dependencies
- run: |
- pip install -U pip setuptools
- pip install -e .[test]
- pip install griffe
+ - name: Install the latest version of uv
+ uses: astral-sh/setup-uv@v6
- - name: Run griffe API check
- id: griffe-check
- continue-on-error: true
+ - name: Get latest release tag
+ id: latest-tag
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
+ # Get the latest git tag (griffe needs a git ref)
+ LATEST_TAG=$(git describe --tags --abbrev=0 origin/main 2>/dev/null || echo "v9.2.1")
+ echo "Latest release tag: $LATEST_TAG"
+ echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
- - 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 }}'
+ - name: Install dependencies
+ run: uv sync --all-packages --all-groups
- 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
+ - name: Check API stability against latest release
+ run: |
+ echo "Comparing current code against tag: ${{ steps.latest-tag.outputs.tag }}"
+ # Use local check_api.py script which includes griffe-public-wildcard-imports extension
+ uv run --no-sync python setuptools-scm/check_api.py --against ${{ steps.latest-tag.outputs.tag }}
diff --git a/.github/workflows/branch-sync.yml b/.github/workflows/branch-sync.yml
new file mode 100644
index 00000000..2ae17107
--- /dev/null
+++ b/.github/workflows/branch-sync.yml
@@ -0,0 +1,361 @@
+name: Branch Synchronization
+
+# Synchronize branches after successful releases:
+# - Triggers after upload jobs complete
+# - Verifies all expected releases for the commit succeeded
+# - Creates sync PRs between branches
+
+on:
+ workflow_run:
+ workflows: ["python tests+artifacts+release"]
+ types: [completed]
+ branches: [main, develop]
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ # Verify all releases completed and sync branches
+ sync-after-release:
+ # Only run if the triggering workflow succeeded and was triggered by a tag push
+ if: |
+ github.event.workflow_run.conclusion == 'success' &&
+ startsWith(github.event.workflow_run.head_branch, 'setuptools-scm-v') ||
+ startsWith(github.event.workflow_run.head_branch, 'vcs-versioning-v')
+ runs-on: ubuntu-latest
+ steps:
+ - name: Verify releases and create sync PR
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const workflowRun = context.payload.workflow_run;
+ const headSha = workflowRun.head_sha;
+ const headBranch = workflowRun.head_branch;
+
+ console.log(`Workflow completed for: ${headBranch} at ${headSha}`);
+
+ // Get all tags pointing to this commit
+ const { data: tagsResponse } = await github.rest.repos.listTags({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ per_page: 100
+ });
+
+ const commitTags = tagsResponse.filter(tag => tag.commit.sha === headSha);
+ console.log(`Tags at commit ${headSha}: ${commitTags.map(t => t.name).join(', ') || 'none'}`);
+
+ if (commitTags.length === 0) {
+ console.log('No tags found at this commit, nothing to sync');
+ return;
+ }
+
+ // Check which packages have tags at this commit
+ const setupToolsTag = commitTags.find(t => t.name.startsWith('setuptools-scm-v'));
+ const vcsVersioningTag = commitTags.find(t => t.name.startsWith('vcs-versioning-v'));
+
+ console.log(`setuptools-scm tag: ${setupToolsTag?.name || 'none'}`);
+ console.log(`vcs-versioning tag: ${vcsVersioningTag?.name || 'none'}`);
+
+ // Verify all expected releases have GitHub releases (created after successful upload)
+ const releasesToVerify = [];
+ if (setupToolsTag) releasesToVerify.push(setupToolsTag.name);
+ if (vcsVersioningTag) releasesToVerify.push(vcsVersioningTag.name);
+
+ for (const tagName of releasesToVerify) {
+ try {
+ const { data: release } = await github.rest.repos.getReleaseByTag({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ tag: tagName
+ });
+ console.log(`✓ Release exists for ${tagName}: ${release.html_url}`);
+ } catch (error) {
+ if (error.status === 404) {
+ console.log(`✗ Release not found for ${tagName}, waiting for all releases to complete`);
+ return;
+ }
+ throw error;
+ }
+ }
+
+ console.log('All expected releases verified successfully');
+
+ // Determine which branch this commit is on
+ // Check if this commit is on develop (for develop→main sync)
+ let isDevelopRelease = false;
+ try {
+ const { data: developBranch } = await github.rest.repos.getBranch({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ branch: 'develop'
+ });
+
+ // Check if this commit is an ancestor of develop
+ const { data: comparison } = await github.rest.repos.compareCommits({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ base: headSha,
+ head: 'develop'
+ });
+
+ // If develop is at or ahead of this commit, it's a develop release
+ isDevelopRelease = comparison.status === 'identical' || comparison.status === 'ahead';
+ console.log(`Commit is on develop: ${isDevelopRelease}`);
+ } catch (error) {
+ if (error.status === 404) {
+ console.log('develop branch does not exist');
+ } else {
+ throw error;
+ }
+ }
+
+ if (!isDevelopRelease) {
+ console.log('This is a main branch release, no sync needed (main→develop sync happens on PR merge)');
+ return;
+ }
+
+ // For develop releases, create sync PR to main
+ console.log('Creating sync PR from develop release to main');
+
+ // Use short commit SHA for branch name (simple and unique)
+ const tempBranchName = `sync/develop-to-main-${headSha.substring(0, 8)}`;
+
+ console.log(`Creating temporary branch ${tempBranchName} from commit ${headSha}`);
+
+ // Check if the commit has changes compared to main
+ const mainComparison = await github.rest.repos.compareCommits({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ base: 'main',
+ head: headSha
+ });
+
+ if (mainComparison.data.ahead_by === 0) {
+ console.log('Commit has no new changes for main, skipping');
+ return;
+ }
+
+ console.log(`Commit has ${mainComparison.data.ahead_by} commits not on main`);
+
+ // Check for existing sync PR
+ const existingPRs = await github.rest.pulls.list({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ head: `${context.repo.owner}:${tempBranchName}`,
+ base: 'main'
+ });
+
+ if (existingPRs.data.length > 0) {
+ console.log(`Sync PR already exists: #${existingPRs.data[0].number}`);
+ return;
+ }
+
+ // Create temporary branch from the exact commit SHA
+ try {
+ await github.rest.git.createRef({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ ref: `refs/heads/${tempBranchName}`,
+ sha: headSha
+ });
+ console.log(`Created temporary branch ${tempBranchName}`);
+ } catch (error) {
+ if (error.status === 422) {
+ console.log(`Branch ${tempBranchName} already exists`);
+ } else {
+ throw error;
+ }
+ }
+
+ // Build release info for PR body
+ const releaseInfo = releasesToVerify.map(tag => `- \`${tag}\``).join('\n');
+
+ // Build PR body
+ const body = [
+ '## Branch Synchronization',
+ '',
+ 'This PR syncs the release from `develop` to `main`.',
+ '',
+ '**Released tags:**',
+ releaseInfo,
+ '',
+ `**Commit:** ${headSha}`,
+ `**Temporary branch:** \`${tempBranchName}\``,
+ '',
+ 'This PR uses a temporary branch created from the exact release commit',
+ 'to ensure only the release changes are included (no extra commits).',
+ '',
+ 'All PyPI uploads have been verified successful before creating this PR.',
+ '',
+ 'This is an automated PR created by the branch-sync workflow.',
+ 'If there are no conflicts, this PR will be auto-merged.',
+ '',
+ 'The temporary branch will be deleted when this PR is merged or closed.'
+ ].join('\n');
+
+ // Build title with tag names
+ const title = `Sync: develop → main (${releasesToVerify.join(', ')})`;
+
+ // Create new sync PR from the temporary branch
+ const { data: pr } = await github.rest.pulls.create({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: title,
+ body: body,
+ head: tempBranchName,
+ base: 'main'
+ });
+
+ console.log(`Created sync PR #${pr.number}`);
+
+ // Add labels
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ labels: ['sync', 'auto-merge']
+ });
+
+ // Try to enable auto-merge
+ try {
+ await github.graphql(`
+ mutation($pullRequestId: ID!) {
+ enablePullRequestAutoMerge(input: {pullRequestId: $pullRequestId, mergeMethod: MERGE}) {
+ pullRequest {
+ autoMergeRequest {
+ enabledAt
+ }
+ }
+ }
+ }
+ `, {
+ pullRequestId: pr.node_id
+ });
+ console.log('Auto-merge enabled');
+ } catch (error) {
+ console.log('Could not enable auto-merge (may require branch protection rules):', error.message);
+ }
+
+ # Forward-port: main → develop
+ # When a release PR is merged to main, create PR to keep develop in sync
+ sync-main-to-develop:
+ if: |
+ github.event.workflow_run.conclusion == 'success' &&
+ (startsWith(github.event.workflow_run.head_branch, 'setuptools-scm-v') ||
+ startsWith(github.event.workflow_run.head_branch, 'vcs-versioning-v'))
+ runs-on: ubuntu-latest
+ steps:
+ - name: Create PR main → develop if needed
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const workflowRun = context.payload.workflow_run;
+ const headSha = workflowRun.head_sha;
+
+ // Check if develop branch exists
+ let developExists = true;
+ try {
+ await github.rest.repos.getBranch({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ branch: 'develop'
+ });
+ } catch (error) {
+ if (error.status === 404) {
+ console.log('develop branch does not exist, skipping');
+ developExists = false;
+ } else {
+ throw error;
+ }
+ }
+
+ if (!developExists) return;
+
+ // Check if this commit is on main but not on develop
+ const { data: mainBranch } = await github.rest.repos.getBranch({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ branch: 'main'
+ });
+
+ // Check if commit is an ancestor of main
+ let isMainRelease = false;
+ try {
+ const { data: comparison } = await github.rest.repos.compareCommits({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ base: headSha,
+ head: 'main'
+ });
+ isMainRelease = comparison.status === 'identical' || comparison.status === 'ahead';
+ } catch {
+ isMainRelease = false;
+ }
+
+ if (!isMainRelease) {
+ console.log('This is not a main branch release, skipping main→develop sync');
+ return;
+ }
+
+ // Check if main has commits that develop doesn't have
+ const comparison = await github.rest.repos.compareCommits({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ base: 'develop',
+ head: 'main'
+ });
+
+ if (comparison.data.ahead_by === 0) {
+ console.log('main has no new commits for develop, skipping');
+ return;
+ }
+
+ console.log(`main has ${comparison.data.ahead_by} commits not on develop`);
+
+ // Check for existing sync PR
+ const existingPRs = await github.rest.pulls.list({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ head: `${context.repo.owner}:main`,
+ base: 'develop'
+ });
+
+ if (existingPRs.data.length > 0) {
+ console.log(`Sync PR already exists: #${existingPRs.data[0].number}`);
+ return;
+ }
+
+ // Build PR body
+ const body = [
+ '## Branch Synchronization',
+ '',
+ 'This PR syncs the release from `main` to `develop`.',
+ '',
+ `**Commit:** ${headSha}`,
+ '',
+ 'This is an automated PR created by the branch-sync workflow.',
+ 'Review and merge to keep `develop` up to date with `main`.'
+ ].join('\n');
+
+ // Create new sync PR
+ const { data: pr } = await github.rest.pulls.create({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: `Sync: main → develop`,
+ body: body,
+ head: 'main',
+ base: 'develop'
+ });
+
+ console.log(`Created sync PR #${pr.number}`);
+
+ // Add label
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ labels: ['sync']
+ });
diff --git a/.github/workflows/create-release-tags.yml b/.github/workflows/create-release-tags.yml
new file mode 100644
index 00000000..1672916f
--- /dev/null
+++ b/.github/workflows/create-release-tags.yml
@@ -0,0 +1,158 @@
+name: Create Release Tags
+
+on:
+ pull_request:
+ types: [closed]
+ branches:
+ - main
+ - develop
+
+permissions:
+ contents: write
+
+jobs:
+ create-tags:
+ # Only run if PR was merged and has release labels
+ if: |
+ github.event.pull_request.merged == true &&
+ (contains(github.event.pull_request.labels.*.name, 'release:setuptools-scm') ||
+ contains(github.event.pull_request.labels.*.name, 'release:vcs-versioning'))
+ runs-on: ubuntu-latest
+ steps:
+ - name: Create tags and releases
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const pr = context.payload.pull_request;
+ const prTitle = pr.title;
+ const mergeCommitSha = pr.merge_commit_sha;
+ const labels = pr.labels.map(l => l.name);
+
+ console.log(`Processing PR #${pr.number}: ${prTitle}`);
+ console.log(`Merge commit: ${mergeCommitSha}`);
+ console.log(`Labels: ${labels.join(', ')}`);
+
+ const tagsCreated = [];
+
+ // Helper to extract version from PR title
+ function extractVersion(title, packageName) {
+ const regex = new RegExp(`${packageName} v(\\d+\\.\\d+\\.\\d+)`);
+ const match = title.match(regex);
+ return match ? match[1] : null;
+ }
+
+ // Helper to extract changelog section for a version
+ async function extractChangelog(packageDir, version) {
+ try {
+ const { data: file } = await github.rest.repos.getContent({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ path: `${packageDir}/CHANGELOG.md`,
+ ref: mergeCommitSha
+ });
+
+ const content = Buffer.from(file.content, 'base64').toString('utf-8');
+ const lines = content.split('\n');
+
+ let inSection = false;
+ let changelog = [];
+
+ for (const line of lines) {
+ if (line.startsWith(`## ${version}`)) {
+ inSection = true;
+ continue; // Skip the header line
+ }
+ if (inSection && line.match(/^## \d/)) {
+ break; // Next version section
+ }
+ if (inSection) {
+ changelog.push(line);
+ }
+ }
+
+ // Trim leading/trailing empty lines
+ while (changelog.length > 0 && changelog[0].trim() === '') {
+ changelog.shift();
+ }
+ while (changelog.length > 0 && changelog[changelog.length - 1].trim() === '') {
+ changelog.pop();
+ }
+
+ return changelog.join('\n');
+ } catch (error) {
+ console.log(`Could not extract changelog: ${error.message}`);
+ return `Release ${version}`;
+ }
+ }
+
+ // Helper to create tag and release
+ async function createTagAndRelease(packageName, packageDir, tagPrefix) {
+ const version = extractVersion(prTitle, packageName);
+ if (!version) {
+ throw new Error(`Failed to extract ${packageName} version from PR title: ${prTitle}`);
+ }
+
+ const tagName = `${tagPrefix}-v${version}`;
+ console.log(`Creating tag: ${tagName} at ${mergeCommitSha}`);
+
+ // Create annotated tag via API
+ const { data: tagObject } = await github.rest.git.createTag({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ tag: tagName,
+ message: `Release ${packageName} v${version}`,
+ object: mergeCommitSha,
+ type: 'commit',
+ tagger: {
+ name: 'github-actions[bot]',
+ email: 'github-actions[bot]@users.noreply.github.com',
+ date: new Date().toISOString()
+ }
+ });
+
+ // Create ref for the tag
+ await github.rest.git.createRef({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ ref: `refs/tags/${tagName}`,
+ sha: tagObject.sha
+ });
+
+ console.log(`Tag ${tagName} created`);
+ tagsCreated.push(tagName);
+
+ // Extract changelog
+ const changelog = await extractChangelog(packageDir, version);
+
+ // Create GitHub release
+ const { data: release } = await github.rest.repos.createRelease({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ tag_name: tagName,
+ name: `${packageName} v${version}`,
+ body: changelog,
+ draft: false,
+ prerelease: false
+ });
+
+ console.log(`Release created: ${release.html_url}`);
+ return { tagName, version };
+ }
+
+ // Process setuptools-scm
+ if (labels.includes('release:setuptools-scm')) {
+ console.log('\n--- Processing setuptools-scm ---');
+ await createTagAndRelease('setuptools-scm', 'setuptools-scm', 'setuptools-scm');
+ }
+
+ // Process vcs-versioning
+ if (labels.includes('release:vcs-versioning')) {
+ console.log('\n--- Processing vcs-versioning ---');
+ await createTagAndRelease('vcs-versioning', 'vcs-versioning', 'vcs-versioning');
+ }
+
+ // Write summary
+ const summary = `## Tags Created\n\n${tagsCreated.map(t => `- \`${t}\``).join('\n')}\n\nPyPI upload will be triggered automatically by tag push.`;
+ await core.summary.addRaw(summary).write();
+
+ console.log(`\nDone! Created tags: ${tagsCreated.join(', ')}`);
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index 17953d55..ad03707d 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -6,7 +6,8 @@ on:
branches:
- "*"
tags:
- - "v*"
+ - "setuptools-scm-v*"
+ - "vcs-versioning-v*"
release:
types: [published]
@@ -20,8 +21,13 @@ env:
jobs:
package:
- name: Build & inspect our package.
+ name: Build & inspect ${{ matrix.package }}
runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ package:
+ - vcs-versioning
+ - setuptools-scm
env:
# Use no-local-version for package builds to ensure clean versions for PyPI uploads
SETUPTOOLS_SCM_NO_LOCAL: "1"
@@ -31,7 +37,11 @@ jobs:
with:
fetch-depth: 0
- - uses: hynek/build-and-inspect-python-package@v2
+ - name: Build ${{ matrix.package }}
+ uses: hynek/build-and-inspect-python-package@v2
+ with:
+ path: ${{ matrix.package }}
+ upload-name-suffix: -${{ matrix.package }}
test:
needs: [package]
@@ -39,7 +49,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python_version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10' ]
+ python_version: [ '3.10', '3.11', '3.12', '3.13', 'pypy-3.10' ]
os: [windows-latest, ubuntu-latest] #, macos-latest]
include:
- os: windows-latest
@@ -95,46 +105,93 @@ jobs:
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: uv sync --group test --group docs --extra rich
- - uses: actions/download-artifact@v5
+ - run: uv sync --all-packages --all-groups
+ - name: Download vcs-versioning packages
+ uses: actions/download-artifact@v4
+ with:
+ name: Packages-vcs-versioning
+ path: dist
+ - name: Download setuptools-scm packages
+ uses: actions/download-artifact@v4
with:
- name: Packages
+ name: Packages-setuptools-scm
path: dist
- - shell: bash
- run: uv pip install "$(echo -n dist/*whl)"
+ - name: Install built wheels
+ shell: bash
+ run: |
+ # Install vcs-versioning first (dependency of setuptools-scm)
+ uv pip install dist/vcs_versioning-*.whl
+ # Then install setuptools-scm
+ uv pip install dist/setuptools_scm-*.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: uv run pytest
+ shell: bash
+ - name: Run tests for both packages
+ run: uv run --no-sync pytest setuptools-scm/testing_scm/ vcs-versioning/testing_vcs/
timeout-minutes: 25
- dist_upload:
+ dist_upload_setuptools_scm:
+ runs-on: ubuntu-latest
+ if: (github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/setuptools-scm-v')) || (github.event_name == 'release' && github.event.action == 'published' && startsWith(github.event.release.tag_name, 'setuptools-scm-v'))
+ permissions:
+ id-token: write
+ needs: [test]
+ steps:
+ - name: Download setuptools-scm packages
+ uses: actions/download-artifact@v4
+ with:
+ name: Packages-setuptools-scm
+ path: dist
+ - name: Publish setuptools-scm to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ dist_upload_vcs_versioning:
runs-on: ubuntu-latest
- if: (github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')) || (github.event_name == 'release' && github.event.action == 'published')
+ if: (github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/vcs-versioning-v')) || (github.event_name == 'release' && github.event.action == 'published' && startsWith(github.event.release.tag_name, 'vcs-versioning-v'))
permissions:
id-token: write
needs: [test]
steps:
- - uses: actions/download-artifact@v5
+ - name: Download vcs-versioning packages
+ uses: actions/download-artifact@v4
with:
- name: Packages
+ name: Packages-vcs-versioning
path: dist
- - name: Publish package to PyPI
+ - name: Publish vcs-versioning to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- upload-release-assets:
+ upload-release-assets-setuptools-scm:
runs-on: ubuntu-latest
- if: github.event_name == 'release' && github.event.action == 'published'
+ if: github.event_name == 'release' && github.event.action == 'published' && startsWith(github.event.release.tag_name, 'setuptools-scm-v')
needs: [test]
permissions:
contents: write
steps:
- - uses: actions/download-artifact@v5
+ - name: Download setuptools-scm packages
+ uses: actions/download-artifact@v4
with:
- name: Packages
+ name: Packages-setuptools-scm
+ path: dist
+ - name: Upload release assets
+ uses: softprops/action-gh-release@v2
+ with:
+ files: dist/*
+ fail_on_unmatched_files: true
+
+ upload-release-assets-vcs-versioning:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'release' && github.event.action == 'published' && startsWith(github.event.release.tag_name, 'vcs-versioning-v')
+ needs: [test]
+ permissions:
+ contents: write
+ steps:
+ - name: Download vcs-versioning packages
+ uses: actions/download-artifact@v4
+ with:
+ name: Packages-vcs-versioning
path: dist
- name: Upload release assets
uses: softprops/action-gh-release@v2
@@ -149,9 +206,15 @@ jobs:
permissions:
id-token: write
steps:
- - uses: actions/download-artifact@v5
+ - name: Download vcs-versioning packages
+ uses: actions/download-artifact@v4
+ with:
+ name: Packages-vcs-versioning
+ path: dist
+ - name: Download setuptools-scm packages
+ uses: actions/download-artifact@v4
with:
- name: Packages
+ name: Packages-setuptools-scm
path: dist
- name: Publish package to PyPI
continue-on-error: true
diff --git a/.github/workflows/release-proposal.yml b/.github/workflows/release-proposal.yml
new file mode 100644
index 00000000..03e1433a
--- /dev/null
+++ b/.github/workflows/release-proposal.yml
@@ -0,0 +1,120 @@
+name: Create Release Proposal
+
+on:
+ push:
+ branches:
+ - main
+ - develop
+ pull_request:
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ create-release-pr:
+ 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 uv
+ uses: astral-sh/setup-uv@v6
+
+ - name: Install dependencies
+ run: |
+ uv sync --all-packages --all-groups
+
+ - name: Configure git
+ if: github.event_name == 'push'
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Run release proposal
+ id: release
+ run: |
+ uv run create-release-proposal \
+ --event "${{ github.event_name }}" \
+ --branch "${{ github.ref_name }}"
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+
+ - name: Create or update release branch
+ if: github.event_name == 'push'
+ run: |
+ # Get release branch from script output
+ RELEASE_BRANCH="${{ steps.release.outputs.release_branch }}"
+ RELEASES="${{ steps.release.outputs.releases }}"
+
+ # Checkout release branch (force)
+ git checkout -B "$RELEASE_BRANCH"
+
+ # Commit towncrier changes
+ git add -A
+ git commit -m "Prepare release: $RELEASES" || echo "No changes to commit"
+
+ # Force push
+ git push origin "$RELEASE_BRANCH" --force
+
+ - name: Create or update PR
+ if: github.event_name == 'push'
+ uses: actions/github-script@v8
+ env:
+ RELEASE_BRANCH: ${{ steps.release.outputs.release_branch }}
+ PR_BASE: ${{ steps.release.outputs.pr_base }}
+ PR_EXISTS: ${{ steps.release.outputs.pr_exists }}
+ PR_NUMBER: ${{ steps.release.outputs.pr_number }}
+ PR_TITLE: ${{ steps.release.outputs.pr_title }}
+ PR_BODY: ${{ steps.release.outputs.pr_body }}
+ LABELS: ${{ steps.release.outputs.labels }}
+ with:
+ script: |
+ const releaseBranch = process.env.RELEASE_BRANCH;
+ const prBase = process.env.PR_BASE;
+ const prExists = process.env.PR_EXISTS === 'true';
+ const prNumber = process.env.PR_NUMBER;
+ const prTitle = process.env.PR_TITLE;
+ const prBody = process.env.PR_BODY;
+ const labels = process.env.LABELS.split(',').filter(l => l);
+
+ if (prExists && prNumber) {
+ // Update existing PR (including base branch if it changed)
+ console.log(`Updating existing PR #${prNumber} to target ${prBase}`);
+ await github.rest.pulls.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: parseInt(prNumber),
+ title: prTitle,
+ body: prBody,
+ base: prBase
+ });
+ } else {
+ // Create new PR - targets the same branch it came from
+ console.log(`Creating new PR targeting ${prBase}`);
+ const { data: pr } = await github.rest.pulls.create({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: prTitle,
+ body: prBody,
+ head: releaseBranch,
+ base: prBase
+ });
+ console.log(`Created PR #${pr.number}`);
+
+ // Add labels
+ if (labels.length > 0) {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ labels: labels
+ });
+ }
+ }
diff --git a/.github/workflows/reusable-towncrier-release.yml b/.github/workflows/reusable-towncrier-release.yml
new file mode 100644
index 00000000..1867b0c2
--- /dev/null
+++ b/.github/workflows/reusable-towncrier-release.yml
@@ -0,0 +1,121 @@
+name: Reusable Towncrier Release
+
+on:
+ workflow_call:
+ inputs:
+ project_name:
+ description: 'Name of the project (used for labeling and tag prefix)'
+ required: true
+ type: string
+ project_directory:
+ description: 'Directory containing the project (relative to repository root)'
+ required: true
+ type: string
+ outputs:
+ version:
+ description: 'The determined next version'
+ value: ${{ jobs.determine-version.outputs.version }}
+ has_fragments:
+ description: 'Whether fragments were found'
+ value: ${{ jobs.determine-version.outputs.has_fragments }}
+
+jobs:
+ determine-version:
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.version.outputs.version }}
+ has_fragments: ${{ steps.check.outputs.has_fragments }}
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Setup Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.11'
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+
+ - name: Install dependencies
+ run: |
+ uv sync --all-packages --group release
+
+ - name: Check for fragments
+ id: check
+ working-directory: ${{ inputs.project_directory }}
+ run: |
+ if [ ! -d "changelog.d" ]; then
+ echo "ERROR: changelog.d directory not found for ${{ inputs.project_name }}"
+ exit 1
+ fi
+
+ FRAGMENT_COUNT=$(find changelog.d -type f -name "*.*.md" ! -name "template.md" ! -name "README.md" 2>/dev/null | wc -l)
+
+ if [ "$FRAGMENT_COUNT" -eq 0 ]; then
+ echo "ERROR: No changelog fragments found for ${{ inputs.project_name }}"
+ echo "Cannot create release without changelog fragments"
+ exit 1
+ fi
+
+ echo "has_fragments=true" >> $GITHUB_OUTPUT
+ echo "Found $FRAGMENT_COUNT fragment(s) for ${{ inputs.project_name }}"
+
+ - name: Determine version
+ if: steps.check.outputs.has_fragments == 'true'
+ id: version
+ run: |
+ # Use vcs-versioning CLI to get the next version from the version scheme
+ NEXT_VERSION=$(uv run --directory ${{ inputs.project_directory }} python -m vcs_versioning \
+ --root . \
+ --version-scheme towncrier-fragments \
+ --local-scheme no-local-version 2>&1 | grep -oP '^\d+\.\d+\.\d+' || echo "")
+
+ if [ -z "$NEXT_VERSION" ]; then
+ echo "ERROR: Failed to determine version for ${{ inputs.project_name }}"
+ echo "Version scheme did not return a valid version"
+ exit 1
+ fi
+
+ echo "version=$NEXT_VERSION" >> $GITHUB_OUTPUT
+ echo "Determined version: $NEXT_VERSION for ${{ inputs.project_name }}"
+
+ build-changelog:
+ needs: determine-version
+ if: needs.determine-version.outputs.has_fragments == 'true'
+ 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 uv
+ uses: astral-sh/setup-uv@v6
+
+ - name: Install dependencies
+ run: |
+ uv sync --all-packages --group release
+
+ - name: Run towncrier
+ working-directory: ${{ inputs.project_directory }}
+ run: |
+ if ! uv run towncrier build --version "${{ needs.determine-version.outputs.version }}" --yes; then
+ echo "ERROR: towncrier build failed for ${{ inputs.project_name }}"
+ exit 1
+ fi
+
+ - name: Upload changelog artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: changelog-${{ inputs.project_name }}
+ path: |
+ ${{ inputs.project_directory }}/CHANGELOG.md
+ ${{ inputs.project_directory }}/changelog.d/
+ retention-days: 5
+
diff --git a/.gitignore b/.gitignore
index f6feaedd..e5c5cb87 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,4 +51,7 @@ coverage.xml
# Sphinx documentation
docs/_build/
+# MkDocs documentation
+site/
+
.serena/cache/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5f66a9f8..54a0dcba 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -25,6 +25,8 @@ repos:
- importlib_metadata
- typing-extensions>=4.5
- rich
+ - PyGithub>=2.0.0
+ - towncrier>=23.11.0
- repo: https://github.com/scientific-python/cookie
rev: 2025.10.01
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 5aa34e7a..a287eb6f 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -3,13 +3,16 @@ version: 2
mkdocs:
configuration: mkdocs.yml
-
build:
os: ubuntu-24.04
tools:
python: "3.13"
jobs:
+ pre_create_environment:
+ - asdf plugin add uv
+ - asdf install uv latest
+ - asdf global uv latest
+ create_environment:
+ - uv venv "${READTHEDOCS_VIRTUALENV_PATH}"
install:
- - pip install -U pip # Official recommended way
- - pip install .
- - pip install --group docs
+ - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --frozen --group docs
diff --git a/.serena/.gitignore b/.serena/.gitignore
new file mode 100644
index 00000000..14d86ad6
--- /dev/null
+++ b/.serena/.gitignore
@@ -0,0 +1 @@
+/cache
diff --git a/.serena/memories/done_checklist.md b/.serena/memories/done_checklist.md
deleted file mode 100644
index 8e0fc3e2..00000000
--- a/.serena/memories/done_checklist.md
+++ /dev/null
@@ -1,16 +0,0 @@
-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
deleted file mode 100644
index cf2670d9..00000000
--- a/.serena/memories/project_overview.md
+++ /dev/null
@@ -1,28 +0,0 @@
-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
deleted file mode 100644
index aec4e917..00000000
--- a/.serena/memories/style_and_conventions.md
+++ /dev/null
@@ -1,17 +0,0 @@
-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
deleted file mode 100644
index 8eeeab96..00000000
--- a/.serena/memories/suggested_commands.md
+++ /dev/null
@@ -1,30 +0,0 @@
-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/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..b1ee29ab
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,151 @@
+# setuptools-scm Development Guide for AI Assistants
+
+## Project Overview
+
+**setuptools-scm monorepo** - Extract Python package versions from Git/Mercurial metadata.
+
+- **Language**: Python 3.10+
+- **Build**: setuptools, uv for dependency management
+- **Quality**: pre-commit hooks (ruff, mypy strict), pytest with fixtures
+
+### Structure
+```
+setuptools-scm/ # Setuptools integration (file finders, hooks)
+├── src/setuptools_scm/ # Integration code
+└── testing_scm/ # Setuptools-specific tests
+
+vcs-versioning/ # Core VCS versioning (standalone library)
+├── src/vcs_versioning/ # Core version inference
+└── testing_vcs/ # Core functionality tests
+```
+
+## Quick Commands
+
+```bash
+# Setup
+uv sync --all-packages --all-groups
+
+# Tests (use -n12 for parallel execution)
+uv run pytest -n12 # all tests
+uv run pytest setuptools-scm/testing_scm -n12 # setuptools tests only
+uv run pytest vcs-versioning/testing_vcs -n12 # core tests only
+
+# Quality (use pre-commit)
+pre-commit run --all-files # run all quality checks
+git commit # pre-commit runs automatically
+
+# Docs
+uv run mkdocs serve # local preview
+uv run mkdocs build --clean --strict
+
+# CLI
+uv run python -m setuptools_scm # version from current repo
+uv run python -m vcs_versioning --help # core CLI
+```
+
+## Code Conventions
+
+### Typing
+- **Strict mypy** - precise types, avoid `Any`
+- Type all public functions/classes
+
+### Style
+- **Ruff** enforces all rules (lint + isort)
+- Single-line imports, ordered by type
+- Lines ≤88 chars where practical
+
+### Testing
+- Use project fixtures (`WorkDir`, `wd`, etc.)
+- Warnings treated as errors
+- Add `@pytest.mark.issue(id)` when fixing bugs
+
+### Logging
+- Log level info/debug in tests
+- Minimal logging in library code
+
+### General
+- Small, focused functions
+- Early returns preferred
+- Explicit error messages
+- Concise docstrings
+
+## Project Rules
+
+1. **Use `uv run pytest -n12`** to run tests (parallel execution)
+2. **Use uv to manage dependencies** - don't use pip/conda
+3. **Follow preexisting conventions** - match surrounding code style
+4. **Use the fixtures** - `WorkDir`, `wd`, etc. for test repositories
+
+### File Organization
+- `setuptools-scm/testing_scm/` - setuptools integration tests
+- `vcs-versioning/testing_vcs/` - core VCS functionality tests
+- Add tests in the appropriate directory based on what layer you're testing
+
+## Before Considering Done
+
+- [ ] **Tests pass**: `uv run pytest -n12`
+- [ ] **Pre-commit passes**: `pre-commit run --all-files` (ruff, mypy, etc.)
+- [ ] **New behavior has tests** (use project fixtures)
+- [ ] **Update docs** if user-facing changes
+- [ ] **Add changelog fragment** (always use towncrier, never edit CHANGELOG.md directly)
+
+## Key Files
+
+- `CONTRIBUTING.md` - Release process with towncrier
+- `TESTING.md` - Test organization and running
+- `docs/` - User-facing documentation (mkdocs)
+- `pyproject.toml` - Workspace config (pytest, mypy, ruff)
+- `uv.lock` - Locked dependencies
+
+## Common Patterns
+
+### Version Schemes
+Located in `vcs_versioning/_version_schemes.py`. Entry points in `pyproject.toml`.
+
+### File Finders
+In `setuptools_scm/_file_finders/`. Register as `setuptools.file_finders` entry point.
+**Always active when setuptools-scm is installed** - even without version inference.
+
+### Integration Hooks
+- `infer_version()` - finalize_options hook (pyproject.toml projects)
+- `version_keyword()` - setup.py `use_scm_version` parameter
+- File finder - always registered, independent of versioning
+
+## Changelog Management
+
+**ALWAYS use towncrier fragments - NEVER edit CHANGELOG.md directly.**
+
+Create fragments in `{project}/changelog.d/`:
+
+```bash
+# Bug fix (patch bump)
+echo "Fix warning logic" > setuptools-scm/changelog.d/1231.bugfix.md
+
+# New feature (minor bump)
+echo "Add new scheme" > vcs-versioning/changelog.d/123.feature.md
+
+# Breaking change (major bump)
+echo "Remove deprecated API" > setuptools-scm/changelog.d/456.removal.md
+```
+
+**Fragment types**: `feature`, `bugfix`, `deprecation`, `removal`, `doc`, `misc`
+
+## Debugging
+
+```bash
+# Check version inference
+uv run python -m setuptools_scm
+
+# With custom config
+uv run python -m vcs_versioning --root . --version-scheme guess-next-dev
+
+# Debug mode (set in tests or CLI)
+SETUPTOOLS_SCM_DEBUG=1 uv run python -m setuptools_scm
+```
+
+---
+
+**Documentation**: https://setuptools-scm.readthedocs.io/
+**Repository**: https://github.com/pypa/setuptools-scm/
+**Issues**: https://github.com/pypa/setuptools-scm/issues
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..472554aa
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,189 @@
+# Contributing to setuptools-scm and vcs-versioning
+
+Thank you for contributing! This document explains the development workflow, including how to add changelog entries and create releases.
+
+## Development Setup
+
+This is a monorepo containing two packages:
+- `setuptools-scm/` - Setuptools integration for version management
+- `vcs-versioning/` - Core VCS version detection and schemes
+
+### Installation
+
+```bash
+# Install all dependencies
+uv sync --all-packages --all-groups
+
+# Run tests
+uv run pytest -n12
+
+# Run tests for specific package
+uv run pytest setuptools-scm/testing_scm/ -n12
+uv run pytest vcs-versioning/testing_vcs/ -n12
+```
+
+## Changelog Fragments
+
+We use [towncrier](https://towncrier.readthedocs.io/) to manage changelog entries. This ensures that changelog entries are added alongside code changes and reduces merge conflicts.
+
+### Adding a Changelog Fragment
+
+When you make a change that should be noted in the changelog, create a fragment file in the appropriate project's `changelog.d/` directory:
+
+**For setuptools-scm changes:**
+```bash
+# Create a fragment file
+echo "Your changelog entry here" > setuptools-scm/changelog.d/123.feature.md
+```
+
+**For vcs-versioning changes:**
+```bash
+# Create a fragment file
+echo "Your changelog entry here" > vcs-versioning/changelog.d/456.bugfix.md
+```
+
+### Fragment Naming Convention
+
+Fragments follow the naming pattern: `{number}.{type}.md`
+
+- **number**: Usually the GitHub issue or PR number (or any unique identifier)
+- **type**: One of the types below
+- **extension**: Always `.md`
+
+### Fragment Types
+
+The fragment type determines the version bump:
+
+| Type | Description | Version Bump | Example |
+|------|-------------|--------------|---------|
+| `feature` | New features or enhancements | **Minor** (0.X.0) | `123.feature.md` |
+| `bugfix` | Bug fixes | **Patch** (0.0.X) | `456.bugfix.md` |
+| `deprecation` | Deprecation notices | **Minor** (0.X.0) | `789.deprecation.md` |
+| `removal` | Breaking changes/removed features | **Major** (X.0.0) | `321.removal.md` |
+| `doc` | Documentation improvements | **Patch** (0.0.X) | `654.doc.md` |
+| `misc` | Internal changes, refactoring | **Patch** (0.0.X) | `987.misc.md` |
+
+### Fragment Content
+
+Keep fragments concise and user-focused. Do not include issue numbers in the content (they're added automatically).
+
+**Good:**
+```markdown
+Add support for custom version schemes via plugin system
+```
+
+**Bad:**
+```markdown
+Fix #123: Added support for custom version schemes via plugin system in the configuration
+```
+
+## Version Scheme Integration
+
+The `towncrier-fragments` version scheme automatically determines version bumps based on changelog fragments. During development builds, the version will reflect the next release version:
+
+```bash
+# If you have a feature fragment, version might be:
+9.3.0.dev5+g1234567
+
+# If you only have bugfix fragments:
+9.2.2.dev5+g1234567
+```
+
+This ensures that the version you see during development will be the actual release version.
+
+## Release Process
+
+Releases are managed through GitHub Actions workflows with manual approval.
+
+### 1. Create a Release Proposal
+
+Maintainers trigger the release workflow manually:
+
+1. Go to **Actions** → **Create Release Proposal**
+2. Select which projects to release:
+ - ☑ Release setuptools-scm
+ - ☑ Release vcs-versioning
+3. Click **Run workflow**
+
+The workflow will:
+- Analyze changelog fragments in each project
+- Determine the version bump (major/minor/patch) based on fragment types
+- Query the `towncrier-fragments` version scheme for the next version
+- Run `towncrier build` to update the CHANGELOG.md
+- Create a release PR with the changes
+- Label the PR with `release:setuptools-scm` and/or `release:vcs-versioning`
+
+### 2. Review and Approve
+
+Review the release PR:
+- Check that the changelog entries are accurate
+- Verify the version numbers are correct
+- Ensure all tests pass
+
+### 3. Merge to Release
+
+When you merge the PR to `main`:
+- The merge triggers the tag creation workflow automatically
+- Tags are created with the project prefix:
+ - `setuptools-scm-v9.3.0`
+ - `vcs-versioning-v0.2.0`
+- GitHub releases are created with changelog excerpts
+- Tag pushes trigger the PyPI upload workflow
+- Only the package(s) matching the tag prefix are uploaded to PyPI
+
+## Workflow Architecture
+
+The release system is designed to be reusable by other projects:
+
+### Key Components
+
+1. **Version Scheme** (`vcs_versioning._version_schemes._towncrier`)
+ - Analyzes fragments to determine version bump
+ - Used by both development builds and release workflow
+ - No version calculation logic in scripts - single source of truth
+
+2. **Release Proposal Workflow** (`.github/workflows/release-proposal.yml`)
+ - Manual trigger with project selection
+ - Uses `vcs-versioning` CLI to query version scheme
+ - Runs `towncrier build` with the determined version
+ - Creates labeled PR
+
+3. **Tag Creation Workflow** (`.github/workflows/create-release-tags.yml`)
+ - Triggered by PR merge with release labels
+ - Creates project-prefixed tags
+ - Creates GitHub releases
+
+4. **Upload Workflow** (`.github/workflows/python-tests.yml`)
+ - Triggered by tag push (filtered by tag prefix)
+ - Uploads only matching package to PyPI
+
+### Benefits
+
+- ✅ Version determination is consistent (version scheme is single source of truth)
+- ✅ Manual approval via familiar PR review process
+- ✅ Atomic releases tied to merge commits
+- ✅ Project-specific tags prevent accidental releases
+- ✅ Can release one or both projects in a single PR
+- ✅ Fully auditable release process
+- ✅ Reusable workflows for other projects
+
+## Testing Locally
+
+You can test the version scheme locally:
+
+```bash
+# See what version would be generated
+cd setuptools-scm
+uv run python -m vcs_versioning --root .. --version-scheme towncrier-fragments
+
+# Test towncrier build (dry-run)
+cd setuptools-scm
+uv run towncrier build --version 9.3.0 --draft
+```
+
+## Questions?
+
+- Check [TESTING.md](./TESTING.md) for testing guidelines
+- Open an issue for bugs or feature requests
+- Ask in discussions for general questions
+
diff --git a/README.md b/README.md
index f4ca4bf9..1e506f0c 100644
--- a/README.md
+++ b/README.md
@@ -1,132 +1,60 @@
-# setuptools-scm
-[](https://github.com/pypa/setuptools-scm/actions/workflows/python-tests.yml)
-[](https://setuptools-scm.readthedocs.io/en/latest/?badge=latest)
-[ ](https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme)
+# setuptools-scm Monorepo
-## about
+This is the monorepo for the setuptools-scm ecosystem, containing two main projects:
-[setuptools-scm] extracts Python package versions from `git` or `hg` metadata
-instead of declaring them as the version argument
-or in a Source Code Managed (SCM) managed file.
+## Projects
-Additionally [setuptools-scm] provides `setuptools` with a list of
-files that are managed by the SCM
-
-(i.e. it automatically adds all the SCM-managed files to the sdist).
-
-Unwanted files must be excluded via `MANIFEST.in`
-or [configuring Git archive][git-archive-docs].
+### [setuptools-scm](./setuptools-scm/)
-> **⚠️ 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.
+The main package that extracts Python package versions from Git or Mercurial metadata and provides setuptools integration.
-## `pyproject.toml` usage
+**[Read setuptools-scm documentation →](./setuptools-scm/README.md)**
-The preferred way to configure [setuptools-scm] is to author
-settings in a `tool.setuptools_scm` section of `pyproject.toml`.
+### [vcs-versioning](./vcs-versioning/)
-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.
+Core VCS versioning functionality extracted as a standalone library that can be used independently of setuptools.
-```toml title="pyproject.toml"
-[build-system]
-requires = ["setuptools>=80", "setuptools-scm>=8"]
-build-backend = "setuptools.build_meta"
-```
-
-That will be sufficient to require [setuptools-scm] for projects
-that support [PEP 518] like [pip] and [build].
-
-[pip]: https://pypi.org/project/pip
-[build]: https://pypi.org/project/build
-[PEP 518]: https://peps.python.org/pep-0518/
+**[Read vcs-versioning documentation →](./vcs-versioning/README.md)**
+## Development
-To enable version inference, you need to set the version
-dynamically in the `project` section of `pyproject.toml`:
+This workspace uses [uv](https://github.com/astral-sh/uv) for dependency management.
-```toml title="pyproject.toml"
-[project]
-# version = "0.0.1" # Remove any existing version parameter.
-dynamic = ["version"]
+### Setup
-[tool.setuptools_scm]
+```bash
+# Install all packages with all dependency groups (tests, docs, etc.)
+uv sync --all-packages --all-groups
```
-!!! 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"
+### Running Tests
- [project]
- dynamic = ["version"]
- ```
+```bash
+# Run all tests
+uv run pytest -n12
- The `[tool.setuptools_scm]` section is only needed if you want to customize
- configuration options.
+# Run tests for setuptools-scm only
+uv run pytest setuptools-scm/testing_scm -n12
-Additionally, a version file can be written by specifying:
-
-```toml title="pyproject.toml"
-[tool.setuptools_scm]
-version_file = "pkg/_version.py"
+# Run tests for vcs-versioning only
+uv run pytest vcs-versioning/testing_vcs -n12
```
-Where `pkg` is the name of your package.
+### Building Documentation
-If you need to confirm which version string is being generated or debug the configuration,
-you can install [setuptools-scm] directly in your working environment and run:
+Documentation is shared across projects:
-```console
-$ python -m setuptools_scm
-# To explore other options, try:
-$ python -m setuptools_scm --help
+```bash
+uv run mkdocs serve
```
-For further configuration see the [documentation].
-
-[setuptools-scm]: https://github.com/pypa/setuptools-scm
-[documentation]: https://setuptools-scm.readthedocs.io/
-[git-archive-docs]: https://setuptools-scm.readthedocs.io/en/stable/usage/#builtin-mechanisms-for-obtaining-version-numbers
-
-
-## Interaction with Enterprise Distributions
-
-Some enterprise distributions like RHEL7
-ship rather old setuptools versions.
-
-In those cases its typically possible to build by using an sdist against `setuptools-scm<2.0`.
-As those old setuptools versions lack sensible types for versions,
-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
-
-
-Everyone interacting in the [setuptools-scm] project's codebases, issue
-trackers, chat rooms, and mailing lists is expected to follow the
-[PSF Code of Conduct].
+## Links
-[PSF Code of Conduct]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
+- **Documentation**: https://setuptools-scm.readthedocs.io/
+- **Repository**: https://github.com/pypa/setuptools-scm/
+- **Issues**: https://github.com/pypa/setuptools-scm/issues
+## License
-## Security Contact
+Both projects are distributed under the terms of the MIT license.
-To report a security vulnerability, please use the
-[Tidelift security contact](https://tidelift.com/security).
-Tidelift will coordinate the fix and disclosure.
diff --git a/RELEASE_SYSTEM.md b/RELEASE_SYSTEM.md
new file mode 100644
index 00000000..69ccba74
--- /dev/null
+++ b/RELEASE_SYSTEM.md
@@ -0,0 +1,48 @@
+# Release System
+
+Towncrier-based release system for the setuptools-scm monorepo.
+
+## Components
+
+- `towncrier-fragments` version scheme: Determines version bumps from changelog fragment types
+- `changelog.d/` directories per project with fragment templates
+- GitHub workflows for release proposals and tag creation
+- Project-prefixed tags: `setuptools-scm-vX.Y.Z`, `vcs-versioning-vX.Y.Z`
+
+## Version Scheme
+
+Fragment types determine version bumps:
+- `removal` → major bump
+- `feature`, `deprecation` → minor bump
+- `bugfix`, `doc`, `misc` → patch bump
+
+Entry point: `vcs_versioning._version_schemes._towncrier:version_from_fragments`
+
+Tests: `vcs-versioning/testing_vcs/test_version_scheme_towncrier.py`
+
+## Workflows
+
+**Release Proposal** (`.github/workflows/release-proposal.yml`):
+Manual trigger, runs towncrier, creates labeled PR
+
+**Tag Creation** (`.github/workflows/create-release-tags.yml`):
+On PR merge, creates tags from PR title, triggers PyPI upload
+
+**Modified Upload** (`.github/workflows/python-tests.yml`):
+Split per-project upload jobs filtered by tag prefix
+
+## Usage
+
+**Contributors:** Add changelog fragment to `{project}/changelog.d/{number}.{type}.md`
+
+**Maintainers:** Trigger release proposal workflow, review PR, merge to create tags and upload to PyPI
+
+## Design Notes
+
+- Version scheme is single source of truth, no custom scripts
+- Manual approval via PR review
+- Workflows fail explicitly if required data is missing
+- Tag prefix filtering controls package uploads
+
+See [CONTRIBUTING.md](./CONTRIBUTING.md) and [TESTING.md](./TESTING.md) for details.
+
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 00000000..863a9cd1
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,161 @@
+# Testing Organization
+
+This document describes the test organization in the setuptools-scm monorepo.
+
+## Directory Structure
+
+The repository contains two test suites:
+
+- **`vcs-versioning/testing_vcs/`** - Core VCS versioning functionality tests
+- **`setuptools-scm/testing_scm/`** - Setuptools integration and wrapper tests
+
+## Separation Principle
+
+Tests are organized by architectural layer:
+
+### Core VCS Layer (`vcs-versioning/testing_vcs/`)
+
+Tests for core version control system functionality:
+- VCS backend operations (Git, Mercurial parsing)
+- Version scheme and formatting logic
+- Configuration validation
+- Version inference
+- Error handling
+- Core utility functions
+
+**When to add tests here:** If the functionality is in `vcs_versioning` package and doesn't depend on setuptools.
+
+### Setuptools Integration Layer (`setuptools-scm/testing_scm/`)
+
+Tests for setuptools-specific functionality:
+- Setuptools hooks and entry points
+- `setup.py` integration (` use_scm_version`)
+- `pyproject.toml` reading and Configuration.from_file()
+- File finding for setuptools (sdist integration)
+- Distribution metadata
+- setuptools-scm CLI wrapper
+
+**When to add tests here:** If the functionality is in `setuptools_scm` package or requires setuptools machinery.
+
+## Running Tests
+
+### Run all tests
+```bash
+uv run pytest -n12
+```
+
+### Run core VCS tests only
+```bash
+uv run pytest vcs-versioning/testing_vcs -n12
+```
+
+### Run setuptools integration tests only
+```bash
+uv run pytest setuptools-scm/testing_scm -n12
+```
+
+### Run specific test file
+```bash
+uv run pytest vcs-versioning/testing_vcs/test_version_schemes.py -v
+# Test the towncrier version scheme
+uv run pytest vcs-versioning/testing_vcs/test_version_scheme_towncrier.py -v
+```
+
+## Test Fixtures
+
+Both test suites use `vcs_versioning.test_api` as a pytest plugin, providing common test infrastructure:
+
+- `WorkDir`: Helper for creating temporary test repositories
+- `TEST_SOURCE_DATE`: Consistent test time for reproducibility
+- `DebugMode`: Context manager for debug logging
+- Repository fixtures: `wd`, `repositories_hg_git`, etc.
+
+See `vcs-versioning/src/vcs_versioning/test_api.py` and `vcs-versioning/src/vcs_versioning/_test_utils.py` for details.
+
+## Testing Release Workflows
+
+### Testing the towncrier-fragments Version Scheme
+
+The `towncrier-fragments` version scheme determines version bumps based on changelog fragments:
+
+```bash
+# Create test fragments
+echo "Test feature" > setuptools-scm/changelog.d/1.feature.md
+echo "Test bugfix" > setuptools-scm/changelog.d/2.bugfix.md
+
+# Check what version would be generated
+cd setuptools-scm
+uv run python -m vcs_versioning --root .. --version-scheme towncrier-fragments
+# Should show a minor bump (e.g., 9.3.0.dev...)
+
+# Clean up test fragments
+rm changelog.d/1.feature.md changelog.d/2.bugfix.md
+```
+
+### Testing Towncrier Build
+
+Test changelog generation without committing:
+
+```bash
+cd setuptools-scm
+
+# Dry-run: see what the changelog would look like
+uv run towncrier build --version 9.3.0 --draft
+
+# Build with keeping fragments (for testing)
+uv run towncrier build --version 9.3.0 --keep
+```
+
+### Testing Version Bump Logic
+
+Fragment types determine version bumps:
+
+- **removal** → Major bump (X.0.0)
+- **feature**, **deprecation** → Minor bump (0.X.0)
+- **bugfix**, **doc**, **misc** → Patch bump (0.0.X)
+
+Create different fragment types and verify the version scheme produces the expected version.
+
+### Local Release Workflow Testing
+
+You can test the release process locally (without actually creating tags):
+
+```bash
+# 1. Create test fragments
+echo "Add new feature" > setuptools-scm/changelog.d/999.feature.md
+
+# 2. Query version scheme
+cd setuptools-scm
+NEXT_VERSION=$(uv run python -m vcs_versioning --root .. --version-scheme towncrier-fragments --local-scheme no-local-version 2>/dev/null | grep -oP '^\d+\.\d+\.\d+')
+echo "Next version: $NEXT_VERSION"
+
+# 3. Build changelog (dry-run)
+uv run towncrier build --version "$NEXT_VERSION" --draft
+
+# 4. Clean up
+rm changelog.d/999.feature.md
+cd ..
+```
+
+### Workflow Validation
+
+Before merging workflow changes:
+
+1. Validate YAML syntax:
+ ```bash
+ # If you have actionlint installed
+ actionlint .github/workflows/*.yml
+ ```
+
+2. Check workflow conditions match your expectations:
+ - Tag filters in `python-tests.yml`
+ - Label checks in `create-release-tags.yml`
+
+3. Test in a fork with reduced scope (test project, Test PyPI)
+
+## Migration Notes
+
+File finders remain in setuptools-scm because they're setuptools integration (registered as `setuptools.file_finders` entry points), not core VCS functionality.
+
+For deeper unit test conversions beyond basic reorganization, see `setuptools-scm/testing_scm/INTEGRATION_MIGRATION_PLAN.md`.
+
diff --git a/_own_version_helper.py b/_own_version_helper.py
deleted file mode 100644
index 12ffeb07..00000000
--- a/_own_version_helper.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""
-this module is a hack only in place to allow for setuptools
-to use the attribute for the versions
-
-it works only if the backend-path of the build-system section
-from pyproject.toml is respected
-"""
-
-from __future__ import annotations
-
-import logging
-import os
-
-from typing import Callable
-
-from setuptools import build_meta as build_meta
-
-from setuptools_scm import Configuration
-from setuptools_scm import _types as _t
-from setuptools_scm import get_version
-from setuptools_scm import git
-from setuptools_scm import hg
-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")
-# todo: take fake entrypoints from pyproject.toml
-try_parse: list[Callable[[_t.PathT, Configuration], ScmVersion | None]] = [
- parse_pkginfo,
- git.parse,
- hg.parse,
- git.parse_archival,
- hg.parse_archival,
-]
-
-
-def parse(root: str, config: Configuration) -> ScmVersion | None:
- for maybe_parse in try_parse:
- try:
- parsed = maybe_parse(root, config)
- except OSError as e:
- log.warning("parse with %s failed with: %s", maybe_parse, e)
- else:
- if parsed is not None:
- return parsed
- return 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=local_scheme,
- )
-
-
-version: str
-
-
-def __getattr__(name: str) -> str:
- if name == "version":
- global version
- version = scm_version()
- return version
- raise AttributeError(name)
diff --git a/docs/changelog.md b/docs/changelog.md
index 15fe40c9..172171bd 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,3 +1,3 @@
{%
- include-markdown "../CHANGELOG.md"
+ include-markdown "../setuptools-scm/CHANGELOG.md"
%}
diff --git a/docs/config.md b/docs/config.md
index 83d11e2b..a44ead6d 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -27,12 +27,13 @@ Use the `[tool.setuptools_scm]` section when you need to:
- Configure fallback behavior (`fallback_version`)
- Or any other non-default behavior
-## configuration parameters
+## Core Configuration
+
+These configuration options control version inference and formatting behavior.
Configuration parameters can be configured in `pyproject.toml` or `setup.py`.
Callables or other Python objects have to be passed in `setup.py` (via the `use_scm_version` keyword argument).
-
`root : Path | PathLike[str]`
: Relative path to the SCM root, defaults to `.` and is relative to the file path passed in `relative_to`
@@ -45,42 +46,12 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
either an entrypoint name or a callable.
See [Version number construction](extending.md#setuptools_scmlocal_scheme) for predefined implementations.
-
-`version_file: Path | PathLike[str] | None = None`
-: A path to a file that gets replaced with a file containing the current
- version. It is ideal for creating a ``_version.py`` file within the
- package, typically used to avoid using `importlib.metadata`
- (which adds some overhead).
-
- !!! warning ""
-
- Only files with `.py` and `.txt` extensions have builtin templates,
- for other file types it is necessary to provide `version_file_template`.
-
-`version_file_template: str | None = None`
-: A new-style format string taking `version`, `scm_version` and `version_tuple` as parameters.
- `version` is the generated next_version as string,
- `version_tuple` is a tuple of split numbers/strings and
- `scm_version` is the `ScmVersion` instance the current `version` was rendered from
-
-
-`write_to: Pathlike[str] | Path | None = None`
-: (deprecated) legacy option to create a version file relative to the scm root
- it's broken for usage from a sdist and fixing it would be a fatal breaking change,
- use `version_file` instead.
-
-`relative_to: Path|Pathlike[str] = "pyproject.toml"`
-: A file/directory from which the root can be resolved.
- Typically called by a script or module that is not in the root of the
- repository to point `setuptools_scm` at the root of the repository by
- supplying `__file__`.
-
`tag_regex: str|Pattern[str]`
: A Python regex string to extract the version part from any SCM tag.
The regex needs to contain either a single match group, or a group
named `version`, that captures the actual version information.
- Defaults to the value of [setuptools_scm._config.DEFAULT_TAG_REGEX][]
+ Defaults to the value of [vcs_versioning._config.DEFAULT_TAG_REGEX][]
which supports tags with optional "v" prefix (recommended), project prefixes,
and various version formats.
@@ -118,16 +89,31 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
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.
+`normalize`
+: A boolean flag indicating if the version string should be normalized.
+ Defaults to `True`. Setting this to `False` is equivalent to setting
+ `version_cls` to [vcs_versioning.NonNormalizedVersion][]
+
+`version_cls: type|str = packaging.version.Version`
+: An optional class used to parse, verify and possibly normalize the version
+ string. Its constructor should receive a single string argument, and its
+ `str` should return the normalized version string to use.
+ This option can also receive a class qualified name as a string.
+
+ The [vcs_versioning.NonNormalizedVersion][] convenience class is
+ provided to disable the normalization step done by
+ `packaging.version.Version`. If this is used while `setuptools-scm`
+ is integrated in a setuptools packaging process, the non-normalized
+ version number will appear in all files (see `version_file` note).
+
+ !!! note "normalization still applies to artifact filenames"
+ Setuptools will still normalize it to create the final distribution,
+ so as to stay compliant with the python packaging standards.
`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][]
+ Defaults to the value set by [vcs_versioning._backends._git.DEFAULT_DESCRIBE][]
`scm.git.pre_parse`
: A string specifying which git pre-parse function to use before parsing version information.
@@ -148,29 +134,49 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
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
- `version_cls` to [setuptools_scm.NonNormalizedVersion][]
+`relative_to: Path|Pathlike[str] = "pyproject.toml"`
+: A file/directory from which the root can be resolved.
+ Typically called by a script or module that is not in the root of the
+ repository to point to the root of the repository by
+ supplying `__file__`.
-`version_cls: type|str = packaging.version.Version`
-: An optional class used to parse, verify and possibly normalize the version
- string. Its constructor should receive a single string argument, and its
- `str` should return the normalized version string to use.
- This option can also receive a class qualified name as a string.
+`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 vcs-versioning internals to use it.
- The [setuptools_scm.NonNormalizedVersion][] convenience class is
- provided to disable the normalization step done by
- `packaging.version.Version`. If this is used while `setuptools-scm`
- is integrated in a setuptools packaging process, the non-normalized
- version number will appear in all files (see `version_file` note).
+`version_file: Path | PathLike[str] | None = None`
+: A path to a file that gets replaced with a file containing the current
+ version. It is ideal for creating a ``_version.py`` file within the
+ package, typically used to avoid using `importlib.metadata`
+ (which adds some overhead).
- !!! note "normalization still applies to artifact filenames"
- Setuptools will still normalize it to create the final distribution,
- so as to stay compliant with the python packaging standards.
+ !!! warning ""
+
+ Only files with `.py` and `.txt` extensions have builtin templates,
+ for other file types it is necessary to provide `version_file_template`.
+
+`version_file_template: str | None = None`
+: A new-style format string taking `version`, `scm_version` and `version_tuple` as parameters.
+ `version` is the generated next_version as string,
+ `version_tuple` is a tuple of split numbers/strings and
+ `scm_version` is the `ScmVersion` instance the current `version` was rendered from
+
+## setuptools-scm Specific Configuration
+
+These options control setuptools integration behavior.
+`write_to: Pathlike[str] | Path | None = None`
+: (deprecated) legacy option to create a version file relative to the scm root
+ it's broken for usage from a sdist and fixing it would be a fatal breaking change,
+ use `version_file` instead.
-## environment variables
+## Environment Variables
+
+### Version Detection Overrides
+
+These environment variables override version detection behavior.
`SETUPTOOLS_SCM_PRETEND_VERSION`
: used as the primary source for the version number
@@ -193,14 +199,25 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
this will take precedence over ``SETUPTOOLS_SCM_PRETEND_VERSION``
+`SETUPTOOLS_SCM_PRETEND_METADATA`
+: A TOML inline table for overriding individual version metadata fields.
+ See the [overrides documentation](overrides.md#pretend-metadata-core) for details.
+
+`SETUPTOOLS_SCM_PRETEND_METADATA_FOR_${DIST_NAME}`
+: Same as above but specific to a package (recommended over the generic version).
+
`SETUPTOOLS_SCM_DEBUG`
-: enable the debug logging
+: Enable debug logging for version detection and processing.
`SOURCE_DATE_EPOCH`
-: used as the timestamp from which the
+: Used as the timestamp from which the
``node-and-date`` and ``node-and-timestamp`` local parts are
- derived, otherwise the current time is used
- (https://reproducible-builds.org/docs/source-date-epoch/)
+ derived, otherwise the current time is used.
+ Standard environment variable from [reproducible-builds.org](https://reproducible-builds.org/docs/source-date-epoch/).
+
+### setuptools-scm Overrides
+
+These environment variables control setuptools-scm specific behavior.
`SETUPTOOLS_SCM_IGNORE_VCS_ROOTS`
: a ``os.pathsep`` separated list
@@ -211,11 +228,15 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
for example, set this to ``chg`` to reduce start-up overhead of Mercurial
+`SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}`
+: A TOML inline table to override configuration from `pyproject.toml`.
+ See the [overrides documentation](overrides.md#config-overrides) for details.
+`SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT`
+: Override the subprocess timeout (default: 40 seconds).
+ See the [overrides documentation](overrides.md#subprocess-timeouts) for details.
-
-
-## automatic file inclusion
+## Automatic File Inclusion
!!! warning "Setuptools File Finder Integration"
@@ -274,21 +295,20 @@ tar -tzf dist/package-*.tar.gz
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
+## API Reference
### constants
-::: setuptools_scm._config.DEFAULT_TAG_REGEX
+::: vcs_versioning._config.DEFAULT_TAG_REGEX
options:
heading_level: 4
-::: setuptools_scm.git.DEFAULT_DESCRIBE
+::: vcs_versioning._backends._git.DEFAULT_DESCRIBE
options:
heading_level: 4
### the configuration class
-::: setuptools_scm.Configuration
+::: vcs_versioning.Configuration
options:
heading_level: 4
diff --git a/docs/customizing.md b/docs/customizing.md
index 18ee8765..4c74cded 100644
--- a/docs/customizing.md
+++ b/docs/customizing.md
@@ -68,4 +68,4 @@ setup(use_scm_version={'local_scheme': clean_scheme})
## alternative version classes
-::: setuptools_scm.NonNormalizedVersion
+::: vcs_versioning.NonNormalizedVersion
diff --git a/docs/examples/version_scheme_code/setup.py b/docs/examples/version_scheme_code/setup.py
index 69f903f2..e0c3bb86 100644
--- a/docs/examples/version_scheme_code/setup.py
+++ b/docs/examples/version_scheme_code/setup.py
@@ -3,7 +3,6 @@
from __future__ import annotations
from setuptools import setup
-
from setuptools_scm import ScmVersion
diff --git a/docs/extending.md b/docs/extending.md
index c4cc2e03..945ae92f 100644
--- a/docs/extending.md
+++ b/docs/extending.md
@@ -14,8 +14,8 @@
entrypoint's name. E.g. for the built-in entrypoint for Git the
entrypoint is named `.git` and references `setuptools_scm.git:parse`
- The return value MUST be a [`setuptools_scm.version.ScmVersion`][] instance
- created by the function [`setuptools_scm.version.meta`][].
+ The return value MUST be a [`vcs_versioning.ScmVersion`][] instance
+ created by the function [`vcs_versioning._version_schemes.meta`][].
`setuptools_scm.files_command`
: Either a string containing a shell command that prints all SCM managed
@@ -27,25 +27,21 @@
### api reference for scm version objects
-::: setuptools_scm.version.ScmVersion
+::: vcs_versioning.ScmVersion
options:
show_root_heading: yes
heading_level: 4
-::: setuptools_scm.version.meta
+::: vcs_versioning._version_schemes.meta
options:
show_root_heading: yes
heading_level: 4
## Version number construction
-
-
-
-
### `setuptools_scm.version_scheme`
Configures how the version number is constructed given a
-[ScmVersion][setuptools_scm.version.ScmVersion] instance and should return a string
+[ScmVersion][vcs_versioning.ScmVersion] instance and should return a string
representing the version.
### Available implementations
@@ -130,7 +126,7 @@ representing the version.
### `setuptools_scm.local_scheme`
Configures how the local part of a version is rendered given a
-[ScmVersion][setuptools_scm.version.ScmVersion] instance and should return a string
+[ScmVersion][vcs_versioning.ScmVersion] instance and should return a string
representing the local version.
Dates and times are in Coordinated Universal Time (UTC), because as part
of the version, they should be location independent.
diff --git a/docs/index.md b/docs/index.md
index c86f93ce..b5774e03 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -22,6 +22,26 @@ or [configuring Git archive][git-archive-docs].
[git-archive-docs]: usage.md#builtin-mechanisms-for-obtaining-version-numbers
+## Architecture
+
+`setuptools-scm` is built on top of [`vcs-versioning`](https://pypi.org/project/vcs-versioning/),
+a standalone library that provides the core VCS version extraction and formatting functionality.
+
+**vcs-versioning** (core library):
+: Handles version extraction from Git and Mercurial repositories, version scheme logic,
+ tag parsing, and version formatting. These are universal concepts that work across
+ different build systems and integrations.
+
+**setuptools-scm** (integration layer):
+: Provides setuptools-specific features like build-time integration, automatic file
+ finder registration, and version file generation during package builds.
+
+!!! info "Understanding the documentation"
+
+ Most configuration options documented here are **core vcs-versioning features** that
+ work universally. Features specific to setuptools-scm integration (like automatic
+ file finders or version file writing) are clearly marked throughout the documentation.
+
## Basic usage
### With setuptools
diff --git a/docs/integrators.md b/docs/integrators.md
new file mode 100644
index 00000000..b59d5731
--- /dev/null
+++ b/docs/integrators.md
@@ -0,0 +1,1002 @@
+# Integrator Guide
+
+This guide is for developers building tools that integrate vcs-versioning (like hatch-vcs, custom build backends, or other version management tools).
+
+## Overview
+
+vcs-versioning provides a flexible override system that allows integrators to:
+
+- Use custom environment variable prefixes (e.g., `HATCH_VCS_*` instead of `SETUPTOOLS_SCM_*`)
+- Automatically fall back to `VCS_VERSIONING_*` variables for universal configuration
+- Apply global overrides once at entry points using a context manager pattern
+- Access override values throughout the execution via thread-safe accessor functions
+
+## Quick Start
+
+The simplest way to use the overrides system is with the `GlobalOverrides` context manager:
+
+```python
+from vcs_versioning.overrides import GlobalOverrides
+from vcs_versioning import infer_version_string
+
+# Use your own prefix
+with GlobalOverrides.from_env("HATCH_VCS"):
+ # All modules now use HATCH_VCS_* env vars with VCS_VERSIONING_* fallback
+ version = infer_version_string(
+ dist_name="my-package",
+ pyproject_data=pyproject_data,
+ )
+```
+
+That's it! The context manager:
+1. Reads all global override values from environment variables
+2. Makes them available to all vcs-versioning internal modules
+3. Automatically cleans up when exiting the context
+
+## GlobalOverrides Context Manager
+
+### Basic Usage
+
+```python
+from vcs_versioning.overrides import GlobalOverrides
+
+with GlobalOverrides.from_env("YOUR_TOOL"):
+ # Your version detection code here
+ pass
+```
+
+### What Gets Configured
+
+The `GlobalOverrides` context manager reads and applies these configuration values, and automatically configures logging:
+
+| Field | Environment Variables | Default | Description |
+|-------|----------------------|---------|-------------|
+| `debug` | `{TOOL}_DEBUG`
`VCS_VERSIONING_DEBUG` | `False` (WARNING level) | Debug logging level (int) or False |
+| `subprocess_timeout` | `{TOOL}_SUBPROCESS_TIMEOUT`
`VCS_VERSIONING_SUBPROCESS_TIMEOUT` | `40` | Timeout for subprocess commands in seconds |
+| `hg_command` | `{TOOL}_HG_COMMAND`
`VCS_VERSIONING_HG_COMMAND` | `"hg"` | Command to use for Mercurial operations |
+| `source_date_epoch` | `SOURCE_DATE_EPOCH` | `None` | Unix timestamp for reproducible builds |
+
+**Note:** Logging is automatically configured when entering the `GlobalOverrides` context. The debug level is used to set the log level for all vcs-versioning and setuptools-scm loggers.
+
+### Debug Logging Levels
+
+The `debug` field supports multiple formats:
+
+```bash
+# Boolean flag - enables DEBUG level
+export HATCH_VCS_DEBUG=1
+
+# Explicit log level (int from logging module)
+export HATCH_VCS_DEBUG=10 # DEBUG
+export HATCH_VCS_DEBUG=20 # INFO
+export HATCH_VCS_DEBUG=30 # WARNING
+
+# Omitted or empty - uses WARNING level (default)
+```
+
+### Accessing Override Values
+
+Within the context, you can access override values:
+
+```python
+from vcs_versioning.overrides import GlobalOverrides, get_active_overrides
+
+with GlobalOverrides.from_env("HATCH_VCS") as overrides:
+ # Direct access
+ print(f"Debug level: {overrides.debug}")
+ print(f"Timeout: {overrides.subprocess_timeout}")
+
+ # Or via accessor function
+ current = get_active_overrides()
+ log_level = current.log_level() # Returns int from logging module
+```
+
+### Creating Modified Overrides
+
+Use `from_active()` to create a modified version of the currently active overrides:
+
+```python
+from vcs_versioning.overrides import GlobalOverrides
+import logging
+
+with GlobalOverrides.from_env("TOOL"):
+ # Original context with default settings
+
+ # Create a nested context with modified values
+ with GlobalOverrides.from_active(debug=logging.INFO, subprocess_timeout=100):
+ # This context has INFO logging and 100s timeout
+ # Other fields (hg_command, source_date_epoch, tool) are preserved
+ pass
+```
+
+This is particularly useful in tests where you want to modify specific overrides without affecting others:
+
+```python
+def test_with_custom_timeout():
+ # Start with standard test overrides
+ with GlobalOverrides.from_active(subprocess_timeout=5):
+ # Test with short timeout
+ pass
+```
+
+### Exporting Overrides
+
+Use `export()` to export overrides to environment variables or pytest monkeypatch:
+
+```python
+from vcs_versioning.overrides import GlobalOverrides
+
+# Export to environment dict
+overrides = GlobalOverrides.from_env("TOOL", env={"TOOL_DEBUG": "INFO"})
+env = {}
+overrides.export(env)
+# env now contains: {"TOOL_DEBUG": "20", "TOOL_SUBPROCESS_TIMEOUT": "40", ...}
+
+# Export via pytest monkeypatch
+def test_subprocess(monkeypatch):
+ overrides = GlobalOverrides.from_active(debug=logging.DEBUG)
+ overrides.export(monkeypatch)
+ # Environment is now set for subprocess calls
+ result = subprocess.run(["my-command"], env=os.environ)
+```
+
+This is useful when you need to:
+- Pass overrides to subprocesses
+- Set up environment for integration tests
+- Export configuration for external tools
+
+## Automatic Fallback Behavior
+
+The overrides system checks environment variables in this order:
+
+1. **Tool-specific prefix**: `{YOUR_TOOL}_*`
+2. **VCS_VERSIONING prefix**: `VCS_VERSIONING_*` (universal fallback)
+3. **Default value**: Hard-coded defaults
+
+### Example
+
+```python
+with GlobalOverrides.from_env("HATCH_VCS"):
+ # Checks in order:
+ # 1. HATCH_VCS_DEBUG
+ # 2. VCS_VERSIONING_DEBUG
+ # 3. Default: False (WARNING level)
+ pass
+```
+
+This means:
+- Users can set `VCS_VERSIONING_DEBUG=1` to enable debug mode for all tools
+- Or set `HATCH_VCS_DEBUG=1` to enable it only for hatch-vcs
+- The tool-specific setting takes precedence
+
+## Distribution-Specific Overrides
+
+For dist-specific overrides like pretend versions and metadata, use `EnvReader`:
+
+```python
+from vcs_versioning.overrides import EnvReader
+import os
+
+# Read pretend version for a specific distribution
+reader = EnvReader(
+ tools_names=("HATCH_VCS", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package",
+)
+pretend_version = reader.read("PRETEND_VERSION")
+
+# This checks in order:
+# 1. HATCH_VCS_PRETEND_VERSION_FOR_MY_PACKAGE
+# 2. VCS_VERSIONING_PRETEND_VERSION_FOR_MY_PACKAGE
+# 3. HATCH_VCS_PRETEND_VERSION (generic)
+# 4. VCS_VERSIONING_PRETEND_VERSION (generic)
+```
+
+### Distribution Name Normalization
+
+Distribution names are normalized following PEP 503 semantics, then converted to environment variable format:
+
+```python
+"my-package" → "MY_PACKAGE"
+"My.Package_123" → "MY_PACKAGE_123"
+"pkg--name___v2" → "PKG_NAME_V2"
+```
+
+The normalization:
+1. Uses `packaging.utils.canonicalize_name()` (PEP 503)
+2. Replaces `-` with `_`
+3. Converts to uppercase
+
+## EnvReader: Advanced Environment Variable Reading
+
+The `EnvReader` class is the core utility for reading environment variables with automatic fallback between tool prefixes. While `GlobalOverrides` handles the standard global overrides automatically, `EnvReader` is useful when you need to read custom or distribution-specific environment variables.
+
+### Basic Usage
+
+```python
+from vcs_versioning.overrides import EnvReader
+import os
+
+# Create reader with tool prefix fallback
+reader = EnvReader(
+ tools_names=("HATCH_VCS", "VCS_VERSIONING"),
+ env=os.environ,
+)
+
+# Read simple values
+debug = reader.read("DEBUG")
+timeout = reader.read("SUBPROCESS_TIMEOUT")
+custom = reader.read("MY_CUSTOM_VAR")
+
+# Returns None if not found
+value = reader.read("NONEXISTENT") # None
+```
+
+### Reading Distribution-Specific Variables
+
+When you provide a `dist_name`, `EnvReader` automatically checks distribution-specific variants first:
+
+```python
+reader = EnvReader(
+ tools_names=("HATCH_VCS", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package",
+)
+
+# Reading "PRETEND_VERSION" checks in order:
+# 1. HATCH_VCS_PRETEND_VERSION_FOR_MY_PACKAGE (tool + dist)
+# 2. VCS_VERSIONING_PRETEND_VERSION_FOR_MY_PACKAGE (fallback + dist)
+# 3. HATCH_VCS_PRETEND_VERSION (tool generic)
+# 4. VCS_VERSIONING_PRETEND_VERSION (fallback generic)
+pretend = reader.read("PRETEND_VERSION")
+```
+
+### Reading TOML Configuration
+
+For structured configuration, use `read_toml()` with TypedDict schemas:
+
+```python
+from typing import TypedDict
+from vcs_versioning.overrides import EnvReader
+
+class MyConfigSchema(TypedDict, total=False):
+ """Schema for configuration validation."""
+ local_scheme: str
+ version_scheme: str
+ timeout: int
+ enabled: bool
+
+reader = EnvReader(
+ tools_names=("MY_TOOL", "VCS_VERSIONING"),
+ env={
+ "MY_TOOL_CONFIG": '{local_scheme = "no-local-version", timeout = 120}'
+ }
+)
+
+# Parse TOML with schema validation
+config = reader.read_toml("CONFIG", schema=MyConfigSchema)
+# Result: {'local_scheme': 'no-local-version', 'timeout': 120}
+
+# Invalid fields are automatically removed and logged as warnings
+```
+
+**TOML Format Support:**
+
+- **Inline maps**: `{key = "value", number = 42}`
+- **Full documents**: Multi-line TOML with proper structure
+- **Type coercion**: TOML types are preserved (int, bool, datetime, etc.)
+
+### Error Handling and Diagnostics
+
+`EnvReader` provides helpful diagnostics for common mistakes:
+
+#### Alternative Normalizations
+
+If you use a slightly different normalization, you'll get a warning:
+
+```python
+reader = EnvReader(
+ tools_names=("TOOL",),
+ env={"TOOL_VAR_FOR_MY-PACKAGE": "value"}, # Using dashes
+ dist_name="my-package"
+)
+
+value = reader.read("VAR")
+# Warning: Found environment variable 'TOOL_VAR_FOR_MY-PACKAGE' for dist name 'my-package',
+# but expected 'TOOL_VAR_FOR_MY_PACKAGE'. Consider using the standard normalized name.
+# Returns: "value" (still works!)
+```
+
+#### Typo Detection
+
+If you have a typo in the distribution name suffix, you'll get suggestions:
+
+```python
+reader = EnvReader(
+ tools_names=("TOOL",),
+ env={"TOOL_VAR_FOR_MY_PACKGE": "value"}, # Typo: PACKAGE
+ dist_name="my-package"
+)
+
+value = reader.read("VAR")
+# Warning: Environment variable 'TOOL_VAR_FOR_MY_PACKAGE' not found for dist name 'my-package'
+# (canonicalized as 'my-package'). Did you mean one of these? ['TOOL_VAR_FOR_MY_PACKGE']
+# Returns: None (doesn't match)
+```
+
+### Common Patterns
+
+#### Pattern: Reading Pretend Metadata (TOML)
+
+```python
+from vcs_versioning._overrides import PretendMetadataDict
+from vcs_versioning.overrides import EnvReader
+
+reader = EnvReader(
+ tools_names=("MY_TOOL", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package"
+)
+
+# Read TOML metadata
+metadata = reader.read_toml("PRETEND_METADATA", schema=PretendMetadataDict)
+# Example result: {'node': 'g1337beef', 'distance': 4, 'dirty': False}
+```
+
+#### Pattern: Reading Configuration Overrides
+
+```python
+from vcs_versioning._overrides import ConfigOverridesDict
+from vcs_versioning.overrides import EnvReader
+
+reader = EnvReader(
+ tools_names=("MY_TOOL", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package"
+)
+
+# Read config overrides
+overrides = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+# Example: {'local_scheme': 'no-local-version', 'version_scheme': 'release-branch-semver'}
+```
+
+#### Pattern: Reusing Reader for Multiple Reads
+
+```python
+reader = EnvReader(
+ tools_names=("MY_TOOL", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package"
+)
+
+# Efficient: reuse reader for multiple variables
+pretend_version = reader.read("PRETEND_VERSION")
+pretend_metadata = reader.read_toml("PRETEND_METADATA", schema=PretendMetadataDict)
+config_overrides = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+custom_setting = reader.read("CUSTOM_SETTING")
+```
+
+### When to Use EnvReader
+
+**Use `EnvReader` when you need to:**
+
+- Read custom environment variables beyond the standard global overrides
+- Support distribution-specific configuration
+- Parse structured TOML data from environment variables
+- Implement your own override system on top of vcs-versioning
+
+**Don't use `EnvReader` for:**
+
+- Standard global overrides (debug, timeout, etc.) - use `GlobalOverrides` instead
+- One-time reads - it's designed for efficiency with multiple reads
+
+### EnvReader vs GlobalOverrides
+
+| Feature | `GlobalOverrides` | `EnvReader` |
+|---------|------------------|-------------|
+| **Purpose** | Manage standard global overrides | Read any custom env vars |
+| **Context Manager** | ✅ Yes | ❌ No |
+| **Auto-configures logging** | ✅ Yes | ❌ No |
+| **Tool fallback** | ✅ Automatic | ✅ Automatic |
+| **Dist-specific vars** | ❌ No | ✅ Yes |
+| **TOML parsing** | ❌ No | ✅ Yes |
+| **Use case** | Entry point setup | Custom config reading |
+
+**Typical usage together:**
+
+```python
+from vcs_versioning.overrides import GlobalOverrides, EnvReader
+import os
+
+# Apply global overrides
+with GlobalOverrides.from_env("MY_TOOL"):
+ # Read custom configuration
+ reader = EnvReader(
+ tools_names=("MY_TOOL", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package"
+ )
+
+ custom_config = reader.read_toml("CUSTOM_CONFIG", schema=MySchema)
+
+ # Both global overrides and custom config are now available
+ version = detect_version_with_config(custom_config)
+```
+
+## Environment Variable Patterns
+
+### Global Override Patterns
+
+| Override | Environment Variables | Example |
+|----------|----------------------|---------|
+| Debug | `{TOOL}_DEBUG`
`VCS_VERSIONING_DEBUG` | `HATCH_VCS_DEBUG=1` |
+| Subprocess Timeout | `{TOOL}_SUBPROCESS_TIMEOUT`
`VCS_VERSIONING_SUBPROCESS_TIMEOUT` | `HATCH_VCS_SUBPROCESS_TIMEOUT=120` |
+| Mercurial Command | `{TOOL}_HG_COMMAND`
`VCS_VERSIONING_HG_COMMAND` | `HATCH_VCS_HG_COMMAND=chg` |
+| Source Date Epoch | `SOURCE_DATE_EPOCH` | `SOURCE_DATE_EPOCH=1672531200` |
+
+### Distribution-Specific Patterns
+
+| Override | Environment Variables | Example |
+|----------|----------------------|---------|
+| Pretend Version (specific) | `{TOOL}_PRETEND_VERSION_FOR_{DIST}`
`VCS_VERSIONING_PRETEND_VERSION_FOR_{DIST}` | `HATCH_VCS_PRETEND_VERSION_FOR_MY_PKG=1.0.0` |
+| Pretend Version (generic) | `{TOOL}_PRETEND_VERSION`
`VCS_VERSIONING_PRETEND_VERSION` | `HATCH_VCS_PRETEND_VERSION=1.0.0` |
+| Pretend Metadata (specific) | `{TOOL}_PRETEND_METADATA_FOR_{DIST}`
`VCS_VERSIONING_PRETEND_METADATA_FOR_{DIST}` | `HATCH_VCS_PRETEND_METADATA_FOR_MY_PKG='{node="g123", distance=4}'` |
+| Pretend Metadata (generic) | `{TOOL}_PRETEND_METADATA`
`VCS_VERSIONING_PRETEND_METADATA` | `HATCH_VCS_PRETEND_METADATA='{dirty=true}'` |
+| Config Overrides (specific) | `{TOOL}_OVERRIDES_FOR_{DIST}`
`VCS_VERSIONING_OVERRIDES_FOR_{DIST}` | `HATCH_VCS_OVERRIDES_FOR_MY_PKG='{"local_scheme": "no-local-version"}'` |
+
+## Complete Integration Example
+
+Here's a complete example of integrating vcs-versioning into a build backend:
+
+```python
+# my_build_backend.py
+from __future__ import annotations
+
+from typing import Any
+
+from vcs_versioning.overrides import GlobalOverrides
+from vcs_versioning import infer_version_string
+
+
+def get_version_for_build(
+ dist_name: str,
+ pyproject_data: dict[str, Any],
+ config_overrides: dict[str, Any] | None = None,
+) -> str:
+ """Get version for build, using MYBUILD_* environment variables.
+
+ Args:
+ dist_name: The distribution/package name (e.g., "my-package")
+ pyproject_data: Parsed pyproject.toml data
+ config_overrides: Optional configuration overrides
+
+ Returns:
+ The computed version string
+ """
+
+ # Apply global overrides with custom prefix
+ # Logging is automatically configured based on MYBUILD_DEBUG
+ with GlobalOverrides.from_env("MYBUILD"):
+ # Get version - all subprocess calls and logging respect MYBUILD_* vars
+ # dist_name is used for distribution-specific env var lookups
+ version = infer_version_string(
+ dist_name=dist_name,
+ pyproject_data=pyproject_data,
+ overrides=config_overrides,
+ )
+
+ return version
+```
+
+### Usage
+
+The function is called with the distribution name, enabling package-specific overrides:
+
+```python
+# Example: Using in a build backend
+version = get_version_for_build(
+ dist_name="my-package",
+ pyproject_data=parsed_pyproject,
+ config_overrides={"local_scheme": "no-local-version"},
+)
+```
+
+Environment variables can override behavior per package:
+
+```bash
+# Enable debug logging for this tool only
+export MYBUILD_DEBUG=1
+
+# Or use universal VCS_VERSIONING prefix
+export VCS_VERSIONING_DEBUG=1
+
+# Override subprocess timeout
+export MYBUILD_SUBPROCESS_TIMEOUT=120
+
+# Pretend version for specific package (dist_name="my-package")
+export MYBUILD_PRETEND_VERSION_FOR_MY_PACKAGE=1.2.3.dev4
+
+# Or generic pretend version (applies to all packages)
+export MYBUILD_PRETEND_VERSION=1.2.3
+
+python -m build
+```
+
+## Testing with Custom Prefixes
+
+When testing your integration, you can mock the environment:
+
+```python
+import pytest
+from vcs_versioning.overrides import GlobalOverrides
+
+
+def test_with_custom_overrides():
+ """Test version detection with custom override prefix."""
+ mock_env = {
+ "MYTEST_DEBUG": "1",
+ "MYTEST_SUBPROCESS_TIMEOUT": "60",
+ "SOURCE_DATE_EPOCH": "1672531200",
+ }
+
+ with GlobalOverrides.from_env("MYTEST", env=mock_env) as overrides:
+ # Verify overrides loaded correctly
+ assert overrides.debug != False
+ assert overrides.subprocess_timeout == 60
+ assert overrides.source_date_epoch == 1672531200
+
+ # Test your version detection logic
+ version = detect_version_somehow()
+ assert version is not None
+
+
+def test_with_vcs_versioning_fallback():
+ """Test that VCS_VERSIONING prefix works as fallback."""
+ mock_env = {
+ "VCS_VERSIONING_DEBUG": "1",
+ # No MYTEST_ variables
+ }
+
+ with GlobalOverrides.from_env("MYTEST", env=mock_env) as overrides:
+ # Should use VCS_VERSIONING fallback
+ assert overrides.debug != False
+```
+
+## Advanced Usage
+
+### Inspecting Active Overrides
+
+```python
+from vcs_versioning import get_active_overrides
+
+# Outside any context
+overrides = get_active_overrides()
+assert overrides is None
+
+# Inside a context
+with GlobalOverrides.from_env("HATCH_VCS"):
+ overrides = get_active_overrides()
+ assert overrides is not None
+ assert overrides.tool == "HATCH_VCS"
+```
+
+### Using Accessor Functions Directly
+
+```python
+from vcs_versioning import (
+ get_debug_level,
+ get_subprocess_timeout,
+ get_hg_command,
+ get_source_date_epoch,
+)
+
+with GlobalOverrides.from_env("HATCH_VCS"):
+ # These functions return values from the active context
+ debug = get_debug_level()
+ timeout = get_subprocess_timeout()
+ hg_cmd = get_hg_command()
+ epoch = get_source_date_epoch()
+```
+
+Outside a context, these functions fall back to reading `os.environ` directly for backward compatibility.
+
+### Custom Distribution-Specific Overrides
+
+If you need to read custom dist-specific overrides:
+
+```python
+from vcs_versioning.overrides import EnvReader
+import os
+
+# Read a custom override
+reader = EnvReader(
+ tools_names=("HATCH_VCS", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package",
+)
+custom_value = reader.read("MY_CUSTOM_SETTING")
+
+# This checks in order:
+# 1. HATCH_VCS_MY_CUSTOM_SETTING_FOR_MY_PACKAGE
+# 2. VCS_VERSIONING_MY_CUSTOM_SETTING_FOR_MY_PACKAGE
+# 3. HATCH_VCS_MY_CUSTOM_SETTING
+# 4. VCS_VERSIONING_MY_CUSTOM_SETTING
+```
+
+`EnvReader` includes fuzzy matching and helpful warnings if users specify distribution names incorrectly.
+
+## Best Practices
+
+### 1. Choose Descriptive Prefixes
+
+Use clear, tool-specific prefixes:
+- ✅ `HATCH_VCS`, `MYBUILD`, `POETRY_VCS`
+- ❌ `TOOL`, `MY`, `X`
+
+### 2. Apply Context at Entry Points
+
+Apply the `GlobalOverrides` context once at your tool's entry point, not repeatedly:
+
+```python
+# ✅ Good - apply once at entry point
+def main():
+ with GlobalOverrides.from_env("HATCH_VCS"):
+ # All operations here have access to overrides
+ build_project()
+
+# ❌ Bad - repeated context application
+def build_project():
+ with GlobalOverrides.from_env("HATCH_VCS"):
+ get_version()
+
+ with GlobalOverrides.from_env("HATCH_VCS"): # Wasteful
+ write_version_file()
+```
+
+### 3. Document Your Environment Variables
+
+Document the environment variables your tool supports, including the fallback behavior:
+
+```markdown
+## Environment Variables
+
+- `HATCH_VCS_DEBUG`: Enable debug logging (falls back to `VCS_VERSIONING_DEBUG`)
+- `HATCH_VCS_PRETEND_VERSION_FOR_{DIST}`: Override version for distribution
+```
+
+### 4. Test Both Prefixes
+
+Test that both your custom prefix and the `VCS_VERSIONING_*` fallback work:
+
+```python
+def test_custom_prefix():
+ with GlobalOverrides.from_env("MYTOOL", env={"MYTOOL_DEBUG": "1"}):
+ ...
+
+def test_fallback_prefix():
+ with GlobalOverrides.from_env("MYTOOL", env={"VCS_VERSIONING_DEBUG": "1"}):
+ ...
+```
+
+### 5. Avoid Nesting Contexts
+
+Don't nest `GlobalOverrides` contexts - it's rarely needed and can be confusing:
+
+```python
+# ❌ Avoid this
+with GlobalOverrides.from_env("TOOL1"):
+ with GlobalOverrides.from_env("TOOL2"): # Inner context shadows outer
+ ...
+```
+
+## Thread Safety
+
+The override system uses `contextvars.ContextVar` for thread-local storage, making it safe for concurrent execution:
+
+```python
+import concurrent.futures
+from vcs_versioning.overrides import GlobalOverrides
+
+def build_package(tool_prefix: str) -> str:
+ with GlobalOverrides.from_env(tool_prefix):
+ return get_version()
+
+# Each thread has its own override context
+with concurrent.futures.ThreadPoolExecutor() as executor:
+ futures = [
+ executor.submit(build_package, "TOOL1"),
+ executor.submit(build_package, "TOOL2"),
+ ]
+ results = [f.result() for f in futures]
+```
+
+## Migration from Direct Environment Reads
+
+If you're migrating code that directly reads environment variables:
+
+```python
+# Before
+import os
+
+def my_function():
+ debug = os.environ.get("SETUPTOOLS_SCM_DEBUG")
+ timeout = int(os.environ.get("SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT", "40"))
+ # ...
+
+# After
+from vcs_versioning.overrides import GlobalOverrides
+
+def main():
+ with GlobalOverrides.from_env("MYTOOL"):
+ my_function() # Now uses override context automatically
+
+def my_function():
+ # No changes needed! Internal vcs-versioning code uses the context
+ pass
+```
+
+All internal vcs-versioning modules automatically use the active override context, so you don't need to change their usage.
+
+## Experimental Integrator API
+
+!!! warning "Experimental"
+ This API is marked as experimental and may change in future versions.
+ Use with caution in production code.
+
+vcs-versioning provides helper functions for integrators to build configurations with proper override priority handling.
+
+### Overview
+
+The experimental API provides:
+- `PyProjectData`: Public class for composing pyproject.toml data
+- `build_configuration_from_pyproject()`: Substantial orchestration helper for building Configuration
+
+### Priority Order
+
+When building configurations, overrides are applied in this priority order (highest to lowest):
+
+1. **Environment TOML overrides** - `TOOL_OVERRIDES_FOR_DIST`, `TOOL_OVERRIDES`
+2. **Integrator overrides** - Python arguments passed by the integrator
+3. **Config file** - `pyproject.toml` `[tool.vcs-versioning]` section
+4. **Defaults** - vcs-versioning defaults
+
+This ensures that:
+- Users can always override via environment variables
+- Integrators can provide their own defaults/transformations
+- Config file settings are respected
+- Sensible defaults are always available
+
+### Basic Workflow
+
+```python
+from vcs_versioning import (
+ PyProjectData,
+ build_configuration_from_pyproject,
+ infer_version_string,
+)
+from vcs_versioning.overrides import GlobalOverrides
+
+def get_version_for_my_tool(pyproject_path="pyproject.toml", dist_name=None):
+ """Complete integrator workflow."""
+ # 1. Setup global overrides context (handles env vars, logging, etc.)
+ with GlobalOverrides.from_env("MY_TOOL", dist_name=dist_name):
+
+ # 2. Load pyproject data
+ pyproject = PyProjectData.from_file(pyproject_path)
+
+ # 3. Build configuration with proper override priority
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name=dist_name,
+ # Optional: integrator overrides (override config file, not env)
+ # local_scheme="no-local-version",
+ )
+
+ # 4. Infer version
+ version = infer_version_string(
+ dist_name=dist_name or pyproject.project_name,
+ pyproject_data=pyproject,
+ )
+
+ return version
+```
+
+### PyProjectData Composition
+
+Integrators can create `PyProjectData` in two ways:
+
+#### 1. From File (Recommended)
+
+```python
+from vcs_versioning import PyProjectData
+
+# Load from pyproject.toml (reads tool.vcs-versioning section)
+pyproject = PyProjectData.from_file("pyproject.toml")
+```
+
+#### 2. Manual Composition
+
+If your tool already has its own TOML reading logic:
+
+```python
+from pathlib import Path
+from vcs_versioning import PyProjectData
+
+pyproject = PyProjectData(
+ path=Path("pyproject.toml"),
+ tool_name="vcs-versioning",
+ project={"name": "my-pkg", "dynamic": ["version"]},
+ section={"local_scheme": "no-local-version"},
+ is_required=True,
+ section_present=True,
+ project_present=True,
+ build_requires=["vcs-versioning"],
+)
+```
+
+### Building Configuration with Overrides
+
+The `build_configuration_from_pyproject()` function orchestrates the complete configuration workflow:
+
+```python
+from vcs_versioning import build_configuration_from_pyproject
+
+config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name="my-package", # Optional: override project.name
+ # Integrator overrides (middle priority):
+ version_scheme="release-branch-semver",
+ local_scheme="no-local-version",
+)
+```
+
+**What it does:**
+1. Extracts config from `pyproject_data.section`
+2. Determines `dist_name` (argument > config > project.name)
+3. Merges integrator overrides (kwargs)
+4. Reads and applies environment TOML overrides
+5. Builds and validates `Configuration` instance
+
+### Environment TOML Overrides
+
+Users can override configuration via environment variables:
+
+```bash
+# Inline TOML format
+export MY_TOOL_OVERRIDES='{local_scheme = "no-local-version"}'
+
+# Distribution-specific
+export MY_TOOL_OVERRIDES_FOR_MY_PACKAGE='{version_scheme = "guess-next-dev"}'
+```
+
+These always have the highest priority, even over integrator overrides.
+
+### Complete Example: Hatch Integration
+
+```python
+# In your hatch plugin
+from pathlib import Path
+from vcs_versioning import (
+ PyProjectData,
+ build_configuration_from_pyproject,
+ infer_version_string,
+)
+from vcs_versioning.overrides import GlobalOverrides
+
+
+class HatchVCSVersion:
+ """Hatch version source plugin using vcs-versioning."""
+
+ def get_version_data(self):
+ """Get version from VCS."""
+ # Setup global context with HATCH_VCS prefix
+ with GlobalOverrides.from_env("HATCH_VCS", dist_name=self.config["dist-name"]):
+
+ # Load pyproject data
+ pyproject_path = Path(self.root) / "pyproject.toml"
+ pyproject = PyProjectData.from_file(pyproject_path)
+
+ # Build configuration
+ # Hatch-specific transformations can go here as kwargs
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name=self.config["dist-name"],
+ root=self.root, # Hatch provides the root
+ )
+
+ # Get version
+ version = infer_version_string(
+ dist_name=self.config["dist-name"],
+ pyproject_data=pyproject,
+ )
+
+ return {"version": version}
+```
+
+### Tool Section Naming
+
+**Important:** The public experimental API only accepts `tool.vcs-versioning` sections.
+
+```toml
+# ✅ Correct - use tool.vcs-versioning
+[tool.vcs-versioning]
+version_scheme = "guess-next-dev"
+local_scheme = "no-local-version"
+
+# ❌ Wrong - tool.setuptools_scm not supported in public API
+[tool.setuptools_scm]
+version_scheme = "guess-next-dev"
+```
+
+Only `setuptools_scm` should use `tool.setuptools_scm` (for backward compatibility during transition).
+
+### API Reference
+
+#### `PyProjectData.from_file()`
+
+```python
+@classmethod
+def from_file(
+ cls,
+ path: str | os.PathLike = "pyproject.toml",
+ *,
+ _tool_names: list[str] | None = None,
+) -> PyProjectData:
+ """Load PyProjectData from pyproject.toml.
+
+ Public API reads tool.vcs-versioning section.
+ Internal: pass _tool_names for multi-tool support.
+ """
+```
+
+#### `build_configuration_from_pyproject()`
+
+```python
+def build_configuration_from_pyproject(
+ pyproject_data: PyProjectData,
+ *,
+ dist_name: str | None = None,
+ **integrator_overrides: Any,
+) -> Configuration:
+ """Build Configuration with full workflow orchestration.
+
+ Priority order:
+ 1. Environment TOML overrides (highest)
+ 2. Integrator **integrator_overrides
+ 3. pyproject_data.section configuration
+ 4. Configuration defaults (lowest)
+ """
+```
+
+### Migration from Direct API Usage
+
+If you were previously using internal APIs directly:
+
+**Before:**
+```python
+from vcs_versioning._config import Configuration
+
+config = Configuration.from_file("pyproject.toml", dist_name="my-pkg")
+```
+
+**After (Experimental API):**
+```python
+from vcs_versioning import PyProjectData, build_configuration_from_pyproject
+from vcs_versioning.overrides import GlobalOverrides
+
+with GlobalOverrides.from_env("MY_TOOL", dist_name="my-pkg"):
+ pyproject = PyProjectData.from_file("pyproject.toml")
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name="my-pkg",
+ )
+```
+
+The experimental API provides better separation of concerns and proper override priority handling.
+
+## See Also
+
+- [Overrides Documentation](overrides.md) - User-facing documentation for setuptools-scm
+- [Configuration Guide](config.md) - Configuring vcs-versioning behavior
+- [Extending Guide](extending.md) - Creating custom version schemes and plugins
+
diff --git a/docs/overrides.md b/docs/overrides.md
index 4d136db2..72e57d3a 100644
--- a/docs/overrides.md
+++ b/docs/overrides.md
@@ -1,26 +1,58 @@
# Overrides
-## pretend versions
+!!! info "For Integrators"
-setuptools-scm provides a mechanism to override the version number build time.
+ If you're building a tool that integrates vcs-versioning (like hatch-vcs), see the [Integrator Guide](integrators.md) for using the overrides API with custom prefixes and the `GlobalOverrides` context manager.
-the environment variable `SETUPTOOLS_SCM_PRETEND_VERSION` is used
+## About Overrides
+
+Environment variables provide runtime configuration overrides, primarily useful in CI/CD
+environments where you need different behavior without modifying `pyproject.toml` or code.
+
+All environment variables support both `SETUPTOOLS_SCM_*` and `VCS_VERSIONING_*` prefixes. The `VCS_VERSIONING_*` prefix serves as a universal fallback that works across all tools using vcs-versioning.
+
+## Version Detection Overrides
+
+### Pretend Versions
+
+Override the version number at build time.
+
+**setuptools-scm usage:**
+
+The environment variable `SETUPTOOLS_SCM_PRETEND_VERSION` (or `VCS_VERSIONING_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_${DIST_NAME}`
-where the dist name normalization follows adapted PEP 503 semantics.
+!!! warning ""
-## pretend metadata
+ it is strongly recommended to use distribution-specific pretend versions
+ (see below).
-setuptools-scm provides a mechanism to override individual version metadata fields at build time.
+`SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}` or `VCS_VERSIONING_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
+ avoid possible collisions with third party distributions
+ also using vcs-versioning.
-The environment variable `SETUPTOOLS_SCM_PRETEND_METADATA` accepts a TOML inline table
-with field overrides for the ScmVersion object.
+ The dist name normalization follows adapted PEP 503 semantics, with one or
+ more of ".-\_" being replaced by a single "\_", and the name being upper-cased.
-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.
+ This will take precedence over the generic ``SETUPTOOLS_SCM_PRETEND_VERSION`` or ``VCS_VERSIONING_PRETEND_VERSION``.
-### Supported fields
+### Pretend Metadata
+
+Override individual version metadata fields at build time.
+
+**setuptools-scm usage:**
+
+`SETUPTOOLS_SCM_PRETEND_METADATA`
+: Accepts a TOML inline table with field overrides for the ScmVersion object.
+
+`SETUPTOOLS_SCM_PRETEND_METADATA_FOR_${DIST_NAME}`
+: Same as above but specific to a package (recommended over the generic version).
+ The dist name normalization follows adapted PEP 503 semantics.
+
+#### Supported fields
The following ScmVersion fields can be overridden:
@@ -33,7 +65,7 @@ The following ScmVersion fields can be overridden:
- `preformatted` (bool): Whether the version string is preformatted
- `tag`: The version tag (can be string or version object)
-### Examples
+#### Examples
Override commit hash and distance:
```bash
@@ -59,7 +91,7 @@ export SETUPTOOLS_SCM_PRETEND_METADATA_FOR_MY_PACKAGE='{node="g1234567", distanc
This ensures consistency with setuptools-scm's automatic node ID formatting.
-### Use case: CI/CD environments
+#### 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:
@@ -80,14 +112,65 @@ export SETUPTOOLS_SCM_PRETEND_VERSION="1.2.3.dev4+g1337beef"
export SETUPTOOLS_SCM_PRETEND_METADATA='{node="g1337beef", distance=4}'
```
-## config overrides
+### Debug Logging
+
+Enable debug output from vcs-versioning.
+
+**setuptools-scm usage:**
+
+`SETUPTOOLS_SCM_DEBUG` or `VCS_VERSIONING_DEBUG`
+: Enable debug logging for version detection and processing.
+ Can be set to:
+ - `1` or any non-empty value to enable DEBUG level logging
+ - A level name: `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `CRITICAL` (case-insensitive)
+ - A specific log level integer: `10` (DEBUG), `20` (INFO), `30` (WARNING), etc.
+ - `0` to disable debug logging
+
+### Reproducible Builds
+
+Control timestamps for reproducible builds (from [reproducible-builds.org](https://reproducible-builds.org/docs/source-date-epoch/)).
+
+`SOURCE_DATE_EPOCH`
+: Used as the timestamp from which the ``node-and-date`` and ``node-and-timestamp``
+ local parts are derived, otherwise the current time is used.
+ This is a standard environment variable supported by many build tools.
+
+## setuptools-scm Overrides
+
+### Configuration Overrides
+
+`SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}`
+: A TOML inline table to override configuration from `pyproject.toml`.
+ This allows overriding any configuration option at build time, which is particularly useful
+ in CI/CD environments where you might want different behavior without modifying `pyproject.toml`.
+
+ **Example:**
+ ```bash
+ # Override local_scheme for CI builds
+ export SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE='{"local_scheme": "no-local-version"}'
+ ```
+
+### SCM Root Discovery
+
+`SETUPTOOLS_SCM_IGNORE_VCS_ROOTS`
+: A ``os.pathsep`` separated list of directory names to ignore for root finding.
+
+### Mercurial Command
+
+`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.
-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
-## subprocess timeouts
+`SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT`
+: Override the subprocess timeout (default: 40 seconds).
+ The default should work for most needs. However, users with git lfs + windows reported
+ situations where this was not enough.
-The environment variable `SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT` allows to override the subprocess timeout.
-The default is 40 seconds and should work for most needs. However, users with git lfs + windows reported
-situations where this was not enough.
+ **Example:**
+ ```bash
+ # Increase timeout to 120 seconds
+ export SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT=120
+ ```
diff --git a/docs/usage.md b/docs/usage.md
index b21bf2b6..f94aab69 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -139,6 +139,9 @@ $ python -m setuptools_scm --help
## At runtime
+!!! tip "Recommended Approach"
+ Use standard Python metadata (`importlib.metadata`) for runtime version access. This is the standard, recommended approach that works with any packaging system.
+
### Python Metadata
The standard method to retrieve the version number at runtime is via
@@ -174,6 +177,9 @@ print(v.version_tuple)
### Via setuptools_scm (strongly discouraged)
+!!! warning "Discouraged API"
+ Direct use of `setuptools_scm.get_version()` at runtime is strongly discouraged. Use `importlib.metadata` instead.
+
While the most simple **looking** way to use `setuptools_scm` at
runtime is:
diff --git a/mkdocs.yml b/mkdocs.yml
index d28d85d9..63489b5e 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -8,12 +8,14 @@ nav:
- integrations.md
- extending.md
- overrides.md
+ - integrators.md
- changelog.md
theme:
name: material
watch:
-- src/setuptools_scm
+- setuptools-scm/src/setuptools_scm
+- vcs-versioning/src/vcs_versioning
- docs
markdown_extensions:
- def_list
@@ -30,10 +32,13 @@ plugins:
default_handler: python
handlers:
python:
- paths: [ src ]
-
+ paths: [ setuptools-scm/src, vcs-versioning/src ]
+ inventories:
+ - https://docs.python.org/3/objects.inv
options:
separate_signature: true
show_signature_annotations: true
allow_inspection: true
show_root_heading: true
+ extensions:
+ - griffe_public_wildcard_imports
diff --git a/mypy.ini b/mypy.ini
deleted file mode 100644
index ef383f74..00000000
--- a/mypy.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[mypy]
-python_version = 3.8
-warn_return_any = True
-warn_unused_configs = True
-mypy_path = $MYPY_CONFIG_FILE_DIR/src
-strict = true
diff --git a/nextgen/vcs-versioning/pyproject.toml b/nextgen/vcs-versioning/pyproject.toml
deleted file mode 100644
index c34c5dd1..00000000
--- a/nextgen/vcs-versioning/pyproject.toml
+++ /dev/null
@@ -1,66 +0,0 @@
-[build-system]
-build-backend = "hatchling.build"
-requires = [
- "hatchling",
-]
-
-[project]
-name = "vcs-versioning"
-description = "the blessed package to manage your versions by vcs metadata"
-readme = "README.md"
-keywords = [
-]
-license = "MIT"
-authors = [
- { name = "Ronny Pfannschmidt", email = "opensource@ronnypfannschmidt.de" },
-]
-requires-python = ">=3.8"
-classifiers = [
- "Development Status :: 1 - Planning",
- "Programming Language :: Python",
- "Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- "Programming Language :: Python :: 3.13",
-]
-dynamic = [
- "version",
-]
-dependencies = [
-]
-[project.urls]
-Documentation = "https://github.com/unknown/vcs-versioning#readme"
-Issues = "https://github.com/unknown/vcs-versioning/issues"
-Source = "https://github.com/unknown/vcs-versioning"
-
-[tool.hatch.version]
-path = "vcs_versioning/__about__.py"
-
-[tool.hatch.envs.default]
-dependencies = [
- "pytest",
- "pytest-cov",
-]
-[tool.hatch.envs.default.scripts]
-cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=vcs_versioning --cov=tests {args}"
-no-cov = "cov --no-cov {args}"
-
-[[tool.hatch.envs.test.matrix]]
-python = [ "38", "39", "310", "311", "312", "313" ]
-
-[tool.coverage.run]
-branch = true
-parallel = true
-omit = [
- "vcs_versioning/__about__.py",
-]
-
-[tool.coverage.report]
-exclude_lines = [
- "no cov",
- "if __name__ == .__main__.:",
- "if TYPE_CHECKING:",
-]
diff --git a/nextgen/vcs-versioning/tests/__init__.py b/nextgen/vcs-versioning/tests/__init__.py
deleted file mode 100644
index 9d48db4f..00000000
--- a/nextgen/vcs-versioning/tests/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from __future__ import annotations
diff --git a/nextgen/vcs-versioning/vcs_versioning/__init__.py b/nextgen/vcs-versioning/vcs_versioning/__init__.py
deleted file mode 100644
index 9d48db4f..00000000
--- a/nextgen/vcs-versioning/vcs_versioning/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from __future__ import annotations
diff --git a/pyproject.toml b/pyproject.toml
index 4944fdc2..19649e4e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,172 +1,98 @@
-
-
-[build-system]
-build-backend = "_own_version_helper:build_meta"
-requires = [
- "setuptools>=61",
- 'tomli<=2.0.2; python_version < "3.11"',
-]
-backend-path = [
- ".",
- "src",
-]
+# uv sync --all-packages --all-groups # Install all packages with all dependency groups
[project]
-name = "setuptools-scm"
-description = "the blessed package to manage your versions by scm tags"
-readme = "README.md"
-license.file = "LICENSE"
-authors = [
- {name="Ronny Pfannschmidt", email="opensource@ronnypfannschmidt.de"}
-]
-requires-python = ">=3.8"
-classifiers = [
- "Development Status :: 5 - Production/Stable",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: MIT License",
- "Programming Language :: Python",
- "Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- "Programming Language :: Python :: 3.13",
- "Topic :: Software Development :: Libraries",
- "Topic :: Software Development :: Version Control",
- "Topic :: System :: Software Distribution",
- "Topic :: Utilities",
-]
-dynamic = [
- "version",
-]
+name = "vcs-versioning.workspace"
+description = "workspace configuration for this monorepo"
+version = "0.1+private"
+requires-python = ">=3.10"
dependencies = [
- "packaging>=20",
- # https://github.com/pypa/setuptools-scm/issues/1112 - re-pin in a breaking release
- "setuptools", # >= 61",
- 'tomli>=1; python_version < "3.11"',
- 'typing-extensions; python_version < "3.10"',
+ "vcs-versioning",
+ "setuptools-scm",
+
]
-[project.optional-dependencies]
-rich = ["rich"]
-simple = []
-toml = []
+
+[project.scripts]
+create-release-proposal = "vcs_versioning_workspace.create_release_proposal:main"
[dependency-groups]
docs = [
"mkdocs",
"mkdocs-include-markdown-plugin",
"mkdocs-material",
- "mkdocstrings[python]",
+ "mkdocstrings[python]>=0.29", # 0.29 renamed import to inventories
"pygments",
+ "griffe-public-wildcard-imports",
]
-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",
- "griffe",
- "flake8",
+typing = [
+ "types-setuptools",
+]
+release = [
+ "towncrier>=23.11.0",
+ "PyGithub>=2.0.0; sys_platform != 'win32'",
]
-[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"
-
-[project.entry-points."pipx.run"]
-setuptools-scm = "setuptools_scm._cli:main"
-setuptools_scm = "setuptools_scm._cli:main"
-
-[project.entry-points."setuptools.file_finders"]
-setuptools_scm = "setuptools_scm._file_finders:find_files"
-
-[project.entry-points."setuptools.finalize_distribution_options"]
-setuptools_scm = "setuptools_scm._integration.setuptools:infer_version"
-
-[project.entry-points."setuptools_scm.files_command"]
-".git" = "setuptools_scm._file_finders.git:git_find_files"
-".hg" = "setuptools_scm._file_finders.hg:hg_find_files"
-
-[project.entry-points."setuptools_scm.files_command_fallback"]
-".git_archival.txt" = "setuptools_scm._file_finders.git:git_archive_find_files"
-".hg_archival.txt" = "setuptools_scm._file_finders.hg:hg_archive_find_files"
-
-[project.entry-points."setuptools_scm.local_scheme"]
-dirty-tag = "setuptools_scm.version:get_local_dirty_tag"
-no-local-version = "setuptools_scm.version:get_no_local_node"
-node-and-date = "setuptools_scm.version:get_local_node_and_date"
-node-and-timestamp = "setuptools_scm.version:get_local_node_and_timestamp"
-[project.entry-points."setuptools_scm.parse_scm"]
-".git" = "setuptools_scm.git:parse"
-".hg" = "setuptools_scm.hg:parse"
+[tool.pytest.ini_options]
+minversion = "8.2"
+testpaths = ["setuptools-scm/testing_scm", "vcs-versioning/testing_vcs"]
+xfail_strict = true
+addopts = ["-ra", "--strict-config", "--strict-markers", "-p", "vcs_versioning.test_api"]
+markers = [
+ "issue(id): reference to github issue",
+ "skip_commit: allows to skip committing in the helpers",
+]
-[project.entry-points."setuptools_scm.parse_scm_fallback"]
-".git_archival.txt" = "setuptools_scm.git:parse_archival"
-".hg_archival.txt" = "setuptools_scm.hg:parse_archival"
-PKG-INFO = "setuptools_scm.fallbacks:parse_pkginfo"
-"pyproject.toml" = "setuptools_scm.fallbacks:fallback_version"
-"setup.py" = "setuptools_scm.fallbacks:fallback_version"
+[tool.mypy]
-[project.entry-points."setuptools_scm.version_scheme"]
-"calver-by-date" = "setuptools_scm.version:calver_by_date"
-"guess-next-dev" = "setuptools_scm.version:guess_next_dev_version"
-"no-guess-dev" = "setuptools_scm.version:no_guess_dev_version"
-"only-version" = "setuptools_scm.version:only_version"
-"post-release" = "setuptools_scm.version:postrelease_version"
-"python-simplified-semver" = "setuptools_scm.version:simplified_semver_version"
-"release-branch-semver" = "setuptools_scm.version:release_branch_semver_version"
+enable_error_code = [
+ "ignore-without-code",
+ "redundant-expr",
+ "truthy-bool",
+]
+strict = true
+warn_unused_configs = true
+warn_redundant_casts = true
+warn_unused_ignores = true
+warn_return_any = true
+warn_unreachable = true
+disallow_untyped_defs = true
+disallow_any_generics = true
+check_untyped_defs = true
+no_implicit_reexport = true
+
+# Workspace-specific paths
+mypy_path = "vcs-versioning/src:setuptools-scm/src"
+explicit_package_bases = true
+
+[[tool.mypy.overrides]]
+module = [
+ "vcs-versioning._own_version_of_vcs_versioning",
+ "setuptools-scm._own_version_helper",
+]
+# Version helper files use imports before they're installed
+ignore_errors = true
-[tool.setuptools.packages.find]
-where = ["src"]
-namespaces = false
+[tool.uv]
+package = true
-[tool.setuptools.dynamic]
-version = { attr = "_own_version_helper.version"}
+[tool.uv.workspace]
+members = ["vcs-versioning", "setuptools-scm"]
-[tool.setuptools_scm]
+[tool.uv.sources]
+vcs-versioning = { workspace = true }
+setuptools-scm = { workspace = true }
[tool.ruff]
-lint.extend-select = ["YTT", "B", "C4", "DTZ", "ISC", "LOG", "G", "PIE", "PYI", "PT", "FLY", "I", "C90", "PERF", "W", "PGH", "PLE", "UP", "FURB", "RUF"]
-lint.ignore = ["B028", "LOG015", "PERF203"]
-lint.preview = true
-
-[tool.ruff.lint.isort]
-force-single-line = true
-from-first = false
-lines-between-types = 1
-order-by-type = true
-[tool.repo-review]
-ignore = ["PP305", "GH103", "GH212", "MY100", "PC111", "PC160", "PC170", "PC180", "PC901"]
+lint.extend-select = ["UP", "I", "B"]
-[tool.pytest.ini_options]
-minversion = "8"
-testpaths = ["testing"]
-timeout = 300 # 5 minutes timeout per test for CI protection
-filterwarnings = [
- "error",
- "ignore:.*tool\\.setuptools_scm.*",
- "ignore:.*git archive did not support describe output.*:UserWarning",
-]
-log_level = "debug"
-log_cli_level = "info"
-# disable unraisable until investigated
-addopts = ["-ra", "--strict-config", "--strict-markers"]
-markers = [
- "issue(id): reference to github issue",
- "skip_commit: allows to skip committing in the helpers",
-]
-
-[tool.uv]
-default-groups = ["test", "docs"]
+[tool.repo-review]
+ignore = [
+ "PP002", # this is a uv workspace
+ "PP003", # this tool is drunk
+ "PP304", # log_cli shoudltn be the default
+ "PP309", # filterwarnings is evil unless needed, it should never be error
+ "PY007", # no to tasks
+ "PY005", # no tests folder for the workspace
+ "PY003", # each subproject has one
+
+ "GH103", "GH212", "MY100", "PC111", "PC160", "PC170", "PC180", "PC901"]
\ No newline at end of file
diff --git a/CHANGELOG.md b/setuptools-scm/CHANGELOG.md
similarity index 99%
rename from CHANGELOG.md
rename to setuptools-scm/CHANGELOG.md
index b588430e..f0c0a3bc 100644
--- a/CHANGELOG.md
+++ b/setuptools-scm/CHANGELOG.md
@@ -1,5 +1,6 @@
# Changelog
+
## v9.2.2
diff --git a/LICENSE b/setuptools-scm/LICENSE
similarity index 100%
rename from LICENSE
rename to setuptools-scm/LICENSE
diff --git a/MANIFEST.in b/setuptools-scm/MANIFEST.in
similarity index 52%
rename from MANIFEST.in
rename to setuptools-scm/MANIFEST.in
index b793e6c0..5eb47594 100644
--- a/MANIFEST.in
+++ b/setuptools-scm/MANIFEST.in
@@ -4,24 +4,16 @@ exclude changelog.d/*
exclude .git_archival.txt
exclude .readthedocs.yaml
include *.py
-include testing/*.py
+include testing_scm/*.py
include tox.ini
include *.rst
include LICENSE
include *.toml
include mypy.ini
-include testing/Dockerfile.*
+include testing_scm/Dockerfile.*
include src/setuptools_scm/.git_archival.txt
include README.md
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
+recursive-include testing_scm *.bash
+prune .cursor
\ No newline at end of file
diff --git a/setuptools-scm/README.md b/setuptools-scm/README.md
new file mode 100644
index 00000000..f4ca4bf9
--- /dev/null
+++ b/setuptools-scm/README.md
@@ -0,0 +1,132 @@
+# setuptools-scm
+[](https://github.com/pypa/setuptools-scm/actions/workflows/python-tests.yml)
+[](https://setuptools-scm.readthedocs.io/en/latest/?badge=latest)
+[ ](https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme)
+
+## about
+
+[setuptools-scm] extracts Python package versions from `git` or `hg` metadata
+instead of declaring them as the version argument
+or in a Source Code Managed (SCM) managed file.
+
+Additionally [setuptools-scm] provides `setuptools` with a list of
+files that are managed by the SCM
+
+(i.e. it automatically adds all the SCM-managed files to the sdist).
+
+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 (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>=80", "setuptools-scm>=8"]
+build-backend = "setuptools.build_meta"
+```
+
+That will be sufficient to require [setuptools-scm] for projects
+that support [PEP 518] like [pip] and [build].
+
+[pip]: https://pypi.org/project/pip
+[build]: https://pypi.org/project/build
+[PEP 518]: https://peps.python.org/pep-0518/
+
+
+To enable version inference, you need to set the version
+dynamically in the `project` section of `pyproject.toml`:
+
+```toml title="pyproject.toml"
+[project]
+# version = "0.0.1" # Remove any existing version parameter.
+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"
+[tool.setuptools_scm]
+version_file = "pkg/_version.py"
+```
+
+Where `pkg` is the name of your package.
+
+If you need to confirm which version string is being generated or debug the configuration,
+you can install [setuptools-scm] directly in your working environment and run:
+
+```console
+$ python -m setuptools_scm
+# To explore other options, try:
+$ python -m setuptools_scm --help
+```
+
+For further configuration see the [documentation].
+
+[setuptools-scm]: https://github.com/pypa/setuptools-scm
+[documentation]: https://setuptools-scm.readthedocs.io/
+[git-archive-docs]: https://setuptools-scm.readthedocs.io/en/stable/usage/#builtin-mechanisms-for-obtaining-version-numbers
+
+
+## Interaction with Enterprise Distributions
+
+Some enterprise distributions like RHEL7
+ship rather old setuptools versions.
+
+In those cases its typically possible to build by using an sdist against `setuptools-scm<2.0`.
+As those old setuptools versions lack sensible types for versions,
+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
+
+
+Everyone interacting in the [setuptools-scm] project's codebases, issue
+trackers, chat rooms, and mailing lists is expected to follow the
+[PSF Code of Conduct].
+
+[PSF Code of Conduct]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
+
+
+## Security Contact
+
+To report a security vulnerability, please use the
+[Tidelift security contact](https://tidelift.com/security).
+Tidelift will coordinate the fix and disclosure.
diff --git a/setuptools-scm/_own_version_helper.py b/setuptools-scm/_own_version_helper.py
new file mode 100644
index 00000000..5274b10d
--- /dev/null
+++ b/setuptools-scm/_own_version_helper.py
@@ -0,0 +1,44 @@
+"""
+Version helper for setuptools-scm package.
+
+This module allows setuptools-scm to use VCS metadata for its own version.
+It works only if the backend-path of the build-system section from
+pyproject.toml is respected.
+
+Tag prefix configuration:
+- Currently: No prefix (for backward compatibility with existing tags)
+- Future: Will migrate to 'setuptools-scm-' prefix
+"""
+
+from __future__ import annotations
+
+import os
+
+from setuptools import build_meta as build_meta
+
+from setuptools_scm import get_version
+
+
+def scm_version() -> str:
+ # Use no-local-version if SETUPTOOLS_SCM_NO_LOCAL is set (for CI uploads)
+ local_scheme = (
+ "no-local-version"
+ if os.environ.get("SETUPTOOLS_SCM_NO_LOCAL")
+ else "node-and-date"
+ )
+
+ # Note: tag_regex is currently NOT set to allow backward compatibility
+ # with existing tags. To migrate to 'setuptools-scm-' prefix, uncomment:
+ # tag_regex=r"^setuptools-scm-(?P[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$",
+
+ # Use relative_to parent to find git root (one level up from setuptools-scm/)
+ import pathlib
+
+ return get_version(
+ root=pathlib.Path(__file__).parent.parent,
+ version_scheme="guess-next-dev",
+ local_scheme=local_scheme,
+ )
+
+
+version: str = scm_version()
diff --git a/setuptools-scm/changelog.d/.gitkeep b/setuptools-scm/changelog.d/.gitkeep
new file mode 100644
index 00000000..c199cfd6
--- /dev/null
+++ b/setuptools-scm/changelog.d/.gitkeep
@@ -0,0 +1,8 @@
+# Changelog fragments directory
+# Add your changelog fragments here following the naming convention:
+# {issue_number}.{type}.md
+#
+# Where type is one of: feature, bugfix, deprecation, removal, doc, misc
+#
+# Example: 123.feature.md
+
diff --git a/setuptools-scm/changelog.d/1231.bugfix.md b/setuptools-scm/changelog.d/1231.bugfix.md
new file mode 100644
index 00000000..027e5c46
--- /dev/null
+++ b/setuptools-scm/changelog.d/1231.bugfix.md
@@ -0,0 +1,2 @@
+Fix issue #1231: Don't warn about tool.setuptools.dynamic.version conflict when only using file finder without version inference.
+
diff --git a/setuptools-scm/changelog.d/README.md b/setuptools-scm/changelog.d/README.md
new file mode 100644
index 00000000..3ea52129
--- /dev/null
+++ b/setuptools-scm/changelog.d/README.md
@@ -0,0 +1,32 @@
+# Changelog Fragments
+
+This directory contains changelog fragments that will be assembled into the CHANGELOG.md file during release.
+
+## Fragment Types
+
+- **feature**: New features or enhancements
+- **bugfix**: Bug fixes
+- **deprecation**: Deprecation warnings
+- **removal**: Removed features (breaking changes)
+- **doc**: Documentation improvements
+- **misc**: Internal changes, refactoring, etc.
+
+## Naming Convention
+
+Fragments should be named: `{issue_number}.{type}.md`
+
+Examples:
+- `123.feature.md` - New feature related to issue #123
+- `456.bugfix.md` - Bug fix for issue #456
+- `789.doc.md` - Documentation update for issue #789
+
+## Content
+
+Each fragment should contain a brief description of the change:
+
+```markdown
+Add support for custom version schemes via plugin system
+```
+
+Do not include issue numbers in the content - they will be added automatically.
+
diff --git a/setuptools-scm/changelog.d/internal-refactor.misc.md b/setuptools-scm/changelog.d/internal-refactor.misc.md
new file mode 100644
index 00000000..82dd5976
--- /dev/null
+++ b/setuptools-scm/changelog.d/internal-refactor.misc.md
@@ -0,0 +1,2 @@
+Internal refactoring: modernized type annotations, improved CLI type safety, and enhanced release automation infrastructure.
+
diff --git a/setuptools-scm/changelog.d/py310.removal.md b/setuptools-scm/changelog.d/py310.removal.md
new file mode 100644
index 00000000..930517bd
--- /dev/null
+++ b/setuptools-scm/changelog.d/py310.removal.md
@@ -0,0 +1,2 @@
+Drop Python 3.8 and 3.9 support. Minimum Python version is now 3.10.
+
diff --git a/setuptools-scm/changelog.d/should-infer-function.misc.md b/setuptools-scm/changelog.d/should-infer-function.misc.md
new file mode 100644
index 00000000..c6c0b9cb
--- /dev/null
+++ b/setuptools-scm/changelog.d/should-infer-function.misc.md
@@ -0,0 +1,2 @@
+Refactored should_infer from method to standalone function for better code organization.
+
diff --git a/setuptools-scm/changelog.d/template.md b/setuptools-scm/changelog.d/template.md
new file mode 100644
index 00000000..41a46689
--- /dev/null
+++ b/setuptools-scm/changelog.d/template.md
@@ -0,0 +1,21 @@
+{% for section, _ in sections.items() %}
+{% set underline = underlines[0] %}{% if section %}{{section}}
+{{ underline * section|length }}{% set underline = underlines[1] %}
+
+{% endif %}
+{% if sections[section] %}
+{% for category, val in definitions.items() if category in sections[section]%}
+
+### {{ definitions[category]['name'] }}
+
+{% for text, values in sections[section][category].items() %}
+- {{ text }} ({{ values|join(', ') }})
+{% endfor %}
+
+{% endfor %}
+{% else %}
+No significant changes.
+
+{% endif %}
+{% endfor %}
+
diff --git a/setuptools-scm/changelog.d/test-mypy.misc.md b/setuptools-scm/changelog.d/test-mypy.misc.md
new file mode 100644
index 00000000..ed5eb9ed
--- /dev/null
+++ b/setuptools-scm/changelog.d/test-mypy.misc.md
@@ -0,0 +1,2 @@
+Updated mypy version template test to use uvx with mypy 1.11.2 for Python 3.8 compatibility checking.
+
diff --git a/setuptools-scm/changelog.d/test-parametrize.misc.md b/setuptools-scm/changelog.d/test-parametrize.misc.md
new file mode 100644
index 00000000..8bc5b570
--- /dev/null
+++ b/setuptools-scm/changelog.d/test-parametrize.misc.md
@@ -0,0 +1,2 @@
+Refactored TestBuildPackageWithExtra into parametrized function with custom INI-based decorator for cleaner test data specification.
+
diff --git a/setuptools-scm/changelog.d/vcs-versioning-dep.feature.md b/setuptools-scm/changelog.d/vcs-versioning-dep.feature.md
new file mode 100644
index 00000000..39c03e39
--- /dev/null
+++ b/setuptools-scm/changelog.d/vcs-versioning-dep.feature.md
@@ -0,0 +1,2 @@
+setuptools-scm now depends on vcs-versioning for core version inference logic. This enables other build backends to use the same version inference without setuptools dependency.
+
diff --git a/setuptools-scm/check_api.py b/setuptools-scm/check_api.py
new file mode 100755
index 00000000..84c041f0
--- /dev/null
+++ b/setuptools-scm/check_api.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+"""
+Local script to check API stability using griffe.
+
+Usage:
+ uv run check_api.py [--against TAG]
+
+This script runs griffe with the public-wildcard-imports extension enabled
+to properly detect re-exported symbols from vcs-versioning.
+"""
+
+from __future__ import annotations
+
+import subprocess
+import sys
+
+from pathlib import Path
+
+
+def main() -> int:
+ """Run griffe API check with proper configuration."""
+ # Parse arguments
+ against = "v9.2.1" # Default baseline
+ if len(sys.argv) > 1:
+ if sys.argv[1] == "--against" and len(sys.argv) > 2:
+ against = sys.argv[2]
+ else:
+ against = sys.argv[1]
+
+ # Ensure we're in the right directory
+ repo_root = Path(__file__).parent.parent
+
+ # Build griffe command
+ cmd = [
+ *("griffe", "check", "--verbose", "setuptools_scm"),
+ "-ssrc",
+ "-ssetuptools-scm/src",
+ "-svcs-versioning/src",
+ *("--extensions", "griffe_public_wildcard_imports"),
+ *("--against", against),
+ ]
+
+ result = subprocess.run(cmd, cwd=repo_root)
+
+ return result.returncode
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/hatch.toml b/setuptools-scm/hatch.toml
similarity index 100%
rename from hatch.toml
rename to setuptools-scm/hatch.toml
diff --git a/setuptools-scm/mypy.ini b/setuptools-scm/mypy.ini
new file mode 100644
index 00000000..f37b10e1
--- /dev/null
+++ b/setuptools-scm/mypy.ini
@@ -0,0 +1,6 @@
+[mypy]
+python_version = 3.10
+warn_return_any = True
+warn_unused_configs = True
+mypy_path = $MYPY_CONFIG_FILE_DIR/src:$MYPY_CONFIG_FILE_DIR/nextgen/vcs-versioning/src
+strict = true
diff --git a/setuptools-scm/pyproject.toml b/setuptools-scm/pyproject.toml
new file mode 100644
index 00000000..ce8f0b92
--- /dev/null
+++ b/setuptools-scm/pyproject.toml
@@ -0,0 +1,199 @@
+
+
+[build-system]
+build-backend = "_own_version_helper:build_meta"
+requires = [
+ "setuptools>=77.0.3",
+ "vcs-versioning>=0.1.1",
+ 'tomli<=2.0.2; python_version < "3.11"',
+ 'typing-extensions; python_version < "3.11"',
+]
+backend-path = [
+ ".",
+ "./src",
+ "../vcs-versioning/src"
+]
+[tools.uv.sources]
+vcs-versioning= "../vcs-versioning"
+[project]
+name = "setuptools-scm"
+description = "the blessed package to manage your versions by scm tags"
+readme = "README.md"
+license = "MIT"
+authors = [
+ {name="Ronny Pfannschmidt", email="opensource@ronnypfannschmidt.de"}
+]
+requires-python = ">=3.10"
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Topic :: Software Development :: Libraries",
+ "Topic :: Software Development :: Version Control",
+ "Topic :: System :: Software Distribution",
+ "Topic :: Utilities",
+]
+dynamic = [
+ "version",
+]
+dependencies = [
+ # Core VCS functionality - workspace dependency
+ "vcs-versioning",
+ "packaging>=20",
+ # https://github.com/pypa/setuptools-scm/issues/1112 - re-pin in a breaking release
+ "setuptools", # >= 61",
+ 'tomli>=1; python_version < "3.11"',
+ 'typing-extensions; python_version < "3.11"',
+]
+
+
+[project.optional-dependencies]
+rich = ["rich"]
+simple = []
+toml = []
+
+[dependency-groups]
+docs = [
+ "mkdocs",
+ "mkdocs-entangled-plugin",
+ "mkdocs-include-markdown-plugin",
+ "mkdocs-material",
+ "mkdocstrings[python]",
+ "pygments",
+ "griffe-public-wildcard-imports",
+]
+test = [
+ "pip",
+ "build",
+ "pytest",
+ "pytest-timeout", # Timeout protection for CI/CD
+ "pytest-xdist",
+ "rich",
+ "ruff",
+ "mypy; implementation_name != 'pypy'",
+ 'typing-extensions; python_version < "3.11"',
+ "griffe",
+ "griffe-public-wildcard-imports",
+ "flake8",
+]
+
+[project.urls]
+documentation = "https://setuptools-scm.readthedocs.io/"
+repository = "https://github.com/pypa/setuptools-scm/"
+
+[project.entry-points.console_scripts]
+setuptools-scm = "vcs_versioning._cli:main"
+
+[project.entry-points."distutils.setup_keywords"]
+use_scm_version = "setuptools_scm._integration.setuptools:version_keyword"
+
+[project.entry-points."pipx.run"]
+setuptools-scm = "vcs_versioning._cli:main"
+setuptools_scm = "vcs_versioning._cli:main"
+
+[project.entry-points."setuptools.file_finders"]
+setuptools_scm = "vcs_versioning._file_finders:find_files"
+
+[project.entry-points."setuptools.finalize_distribution_options"]
+setuptools_scm = "setuptools_scm._integration.setuptools:infer_version"
+
+[project.entry-points."setuptools_scm.files_command"]
+".git" = "vcs_versioning._file_finders._git:git_find_files"
+".hg" = "vcs_versioning._file_finders._hg:hg_find_files"
+
+[project.entry-points."setuptools_scm.files_command_fallback"]
+".git_archival.txt" = "vcs_versioning._file_finders._git:git_archive_find_files"
+".hg_archival.txt" = "vcs_versioning._file_finders._hg:hg_archive_find_files"
+
+# VCS-related entry points are now provided by vcs-versioning package
+# Only file-finder entry points remain in setuptools_scm
+
+[tool.setuptools.packages.find]
+where = ["src"]
+namespaces = false
+
+[tool.setuptools.dynamic]
+version = { attr = "_own_version_helper.version"}
+
+[tool.setuptools_scm]
+root = ".."
+version_scheme = "towncrier-fragments"
+tag_regex = "^setuptools-scm-(?Pv?\\d+(?:\\.\\d+){0,2}[^\\+]*)(?:\\+.*)?$"
+fallback_version = "9.2.2" # we trnasion to towncrier informed here
+scm.git.describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "setuptools-scm-*"]
+
+[tool.ruff]
+lint.extend-select = ["YTT", "B", "C4", "DTZ", "ISC", "LOG", "G", "PIE", "PYI", "PT", "FLY", "I", "C90", "PERF", "W", "PGH", "PLE", "UP", "FURB", "RUF"]
+lint.ignore = ["B028", "LOG015", "PERF203"]
+lint.preview = true
+
+[tool.ruff.lint.isort]
+force-single-line = true
+from-first = false
+lines-between-types = 1
+order-by-type = true
+
+[tool.pytest.ini_options]
+minversion = "8"
+testpaths = ["testing_scm"]
+addopts = ["-ra", "--strict-config", "--strict-markers", "-p", "vcs_versioning.test_api"]
+timeout = 300 # 5 minutes timeout per test for CI protection
+filterwarnings = [
+ "error",
+ "ignore:.*tool\\.setuptools_scm.*",
+ "ignore:.*git archive did not support describe output.*:UserWarning",
+]
+log_level = "debug"
+log_cli_level = "info"
+# disable unraisable until investigated
+markers = [
+ "issue(id): reference to github issue",
+ "skip_commit: allows to skip committing in the helpers",
+]
+
+[tool.uv]
+default-groups = ["test"]
+
+[tool.towncrier]
+directory = "changelog.d"
+filename = "CHANGELOG.md"
+start_string = "\n"
+template = "changelog.d/template.md"
+title_format = "## {version} ({project_date})"
+issue_format = "[#{issue}](https://github.com/pypa/setuptools-scm/issues/{issue})"
+underlines = ["", "", ""]
+
+[[tool.towncrier.type]]
+directory = "removal"
+name = "Removed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "deprecation"
+name = "Deprecated"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "feature"
+name = "Added"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "bugfix"
+name = "Fixed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "doc"
+name = "Documentation"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "misc"
+name = "Miscellaneous"
+showcontent = true
diff --git a/src/setuptools_scm/.git_archival.txt b/setuptools-scm/src/setuptools_scm/.git_archival.txt
similarity index 100%
rename from src/setuptools_scm/.git_archival.txt
rename to setuptools-scm/src/setuptools_scm/.git_archival.txt
diff --git a/setuptools-scm/src/setuptools_scm/__init__.py b/setuptools-scm/src/setuptools_scm/__init__.py
new file mode 100644
index 00000000..4be2f671
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/__init__.py
@@ -0,0 +1,30 @@
+"""
+:copyright: 2010-2023 by Ronny Pfannschmidt
+:license: MIT
+"""
+
+from __future__ import annotations
+
+from vcs_versioning import Configuration
+from vcs_versioning import NonNormalizedVersion
+from vcs_versioning import ScmVersion
+from vcs_versioning import Version
+from vcs_versioning._config import DEFAULT_LOCAL_SCHEME
+from vcs_versioning._config import DEFAULT_VERSION_SCHEME
+from vcs_versioning._dump_version import dump_version # soft deprecated
+from vcs_versioning._get_version_impl import _get_version
+from vcs_versioning._get_version_impl import get_version # soft deprecated
+
+# Public API
+__all__ = [
+ "DEFAULT_LOCAL_SCHEME",
+ "DEFAULT_VERSION_SCHEME",
+ "Configuration",
+ "NonNormalizedVersion",
+ "ScmVersion",
+ "Version",
+ "_get_version",
+ "dump_version",
+ # soft deprecated imports, left for backward compatibility
+ "get_version",
+]
diff --git a/setuptools-scm/src/setuptools_scm/__main__.py b/setuptools-scm/src/setuptools_scm/__main__.py
new file mode 100644
index 00000000..439fa674
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/__main__.py
@@ -0,0 +1,6 @@
+"""Backward compatibility shim for __main__.py"""
+
+from vcs_versioning._cli import main
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/src/setuptools_scm/_integration/__init__.py b/setuptools-scm/src/setuptools_scm/_integration/__init__.py
similarity index 100%
rename from src/setuptools_scm/_integration/__init__.py
rename to setuptools-scm/src/setuptools_scm/_integration/__init__.py
diff --git a/src/setuptools_scm/_integration/deprecation.py b/setuptools-scm/src/setuptools_scm/_integration/deprecation.py
similarity index 100%
rename from src/setuptools_scm/_integration/deprecation.py
rename to setuptools-scm/src/setuptools_scm/_integration/deprecation.py
diff --git a/setuptools-scm/src/setuptools_scm/_integration/pyproject_reading.py b/setuptools-scm/src/setuptools_scm/_integration/pyproject_reading.py
new file mode 100644
index 00000000..453d39e7
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/_integration/pyproject_reading.py
@@ -0,0 +1,147 @@
+from __future__ import annotations
+
+import logging
+
+from collections.abc import Sequence
+from pathlib import Path
+
+from vcs_versioning._pyproject_reading import DEFAULT_PYPROJECT_PATH
+from vcs_versioning._pyproject_reading import GivenPyProjectResult
+from vcs_versioning._pyproject_reading import PyProjectData
+from vcs_versioning._pyproject_reading import get_args_for_pyproject
+from vcs_versioning._pyproject_reading import read_pyproject as _vcs_read_pyproject
+from vcs_versioning._requirement_cls import Requirement
+from vcs_versioning._requirement_cls import extract_package_name
+from vcs_versioning._toml import TOML_RESULT
+
+log = logging.getLogger(__name__)
+
+_ROOT = "root"
+
+__all__ = [
+ "PyProjectData",
+ "get_args_for_pyproject",
+ "has_build_package_with_extra",
+ "read_pyproject",
+ "should_infer",
+]
+
+
+def should_infer(pyproject_data: PyProjectData) -> 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
+
+ Args:
+ pyproject_data: The PyProjectData instance to check
+
+ Returns:
+ True if version should be inferred, False otherwise
+ """
+ # Original behavior: explicit tool section
+ if pyproject_data.section_present:
+ return True
+
+ # New behavior: simple extra + dynamic version
+ if pyproject_data.project_present:
+ dynamic_fields = pyproject_data.project.get("dynamic", [])
+ if "version" in dynamic_fields:
+ if has_build_package_with_extra(
+ pyproject_data.build_requires, "setuptools-scm", "simple"
+ ):
+ 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
+ """
+ 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 _check_setuptools_dynamic_version_conflict(
+ path: Path, pyproject_data: PyProjectData
+) -> None:
+ """Warn if tool.setuptools.dynamic.version conflicts with setuptools-scm.
+
+ Only warns 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.
+ """
+ # Only warn if setuptools-scm is performing version inference
+ if not should_infer(pyproject_data):
+ return
+
+ # Check if tool.setuptools.dynamic.version exists
+ tool = pyproject_data.definition.get("tool", {})
+ if not isinstance(tool, dict):
+ return
+
+ setuptools_config = tool.get("setuptools", {})
+ if not isinstance(setuptools_config, dict):
+ return
+
+ dynamic_config = setuptools_config.get("dynamic", {})
+ if not isinstance(dynamic_config, dict):
+ return
+
+ if "version" in dynamic_config:
+ from .deprecation import warn_pyproject_setuptools_dynamic_version
+
+ warn_pyproject_setuptools_dynamic_version(path)
+
+
+def read_pyproject(
+ path: Path = DEFAULT_PYPROJECT_PATH,
+ tool_name: str = "setuptools_scm",
+ canonical_build_package_name: str = "setuptools-scm",
+ _given_result: GivenPyProjectResult = None,
+ _given_definition: TOML_RESULT | None = None,
+) -> PyProjectData:
+ """Read and parse pyproject configuration with setuptools-specific extensions.
+
+ This wraps vcs_versioning's read_pyproject and adds setuptools-specific behavior.
+ Uses internal multi-tool support to read both setuptools_scm and vcs-versioning sections.
+ """
+ # Use vcs_versioning's reader with multi-tool support (internal API)
+ # This allows setuptools_scm to transition to vcs-versioning section
+ pyproject_data = _vcs_read_pyproject(
+ path,
+ canonical_build_package_name=canonical_build_package_name,
+ _given_result=_given_result,
+ _given_definition=_given_definition,
+ tool_names=[
+ "setuptools_scm",
+ "vcs-versioning",
+ ], # Try both, setuptools_scm first
+ )
+
+ # Check for conflicting tool.setuptools.dynamic configuration
+ # Use the definition from pyproject_data (read by vcs_versioning)
+ _check_setuptools_dynamic_version_conflict(path, pyproject_data)
+
+ return pyproject_data
diff --git a/src/setuptools_scm/_integration/setup_cfg.py b/setuptools-scm/src/setuptools_scm/_integration/setup_cfg.py
similarity index 100%
rename from src/setuptools_scm/_integration/setup_cfg.py
rename to setuptools-scm/src/setuptools_scm/_integration/setup_cfg.py
diff --git a/src/setuptools_scm/_integration/setuptools.py b/setuptools-scm/src/setuptools_scm/_integration/setuptools.py
similarity index 68%
rename from src/setuptools_scm/_integration/setuptools.py
rename to setuptools-scm/src/setuptools_scm/_integration/setuptools.py
index aa1c645a..a84b77ce 100644
--- a/src/setuptools_scm/_integration/setuptools.py
+++ b/setuptools-scm/src/setuptools_scm/_integration/setuptools.py
@@ -3,20 +3,25 @@
import logging
import warnings
+from collections.abc import Callable
from typing import Any
-from typing import Callable
import setuptools
-from .. import _types as _t
+from vcs_versioning._pyproject_reading import GivenPyProjectResult
+from vcs_versioning._toml import InvalidTomlError
+from vcs_versioning.overrides import GlobalOverrides
+from vcs_versioning.overrides import ensure_context
+
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 GetVersionInferenceConfig
from .version_inference import get_version_inference_config
log = logging.getLogger(__name__)
+_setuptools_scm_logger = logging.getLogger("setuptools_scm")
def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None:
@@ -64,19 +69,19 @@ def get_keyword_overrides(
return value
+@ensure_context("SETUPTOOLS_SCM", additional_loggers=_setuptools_scm_logger)
def version_keyword(
dist: setuptools.Distribution,
keyword: str,
value: bool | dict[str, Any] | Callable[[], dict[str, Any]],
*,
- _given_pyproject_data: _t.GivenPyProjectResult = None,
+ _given_pyproject_data: GivenPyProjectResult = None,
_given_legacy_data: SetuptoolsBasicData | None = None,
- _get_version_inference_config: _t.GetVersionInferenceConfig = get_version_inference_config,
+ _get_version_inference_config: 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)
@@ -100,7 +105,7 @@ def version_keyword(
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()
+ pyproject_data = PyProjectData.empty(tool_name="setuptools_scm")
except InvalidTomlError as e:
log.debug("Configuration issue in pyproject.toml: %s", e)
return
@@ -112,22 +117,24 @@ def version_keyword(
else (legacy_data.version or pyproject_data.project_version)
)
- result = _get_version_inference_config(
- dist_name=dist_name,
- current_version=current_version,
- pyproject_data=pyproject_data,
- overrides=overrides,
- )
-
- result.apply(dist)
+ # Always use from_active to inherit current context settings
+ with GlobalOverrides.from_active(dist_name=dist_name):
+ result = _get_version_inference_config(
+ dist_name=dist_name,
+ current_version=current_version,
+ pyproject_data=pyproject_data,
+ overrides=overrides,
+ )
+ result.apply(dist)
+@ensure_context("SETUPTOOLS_SCM", additional_loggers=_setuptools_scm_logger)
def infer_version(
dist: setuptools.Distribution,
*,
- _given_pyproject_data: _t.GivenPyProjectResult = None,
+ _given_pyproject_data: GivenPyProjectResult = None,
_given_legacy_data: SetuptoolsBasicData | None = None,
- _get_version_inference_config: _t.GetVersionInferenceConfig = get_version_inference_config,
+ _get_version_inference_config: 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
@@ -135,12 +142,31 @@ def infer_version(
if the version keyword is used, it will override the version from this hook
as user might have passed custom code version schemes
"""
-
_log_hookstart("infer_version", dist)
legacy_data = extract_from_legacy(dist, _given_legacy_data=_given_legacy_data)
- dist_name = legacy_data.name
+ dist_name: str | None = legacy_data.name
+
+ # Always use from_active to inherit current context settings
+ with GlobalOverrides.from_active(dist_name=dist_name):
+ _infer_version_impl(
+ dist,
+ dist_name=dist_name,
+ legacy_data=legacy_data,
+ _given_pyproject_data=_given_pyproject_data,
+ _get_version_inference_config=_get_version_inference_config,
+ )
+
+def _infer_version_impl(
+ dist: setuptools.Distribution,
+ *,
+ dist_name: str | None,
+ legacy_data: SetuptoolsBasicData,
+ _given_pyproject_data: GivenPyProjectResult = None,
+ _get_version_inference_config: GetVersionInferenceConfig = get_version_inference_config,
+) -> None:
+ """Internal implementation of infer_version."""
try:
pyproject_data = read_pyproject(_given_result=_given_pyproject_data)
except FileNotFoundError:
diff --git a/src/setuptools_scm/_integration/version_inference.py b/setuptools-scm/src/setuptools_scm/_integration/version_inference.py
similarity index 62%
rename from src/setuptools_scm/_integration/version_inference.py
rename to setuptools-scm/src/setuptools_scm/_integration/version_inference.py
index 6258d90b..381fd37b 100644
--- a/src/setuptools_scm/_integration/version_inference.py
+++ b/setuptools-scm/src/setuptools_scm/_integration/version_inference.py
@@ -1,18 +1,38 @@
from __future__ import annotations
+import logging
+
from dataclasses import dataclass
-from typing import TYPE_CHECKING
from typing import Any
-from typing import Union
+from typing import Protocol
+from typing import TypeAlias
from setuptools import Distribution
+from vcs_versioning._pyproject_reading import PyProjectData
+
+from .pyproject_reading import should_infer
+
+log = logging.getLogger(__name__)
+
-from .. import _log
+class VersionInferenceApplicable(Protocol):
+ """A result object from version inference decision that can be applied to a dist."""
-if TYPE_CHECKING:
- from .pyproject_reading import PyProjectData
+ def apply(self, dist: Distribution) -> None: # pragma: no cover - structural type
+ ...
-log = _log.log.getChild("version_inference")
+
+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
+ ...
@dataclass
@@ -39,16 +59,16 @@ def apply(self, dist: Distribution) -> None:
@dataclass
-class VersionInferenceWarning:
- """Error message for user."""
+class VersionAlreadySetWarning:
+ """Warning that version was already set, inference would override it."""
- message: str
+ dist_name: str | None
def apply(self, dist: Distribution) -> None:
- """Apply error handling to the distribution."""
+ """Warn user that version is already set."""
import warnings
- warnings.warn(self.message)
+ warnings.warn(f"version of {self.dist_name} already set")
@dataclass(frozen=True)
@@ -59,11 +79,11 @@ 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)
-]
+VersionInferenceResult: TypeAlias = (
+ VersionInferenceConfig # Proceed with inference
+ | VersionAlreadySetWarning # Warn: version already set
+ | VersionInferenceNoOp # Don't infer (silent)
+)
def infer_version_string(
@@ -87,20 +107,17 @@ def infer_version_string(
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 {})
+ from vcs_versioning._version_inference import (
+ infer_version_string as _vcs_infer_version_string,
)
- maybe_version = _get_version(
- config, force_write_version_files=force_write_version_files
+ # Delegate to vcs_versioning implementation
+ return _vcs_infer_version_string(
+ dist_name,
+ pyproject_data,
+ overrides,
+ force_write_version_files=force_write_version_files,
)
- if maybe_version is None:
- _version_missing(config)
- return maybe_version
def get_version_inference_config(
@@ -128,14 +145,12 @@ def get_version_inference_config(
overrides=overrides,
)
- inference_implied = pyproject_data.should_infer() or overrides is not None
+ inference_implied = should_infer(pyproject_data) 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",
- )
+ return VersionAlreadySetWarning(dist_name)
else:
return VersionInferenceNoOp()
diff --git a/setuptools-scm/src/setuptools_scm/discover.py b/setuptools-scm/src/setuptools_scm/discover.py
new file mode 100644
index 00000000..d5a0d90c
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/discover.py
@@ -0,0 +1,18 @@
+"""Re-export discover from vcs_versioning for backward compatibility"""
+
+from __future__ import annotations
+
+from vcs_versioning._discover import (
+ iter_matching_entrypoints as iter_matching_entrypoints,
+)
+from vcs_versioning._discover import log as log
+from vcs_versioning._discover import match_entrypoint as match_entrypoint
+from vcs_versioning._discover import walk_potential_roots as walk_potential_roots
+
+__all__ = [
+ # Functions
+ "iter_matching_entrypoints",
+ "log",
+ "match_entrypoint",
+ "walk_potential_roots",
+]
diff --git a/setuptools-scm/src/setuptools_scm/fallbacks.py b/setuptools-scm/src/setuptools_scm/fallbacks.py
new file mode 100644
index 00000000..a0a846ed
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/fallbacks.py
@@ -0,0 +1,14 @@
+"""Re-export fallbacks from vcs_versioning for backward compatibility"""
+
+from __future__ import annotations
+
+from vcs_versioning._fallbacks import fallback_version as fallback_version
+from vcs_versioning._fallbacks import log as log
+from vcs_versioning._fallbacks import parse_pkginfo as parse_pkginfo
+
+__all__ = [
+ # Functions
+ "fallback_version",
+ "log",
+ "parse_pkginfo",
+]
diff --git a/setuptools-scm/src/setuptools_scm/git.py b/setuptools-scm/src/setuptools_scm/git.py
new file mode 100644
index 00000000..acfc1b56
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/git.py
@@ -0,0 +1,48 @@
+"""Re-export git backend from vcs_versioning for backward compatibility
+
+NOTE: The git backend is private in vcs_versioning and accessed via entry points.
+This module provides backward compatibility for code that imported from setuptools_scm.git
+"""
+
+from __future__ import annotations
+
+from vcs_versioning._backends._git import DEFAULT_DESCRIBE as DEFAULT_DESCRIBE
+from vcs_versioning._backends._git import DESCRIBE_UNSUPPORTED as DESCRIBE_UNSUPPORTED
+from vcs_versioning._backends._git import REF_TAG_RE as REF_TAG_RE
+from vcs_versioning._backends._git import GitPreParse as GitPreParse
+from vcs_versioning._backends._git import GitWorkdir as GitWorkdir
+from vcs_versioning._backends._git import archival_to_version as archival_to_version
+from vcs_versioning._backends._git import (
+ fail_on_missing_submodules as fail_on_missing_submodules,
+)
+from vcs_versioning._backends._git import fail_on_shallow as fail_on_shallow
+from vcs_versioning._backends._git import fetch_on_shallow as fetch_on_shallow
+from vcs_versioning._backends._git import get_working_directory as get_working_directory
+from vcs_versioning._backends._git import log as log
+from vcs_versioning._backends._git import parse as parse
+from vcs_versioning._backends._git import parse_archival as parse_archival
+from vcs_versioning._backends._git import run_git as run_git
+from vcs_versioning._backends._git import version_from_describe as version_from_describe
+from vcs_versioning._backends._git import warn_on_shallow as warn_on_shallow
+
+__all__ = [
+ # Constants
+ "DEFAULT_DESCRIBE",
+ "DESCRIBE_UNSUPPORTED",
+ "REF_TAG_RE",
+ # Classes
+ "GitPreParse",
+ "GitWorkdir",
+ # Functions
+ "archival_to_version",
+ "fail_on_missing_submodules",
+ "fail_on_shallow",
+ "fetch_on_shallow",
+ "get_working_directory",
+ "log",
+ "parse",
+ "parse_archival",
+ "run_git",
+ "version_from_describe",
+ "warn_on_shallow",
+]
diff --git a/setuptools-scm/src/setuptools_scm/hg.py b/setuptools-scm/src/setuptools_scm/hg.py
new file mode 100644
index 00000000..475382a9
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/hg.py
@@ -0,0 +1,25 @@
+"""Re-export hg backend from vcs_versioning for backward compatibility
+
+NOTE: The hg backend is private in vcs_versioning and accessed via entry points.
+This module provides backward compatibility for code that imported from setuptools_scm.hg
+"""
+
+from __future__ import annotations
+
+from vcs_versioning._backends._hg import HgWorkdir as HgWorkdir
+from vcs_versioning._backends._hg import archival_to_version as archival_to_version
+from vcs_versioning._backends._hg import log as log
+from vcs_versioning._backends._hg import parse as parse
+from vcs_versioning._backends._hg import parse_archival as parse_archival
+from vcs_versioning._backends._hg import run_hg as run_hg
+
+__all__ = [
+ # Classes
+ "HgWorkdir",
+ # Functions
+ "archival_to_version",
+ "log",
+ "parse",
+ "parse_archival",
+ "run_hg",
+]
diff --git a/setuptools-scm/src/setuptools_scm/hg_git.py b/setuptools-scm/src/setuptools_scm/hg_git.py
new file mode 100644
index 00000000..7c5409ec
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/hg_git.py
@@ -0,0 +1,16 @@
+"""Re-export hg_git from vcs_versioning for backward compatibility
+
+NOTE: The hg_git module is private in vcs_versioning.
+This module provides backward compatibility for code that imported from setuptools_scm.hg_git
+"""
+
+from __future__ import annotations
+
+from vcs_versioning._backends._hg_git import GitWorkdirHgClient as GitWorkdirHgClient
+from vcs_versioning._backends._hg_git import log as log
+
+__all__ = [
+ # Classes
+ "GitWorkdirHgClient",
+ "log",
+]
diff --git a/setuptools-scm/src/setuptools_scm/integration.py b/setuptools-scm/src/setuptools_scm/integration.py
new file mode 100644
index 00000000..93b8b46f
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/integration.py
@@ -0,0 +1,12 @@
+"""Re-export integration from vcs_versioning for backward compatibility"""
+
+from __future__ import annotations
+
+from vcs_versioning._integration import data_from_mime as data_from_mime
+from vcs_versioning._integration import log as log
+
+__all__ = [
+ # Functions
+ "data_from_mime",
+ "log",
+]
diff --git a/src/setuptools_scm/py.typed b/setuptools-scm/src/setuptools_scm/py.typed
similarity index 100%
rename from src/setuptools_scm/py.typed
rename to setuptools-scm/src/setuptools_scm/py.typed
diff --git a/setuptools-scm/src/setuptools_scm/scm_workdir.py b/setuptools-scm/src/setuptools_scm/scm_workdir.py
new file mode 100644
index 00000000..aa88b8c9
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/scm_workdir.py
@@ -0,0 +1,21 @@
+"""Re-export scm_workdir from vcs_versioning for backward compatibility
+
+NOTE: The scm_workdir module is private in vcs_versioning.
+This module provides backward compatibility for code that imported from setuptools_scm.scm_workdir
+"""
+
+from __future__ import annotations
+
+from vcs_versioning._backends._scm_workdir import Workdir as Workdir
+from vcs_versioning._backends._scm_workdir import (
+ get_latest_file_mtime as get_latest_file_mtime,
+)
+from vcs_versioning._backends._scm_workdir import log as log
+
+__all__ = [
+ # Classes
+ "Workdir",
+ # Functions
+ "get_latest_file_mtime",
+ "log",
+]
diff --git a/setuptools-scm/src/setuptools_scm/version.py b/setuptools-scm/src/setuptools_scm/version.py
new file mode 100644
index 00000000..64937f66
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/version.py
@@ -0,0 +1,76 @@
+"""Re-export version schemes from vcs_versioning for backward compatibility"""
+
+from __future__ import annotations
+
+from vcs_versioning._version_schemes import SEMVER_LEN as SEMVER_LEN
+from vcs_versioning._version_schemes import SEMVER_MINOR as SEMVER_MINOR
+from vcs_versioning._version_schemes import SEMVER_PATCH as SEMVER_PATCH
+from vcs_versioning._version_schemes import ScmVersion as ScmVersion
+from vcs_versioning._version_schemes import (
+ callable_or_entrypoint as callable_or_entrypoint,
+)
+from vcs_versioning._version_schemes import calver_by_date as calver_by_date
+from vcs_versioning._version_schemes import date_ver_match as date_ver_match
+from vcs_versioning._version_schemes import format_version as format_version
+from vcs_versioning._version_schemes import get_local_dirty_tag as get_local_dirty_tag
+from vcs_versioning._version_schemes import (
+ get_local_node_and_date as get_local_node_and_date,
+)
+from vcs_versioning._version_schemes import (
+ get_local_node_and_timestamp as get_local_node_and_timestamp,
+)
+from vcs_versioning._version_schemes import get_no_local_node as get_no_local_node
+from vcs_versioning._version_schemes import guess_next_date_ver as guess_next_date_ver
+from vcs_versioning._version_schemes import (
+ guess_next_dev_version as guess_next_dev_version,
+)
+from vcs_versioning._version_schemes import (
+ guess_next_simple_semver as guess_next_simple_semver,
+)
+from vcs_versioning._version_schemes import guess_next_version as guess_next_version
+from vcs_versioning._version_schemes import log as log
+from vcs_versioning._version_schemes import meta as meta
+from vcs_versioning._version_schemes import no_guess_dev_version as no_guess_dev_version
+from vcs_versioning._version_schemes import only_version as only_version
+from vcs_versioning._version_schemes import postrelease_version as postrelease_version
+from vcs_versioning._version_schemes import (
+ release_branch_semver as release_branch_semver,
+)
+from vcs_versioning._version_schemes import (
+ release_branch_semver_version as release_branch_semver_version,
+)
+from vcs_versioning._version_schemes import (
+ simplified_semver_version as simplified_semver_version,
+)
+from vcs_versioning._version_schemes import tag_to_version as tag_to_version
+
+__all__ = [
+ # Constants
+ "SEMVER_LEN",
+ "SEMVER_MINOR",
+ "SEMVER_PATCH",
+ # Classes
+ "ScmVersion",
+ # Functions
+ "callable_or_entrypoint",
+ "calver_by_date",
+ "date_ver_match",
+ "format_version",
+ "get_local_dirty_tag",
+ "get_local_node_and_date",
+ "get_local_node_and_timestamp",
+ "get_no_local_node",
+ "guess_next_date_ver",
+ "guess_next_dev_version",
+ "guess_next_simple_semver",
+ "guess_next_version",
+ "log",
+ "meta",
+ "no_guess_dev_version",
+ "only_version",
+ "postrelease_version",
+ "release_branch_semver",
+ "release_branch_semver_version",
+ "simplified_semver_version",
+ "tag_to_version",
+]
diff --git a/testing/Dockerfile.busted-buster b/setuptools-scm/testing_scm/Dockerfile.busted-buster
similarity index 100%
rename from testing/Dockerfile.busted-buster
rename to setuptools-scm/testing_scm/Dockerfile.busted-buster
diff --git a/testing/Dockerfile.rawhide-git b/setuptools-scm/testing_scm/Dockerfile.rawhide-git
similarity index 100%
rename from testing/Dockerfile.rawhide-git
rename to setuptools-scm/testing_scm/Dockerfile.rawhide-git
diff --git a/testing/INTEGRATION_MIGRATION_PLAN.md b/setuptools-scm/testing_scm/INTEGRATION_MIGRATION_PLAN.md
similarity index 100%
rename from testing/INTEGRATION_MIGRATION_PLAN.md
rename to setuptools-scm/testing_scm/INTEGRATION_MIGRATION_PLAN.md
diff --git a/testing/__init__.py b/setuptools-scm/testing_scm/__init__.py
similarity index 100%
rename from testing/__init__.py
rename to setuptools-scm/testing_scm/__init__.py
diff --git a/setuptools-scm/testing_scm/conftest.py b/setuptools-scm/testing_scm/conftest.py
new file mode 100644
index 00000000..f404e971
--- /dev/null
+++ b/setuptools-scm/testing_scm/conftest.py
@@ -0,0 +1,83 @@
+"""Pytest configuration for setuptools_scm tests.
+
+Uses vcs_versioning.test_api as a pytest plugin for common test infrastructure.
+"""
+
+from __future__ import annotations
+
+import os
+
+from pathlib import Path
+from typing import Any
+
+import pytest
+
+
+# Re-export for convenience
+from vcs_versioning.test_api import TEST_SOURCE_DATE
+from vcs_versioning.test_api import TEST_SOURCE_DATE_EPOCH
+from vcs_versioning.test_api import TEST_SOURCE_DATE_FORMATTED
+from vcs_versioning.test_api import TEST_SOURCE_DATE_TIMESTAMP
+from vcs_versioning.test_api import DebugMode
+from vcs_versioning.test_api import WorkDir
+
+# Use vcs_versioning test infrastructure as a pytest plugin
+# Moved to pyproject.toml addopts to avoid non-top-level conftest issues
+# pytest_plugins = ["vcs_versioning.test_api"]
+
+__all__ = [
+ "TEST_SOURCE_DATE",
+ "TEST_SOURCE_DATE_EPOCH",
+ "TEST_SOURCE_DATE_FORMATTED",
+ "TEST_SOURCE_DATE_TIMESTAMP",
+ "DebugMode",
+ "WorkDir",
+]
+
+
+def pytest_configure(config: pytest.Config) -> None:
+ """Additional configuration for setuptools_scm tests."""
+ # Set both debug env vars for backward compatibility
+ os.environ["SETUPTOOLS_SCM_DEBUG"] = "1"
+
+
+VERSION_PKGS = [
+ "setuptools",
+ "setuptools_scm",
+ "vcs-versioning",
+ "packaging",
+ "build",
+ "wheel",
+]
+
+
+def pytest_report_header() -> list[str]:
+ """Report package versions at test start."""
+ from importlib.metadata import version
+
+ res = []
+ for pkg in VERSION_PKGS:
+ try:
+ pkg_version = version(pkg)
+ module_name = pkg.replace("-", "_")
+ path = __import__(module_name).__file__
+ 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}")
+ except Exception:
+ pass
+ return res
+
+
+def pytest_addoption(parser: Any) -> None:
+ """Add setuptools_scm-specific test options."""
+ group = parser.getgroup("setuptools_scm")
+ group.addoption(
+ "--test-legacy", dest="scm_test_virtualenv", default=False, action="store_true"
+ )
diff --git a/testing/play_out_381.bash b/setuptools-scm/testing_scm/play_out_381.bash
similarity index 100%
rename from testing/play_out_381.bash
rename to setuptools-scm/testing_scm/play_out_381.bash
diff --git a/testing/test_basic_api.py b/setuptools-scm/testing_scm/test_basic_api.py
similarity index 96%
rename from testing/test_basic_api.py
rename to setuptools-scm/testing_scm/test_basic_api.py
index 7847b352..1d639c73 100644
--- a/testing/test_basic_api.py
+++ b/setuptools-scm/testing_scm/test_basic_api.py
@@ -8,15 +8,17 @@
import pytest
+from vcs_versioning._overrides import PRETEND_KEY
+from vcs_versioning._run_cmd import run
+from vcs_versioning.test_api import WorkDir
+
import setuptools_scm
from setuptools_scm import Configuration
from setuptools_scm import dump_version
-from setuptools_scm._run_cmd import run
from setuptools_scm.integration import data_from_mime
from setuptools_scm.version import ScmVersion
from setuptools_scm.version import meta
-from testing.wd_wrapper import WorkDir
c = Configuration()
@@ -59,7 +61,10 @@ def assertion(config: Configuration) -> ScmVersion:
return ScmVersion(Version("1.0"), config=config)
- monkeypatch.setattr(setuptools_scm._get_version_impl, "parse_version", assertion)
+ # Patch at vcs_versioning level since that's where the implementation lives
+ import vcs_versioning._get_version_impl
+
+ monkeypatch.setattr(vcs_versioning._get_version_impl, "parse_version", assertion)
def test_root_parameter_creation(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -150,7 +155,7 @@ def test_get_version_blank_tag_regex() -> None:
"version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625", "2345"]
)
def test_pretended(version: str, monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setenv(setuptools_scm._overrides.PRETEND_KEY, version)
+ monkeypatch.setenv(PRETEND_KEY, version)
assert setuptools_scm.get_version() == version
diff --git a/testing/test_cli.py b/setuptools-scm/testing_scm/test_cli.py
similarity index 66%
rename from testing/test_cli.py
rename to setuptools-scm/testing_scm/test_cli.py
index ffdcebd2..1392c4f8 100644
--- a/testing/test_cli.py
+++ b/setuptools-scm/testing_scm/test_cli.py
@@ -6,20 +6,56 @@
import pytest
-from setuptools_scm._cli import main
+from vcs_versioning._cli import main
+from vcs_versioning.test_api import WorkDir
+
+from setuptools_scm._integration.pyproject_reading import PyProjectData
from .conftest import DebugMode
-from .test_git import wd as wd_fixture # noqa: F401 (evil fixture reuse)
-from .wd_wrapper import WorkDir
+
+
+@pytest.fixture
+def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> WorkDir:
+ """Set up git for CLI tests."""
+ debug_mode.disable()
+ wd.setup_git(monkeypatch)
+ debug_mode.enable()
+ return wd
+
PYPROJECT_TOML = "pyproject.toml"
PYPROJECT_SIMPLE = "[tool.setuptools_scm]"
PYPROJECT_ROOT = '[tool.setuptools_scm]\nroot=".."'
+# PyProjectData constants for testing
+PYPROJECT_DATA_SIMPLE = PyProjectData.for_testing(
+ tool_name="setuptools_scm", section_present=True
+)
+PYPROJECT_DATA_WITH_PROJECT = PyProjectData.for_testing(
+ tool_name="setuptools_scm",
+ section_present=True,
+ project_present=True,
+ project_name="test",
+)
+
+
+def _create_version_file_pyproject_data() -> PyProjectData:
+ """Create PyProjectData with version_file configuration for testing."""
+ data = PyProjectData.for_testing(
+ tool_name="setuptools_scm",
+ section_present=True,
+ project_present=True,
+ project_name="test",
+ )
+ data.section["version_file"] = "ver.py"
+ return data
+
-def get_output(args: list[str]) -> str:
+def get_output(
+ args: list[str], *, _given_pyproject_data: PyProjectData | None = None
+) -> str:
with redirect_stdout(io.StringIO()) as out:
- main(args)
+ main(args, _given_pyproject_data=_given_pyproject_data)
return out.getvalue()
@@ -59,24 +95,20 @@ def test_cli_force_version_files(
) -> None:
debug_mode.disable()
wd.commit_testfile()
- wd.write(
- PYPROJECT_TOML,
- """
-[project]
-name = "test"
-[tool.setuptools_scm]
-version_file = "ver.py"
-""",
- )
monkeypatch.chdir(wd.cwd)
version_file = wd.cwd.joinpath("ver.py")
assert not version_file.exists()
- get_output([])
+ # Create pyproject data with version_file configuration
+ pyproject_data = _create_version_file_pyproject_data()
+
+ get_output([], _given_pyproject_data=pyproject_data)
assert not version_file.exists()
- output = get_output(["--force-write-version-files"])
+ output = get_output(
+ ["--force-write-version-files"], _given_pyproject_data=pyproject_data
+ )
assert version_file.exists()
assert output[:5] in version_file.read_text("utf-8")
@@ -87,13 +119,16 @@ def test_cli_create_archival_file_stable(
) -> 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"])
+ # Use injected pyproject data instead of creating a file
+ pyproject_data = PYPROJECT_DATA_SIMPLE
+ result = main(
+ ["create-archival-file", "--stable"], _given_pyproject_data=pyproject_data
+ )
assert result == 0
assert archival_file.exists()
@@ -115,13 +150,16 @@ def test_cli_create_archival_file_full(
) -> 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"])
+ # Use injected pyproject data instead of creating a file
+ pyproject_data = PYPROJECT_DATA_SIMPLE
+ result = main(
+ ["create-archival-file", "--full"], _given_pyproject_data=pyproject_data
+ )
assert result == 0
assert archival_file.exists()
@@ -144,15 +182,18 @@ 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.setup_git(monkeypatch)
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"])
+ pyproject_data = PYPROJECT_DATA_SIMPLE
+ result = main(
+ ["create-archival-file", "--stable"], _given_pyproject_data=pyproject_data
+ )
assert result == 1
# Content should be unchanged
@@ -163,15 +204,19 @@ 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.setup_git(monkeypatch)
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"])
+ pyproject_data = PYPROJECT_DATA_SIMPLE
+ result = main(
+ ["create-archival-file", "--stable", "--force"],
+ _given_pyproject_data=pyproject_data,
+ )
assert result == 0
# Content should be updated
@@ -184,26 +229,31 @@ 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.setup_git(monkeypatch)
wd.commit_testfile()
- wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE)
monkeypatch.chdir(wd.cwd)
# Should fail without --stable or --full
+ pyproject_data = PYPROJECT_DATA_SIMPLE
with pytest.raises(SystemExit):
- main(["create-archival-file"])
+ main(["create-archival-file"], _given_pyproject_data=pyproject_data)
def test_cli_create_archival_file_mutually_exclusive(
wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test that --stable and --full are mutually exclusive."""
+ wd.setup_git(monkeypatch)
wd.commit_testfile()
- wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE)
monkeypatch.chdir(wd.cwd)
# Should fail with both --stable and --full
+ pyproject_data = PYPROJECT_DATA_SIMPLE
with pytest.raises(SystemExit):
- main(["create-archival-file", "--stable", "--full"])
+ main(
+ ["create-archival-file", "--stable", "--full"],
+ _given_pyproject_data=pyproject_data,
+ )
def test_cli_create_archival_file_existing_gitattributes(
@@ -211,14 +261,16 @@ def test_cli_create_archival_file_existing_gitattributes(
) -> 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"])
+ pyproject_data = PYPROJECT_DATA_SIMPLE
+ result = main(
+ ["create-archival-file", "--stable"], _given_pyproject_data=pyproject_data
+ )
assert result == 0
archival_file = wd.cwd / ".git_archival.txt"
@@ -230,10 +282,12 @@ def test_cli_create_archival_file_no_gitattributes(
) -> 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"])
+ pyproject_data = PYPROJECT_DATA_SIMPLE
+ result = main(
+ ["create-archival-file", "--stable"], _given_pyproject_data=pyproject_data
+ )
assert result == 0
archival_file = wd.cwd / ".git_archival.txt"
diff --git a/testing/test_config.py b/setuptools-scm/testing_scm/test_config.py
similarity index 57%
rename from testing/test_config.py
rename to setuptools-scm/testing_scm/test_config.py
index d0f06bd6..2d47f18c 100644
--- a/testing/test_config.py
+++ b/setuptools-scm/testing_scm/test_config.py
@@ -1,6 +1,10 @@
+"""Tests for setuptools-scm specific Configuration functionality.
+
+Core Configuration tests have been moved to vcs-versioning/testing_vcs/test_config.py
+"""
+
from __future__ import annotations
-import re
import textwrap
from pathlib import Path
@@ -10,28 +14,6 @@
from setuptools_scm import Configuration
-@pytest.mark.parametrize(
- ("tag", "expected_version"),
- [
- ("apache-arrow-0.9.0", "0.9.0"),
- ("arrow-0.9.0", "0.9.0"),
- ("arrow-0.9.0-rc", "0.9.0-rc"),
- ("arrow-1", "1"),
- ("arrow-1+", "1"),
- ("arrow-1+foo", "1"),
- ("arrow-1.1+foo", "1.1"),
- ("v1.1", "v1.1"),
- ("V1.1", "V1.1"),
- ],
-)
-def test_tag_regex(tag: str, expected_version: str) -> None:
- config = Configuration()
- match = config.tag_regex.match(tag)
- assert match
- version = match.group("version")
- assert version == expected_version
-
-
def test_config_from_pyproject(tmp_path: Path) -> None:
fn = tmp_path / "pyproject.toml"
fn.write_text(
@@ -45,13 +27,7 @@ def test_config_from_pyproject(tmp_path: Path) -> None:
),
encoding="utf-8",
)
- assert Configuration.from_file(str(fn))
-
-
-def test_config_regex_init() -> None:
- tag_regex = re.compile(r"v(\d+)")
- conf = Configuration(tag_regex=tag_regex)
- assert conf.tag_regex is tag_regex
+ Configuration.from_file(str(fn))
def test_config_from_file_protects_relative_to(tmp_path: Path) -> None:
@@ -74,7 +50,7 @@ def test_config_from_file_protects_relative_to(tmp_path: Path) -> None:
"ignoring value relative_to='dont_use_me'"
" as its always relative to the config file",
):
- assert Configuration.from_file(str(fn))
+ Configuration.from_file(str(fn))
def test_config_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -98,23 +74,3 @@ 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/setuptools-scm/testing_scm/test_deprecation.py
similarity index 100%
rename from testing/test_deprecation.py
rename to setuptools-scm/testing_scm/test_deprecation.py
diff --git a/setuptools-scm/testing_scm/test_functions.py b/setuptools-scm/testing_scm/test_functions.py
new file mode 100644
index 00000000..9dd069d8
--- /dev/null
+++ b/setuptools-scm/testing_scm/test_functions.py
@@ -0,0 +1,178 @@
+"""Tests for setuptools-scm specific dump_version functionality.
+
+Core version scheme tests have been moved to vcs-versioning/testing_vcs/test_version_schemes.py
+"""
+
+from __future__ import annotations
+
+import shutil
+import subprocess
+
+from datetime import datetime
+from datetime import timezone
+from pathlib import Path
+
+import pytest
+
+from vcs_versioning._overrides import PRETEND_KEY
+
+from setuptools_scm import Configuration
+from setuptools_scm import dump_version
+from setuptools_scm import get_version
+from setuptools_scm.version import meta
+
+c = Configuration()
+
+# Use explicit time to avoid triggering auto-creation of GlobalOverrides at import time
+VERSIONS = {
+ "exact": meta(
+ "1.1",
+ distance=0,
+ dirty=False,
+ config=c,
+ time=datetime(2009, 2, 13, 23, 31, 30, tzinfo=timezone.utc),
+ ),
+}
+
+
+def test_dump_version_doesnt_bail_on_value_error(tmp_path: Path) -> None:
+ write_to = "VERSION"
+ version = str(VERSIONS["exact"].tag)
+ scm_version = meta(VERSIONS["exact"].tag, config=c)
+ with pytest.raises(ValueError, match=r"^bad file format:"):
+ dump_version(tmp_path, version, write_to, scm_version=scm_version)
+
+
+@pytest.mark.parametrize(
+ "version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625"]
+)
+def test_dump_version_works_with_pretend(
+ version: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ monkeypatch.setenv(PRETEND_KEY, version)
+ name = "VERSION.txt"
+ target = tmp_path.joinpath(name)
+ get_version(root=tmp_path, write_to=name)
+ assert target.read_text(encoding="utf-8") == version
+
+
+def test_dump_version_modern(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
+ version = "1.2.3"
+ monkeypatch.setenv(PRETEND_KEY, version)
+ name = "VERSION.txt"
+
+ project = tmp_path.joinpath("project")
+ target = project.joinpath(name)
+ project.mkdir()
+
+ get_version(root="..", relative_to=target, version_file=name)
+ assert target.read_text(encoding="utf-8") == version
+
+
+def dump_a_version(tmp_path: Path) -> None:
+ from vcs_versioning._dump_version import write_version_to_path
+
+ version = "1.2.3"
+ scm_version = meta(version, config=c)
+ write_version_to_path(
+ tmp_path / "VERSION.py", template=None, version=version, scm_version=scm_version
+ )
+
+
+def test_dump_version_on_old_python(tmp_path: Path) -> None:
+ python37 = shutil.which("python3.7")
+ if python37 is None:
+ pytest.skip("python3.7 not found")
+ dump_a_version(tmp_path)
+ subprocess.run(
+ [python37, "-c", "import VERSION;print(VERSION.version)"],
+ cwd=tmp_path,
+ check=True,
+ )
+
+
+def test_dump_version_mypy(tmp_path: Path) -> None:
+ uvx = shutil.which("uvx")
+ if uvx is None:
+ pytest.skip("uvx not found")
+ dump_a_version(tmp_path)
+ # Use mypy 1.11.2 - last version supporting Python 3.8
+ subprocess.run(
+ [uvx, "mypy==1.11.2", "--python-version=3.8", "--strict", "VERSION.py"],
+ cwd=tmp_path,
+ check=True,
+ )
+
+
+def test_dump_version_flake8(tmp_path: Path) -> None:
+ flake8 = shutil.which("flake8")
+ if flake8 is None:
+ pytest.skip("flake8 not found")
+ dump_a_version(tmp_path)
+ subprocess.run([flake8, "VERSION.py"], cwd=tmp_path, check=True)
+
+
+def test_dump_version_ruff(tmp_path: Path) -> None:
+ ruff = shutil.which("ruff")
+ if ruff is None:
+ pytest.skip("ruff not found")
+ dump_a_version(tmp_path)
+ subprocess.run([ruff, "check", "--no-fix", "VERSION.py"], cwd=tmp_path, check=True)
+
+
+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 vcs_versioning._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 vcs_versioning._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_integration.py b/setuptools-scm/testing_scm/test_integration.py
similarity index 95%
rename from testing/test_integration.py
rename to setuptools-scm/testing_scm/test_integration.py
index e85b5bba..6b0ceb83 100644
--- a/testing/test_integration.py
+++ b/setuptools-scm/testing_scm/test_integration.py
@@ -14,35 +14,32 @@
import pytest
from packaging.version import Version
+from vcs_versioning._requirement_cls import extract_package_name
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 vcs_versioning._overrides import PRETEND_KEY
+from vcs_versioning._overrides import PRETEND_KEY_NAMED
+from vcs_versioning._run_cmd import run
+from vcs_versioning.test_api import WorkDir
+
from setuptools_scm import Configuration
from setuptools_scm._integration.setuptools import _warn_on_old_setuptools
-from setuptools_scm._overrides import PRETEND_KEY
-from setuptools_scm._overrides import PRETEND_KEY_NAMED
-from setuptools_scm._run_cmd import run
-
-from .wd_wrapper import WorkDir
c = Configuration()
+# Module-level fixture for git SCM setup
@pytest.fixture
-def wd(wd: WorkDir) -> WorkDir:
- 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 wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> WorkDir:
+ """Set up git for integration tests."""
+ return wd.setup_git(monkeypatch)
def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -107,7 +104,7 @@ 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
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
monkeypatch.setenv(PRETEND_KEY, "1.2.3.dev4+g1337beef")
monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="g1337beef", distance=4}')
@@ -137,7 +134,7 @@ def test_pretend_metadata_with_version(
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
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY_NAMED
monkeypatch.setenv(
PRETEND_KEY_NAMED.format(name="test".upper()), "1.2.3.dev5+gabcdef12"
@@ -155,7 +152,7 @@ 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
+ from vcs_versioning._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
@@ -172,7 +169,7 @@ 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
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
# Set up a git repo with a tag so we have a base version
wd("git init")
@@ -211,7 +208,7 @@ 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
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
monkeypatch.setenv(PRETEND_KEY, "2.0.0")
monkeypatch.setenv(
@@ -228,7 +225,7 @@ 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
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
monkeypatch.setenv(PRETEND_KEY, "1.0.0")
monkeypatch.setenv(
@@ -240,7 +237,7 @@ def test_pretend_metadata_invalid_fields_filtered(
version = wd.get_version()
assert version == "1.0.0"
- assert "Invalid metadata fields in pretend metadata" in caplog.text
+ assert "Invalid fields in TOML data" in caplog.text
assert "invalid_field" in caplog.text
assert "another_bad_field" in caplog.text
@@ -249,7 +246,7 @@ 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
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
monkeypatch.setenv(PRETEND_KEY, "1.5.0")
monkeypatch.setenv(
@@ -264,7 +261,7 @@ 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
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
monkeypatch.setenv(PRETEND_KEY, "1.0.0")
monkeypatch.setenv(PRETEND_METADATA_KEY, "{invalid toml syntax here}")
@@ -496,6 +493,7 @@ def test_setup_cfg_version_prevents_inference_version_keyword(
# Construct PyProjectData directly without requiring build backend inference
pyproject_data = PyProjectData.for_testing(
+ tool_name="setuptools_scm",
is_required=False, # setuptools-scm not required
section_present=False, # no [tool.setuptools_scm] section
project_present=False, # no [project] section
@@ -658,6 +656,7 @@ def test_integration_function_call_order(
# Create PyProjectData with equivalent configuration - no file I/O!
project_name = "test-call-order"
pyproject_data = PyProjectData.for_testing(
+ tool_name="setuptools_scm",
project_name=project_name,
has_dynamic_version=True,
project_present=True,
diff --git a/testing/test_main.py b/setuptools-scm/testing_scm/test_main.py
similarity index 87%
rename from testing/test_main.py
rename to setuptools-scm/testing_scm/test_main.py
index 3c0ff3f8..c63b0731 100644
--- a/testing/test_main.py
+++ b/setuptools-scm/testing_scm/test_main.py
@@ -7,7 +7,7 @@
import pytest
-from .wd_wrapper import WorkDir
+from vcs_versioning.test_api import WorkDir
def test_main() -> None:
@@ -22,11 +22,7 @@ def test_main() -> None:
@pytest.fixture
def repo(wd: WorkDir) -> WorkDir:
- wd("git init")
- wd("git config user.email user@host")
- wd("git config user.name user")
- wd.add_command = "git add ."
- wd.commit_command = "git commit -m test-{reason}"
+ wd.setup_git()
wd.write("README.rst", "My example")
wd.add_and_commit()
diff --git a/testing/test_pyproject_reading.py b/setuptools-scm/testing_scm/test_pyproject_reading.py
similarity index 56%
rename from testing/test_pyproject_reading.py
rename to setuptools-scm/testing_scm/test_pyproject_reading.py
index 2a1fa89b..55e9938c 100644
--- a/testing/test_pyproject_reading.py
+++ b/setuptools-scm/testing_scm/test_pyproject_reading.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+import configparser
+
from pathlib import Path
from unittest.mock import Mock
@@ -7,6 +9,46 @@
from setuptools_scm._integration.pyproject_reading import has_build_package_with_extra
from setuptools_scm._integration.pyproject_reading import read_pyproject
+from setuptools_scm._integration.pyproject_reading import should_infer
+
+
+def parametrize_build_package_tests(ini_string: str) -> pytest.MarkDecorator:
+ """Parametrize has_build_package_with_extra tests from INI string.
+
+ Specific parser for testing build package requirements with extras.
+
+ Parameters:
+ - requires: multiline list of requirement strings
+ - package_name: string
+ - extra: string
+ - expected: boolean (using ConfigParser's getboolean)
+ """
+ parser = configparser.ConfigParser()
+ parser.read_string(ini_string)
+
+ test_cases = []
+ for section_name in parser.sections():
+ section = parser[section_name]
+
+ # Parse requires as list - split on newlines and strip
+ requires_str = section.get("requires", "")
+ requires = [line.strip() for line in requires_str.splitlines() if line.strip()]
+
+ # Parse strings directly
+ package_name = section.get("package_name")
+ extra = section.get("extra")
+
+ # Parse boolean using ConfigParser's native method
+ expected = section.getboolean("expected")
+
+ test_cases.append(
+ pytest.param(requires, package_name, extra, expected, id=section_name)
+ )
+
+ return pytest.mark.parametrize(
+ ("requires", "package_name", "extra", "expected"),
+ test_cases,
+ )
class TestPyProjectReading:
@@ -44,77 +86,82 @@ def test_read_pyproject_existing_file(self, tmp_path: Path) -> None:
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
- )
+@parametrize_build_package_tests(
+ """
+ [has_simple_extra]
+ requires =
+ setuptools-scm[simple]
+ package_name = setuptools-scm
+ extra = simple
+ expected = true
+
+ [has_no_simple_extra]
+ requires =
+ setuptools-scm
+ package_name = setuptools-scm
+ extra = simple
+ expected = false
+
+ [has_different_extra]
+ requires =
+ setuptools-scm[toml]
+ package_name = setuptools-scm
+ extra = simple
+ expected = false
+
+ [has_multiple_extras_including_simple]
+ requires =
+ setuptools-scm[simple,toml]
+ package_name = setuptools-scm
+ extra = simple
+ expected = true
+
+ [different_package_with_simple_extra]
+ requires =
+ other-package[simple]
+ package_name = setuptools-scm
+ extra = simple
+ expected = false
+
+ [version_specifier_with_extra]
+ requires =
+ setuptools-scm[simple]>=8.0
+ package_name = setuptools-scm
+ extra = simple
+ expected = true
+
+ [complex_requirement_with_extra]
+ requires =
+ setuptools-scm[simple]>=8.0,<9.0
+ package_name = setuptools-scm
+ extra = simple
+ expected = true
+
+ [empty_requires_list]
+ requires =
+ package_name = setuptools-scm
+ extra = simple
+ expected = false
+
+ [invalid_requirement_string]
+ requires =
+ invalid requirement string
+ package_name = setuptools-scm
+ extra = simple
+ expected = false
+ """
+)
+def test_has_build_package_with_extra(
+ requires: list[str], package_name: str, extra: str, expected: bool
+) -> None:
+ """Test the has_build_package_with_extra function with various inputs."""
+ assert has_build_package_with_extra(requires, package_name, extra) is expected
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",
+ "vcs_versioning._toml.read_toml_content",
Mock(side_effect=FileNotFoundError("this test should not read")),
)
@@ -125,7 +172,7 @@ def test_read_pyproject_with_given_definition(monkeypatch: pytest.MonkeyPatch) -
}
)
- assert res.should_infer()
+ assert should_infer(res)
def test_read_pyproject_with_setuptools_dynamic_version_warns() -> None:
@@ -178,4 +225,4 @@ def test_read_pyproject_with_setuptools_dynamic_version_no_warn_when_file_finder
"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()
+ assert not should_infer(pyproject_data)
diff --git a/testing/test_regressions.py b/setuptools-scm/testing_scm/test_regressions.py
similarity index 50%
rename from testing/test_regressions.py
rename to setuptools-scm/testing_scm/test_regressions.py
index 326d62b8..ae6aa8eb 100644
--- a/testing/test_regressions.py
+++ b/setuptools-scm/testing_scm/test_regressions.py
@@ -1,22 +1,24 @@
+"""Setuptools-scm specific regression tests.
+
+Core VCS regression tests have been moved to vcs-versioning/testing_vcs/test_regressions.py
+"""
+
from __future__ import annotations
import pprint
import subprocess
import sys
-from dataclasses import replace
from importlib.metadata import EntryPoint
from importlib.metadata import distribution
from pathlib import Path
-from typing import Sequence
import pytest
-from setuptools_scm import Configuration
-from setuptools_scm._run_cmd import run
-from setuptools_scm.git import parse
+from vcs_versioning._run_cmd import run
+from vcs_versioning.test_api import WorkDir
+
from setuptools_scm.integration import data_from_mime
-from setuptools_scm.version import meta
def test_data_from_mime_ignores_body() -> None:
@@ -91,80 +93,23 @@ def vs(v):
assert res.stdout == "1.0"
-@pytest.mark.skipif(sys.platform != "win32", reason="this bug is only valid on windows")
-def test_case_mismatch_on_windows_git(tmp_path: Path) -> None:
- """Case insensitive path checks on Windows"""
- camel_case_path = tmp_path / "CapitalizedDir"
- camel_case_path.mkdir()
- run("git init", camel_case_path)
- res = parse(str(camel_case_path).lower(), Configuration())
- 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
+
+ from vcs_versioning._file_finders._git import _git_toplevel
# Create git repo structure
repo_path = tmp_path / "my_repo"
repo_path.mkdir()
- run("git init", repo_path)
+ wd = WorkDir(repo_path).setup_git()
# 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)
+ wd.write("CasedDir/test.txt", "test")
+ wd.add_and_commit("Initial commit")
# Now call _git_toplevel with a path that has different casing
# This should cause the assertion to fail
@@ -191,36 +136,3 @@ def test_entrypoints_load() -> None:
failed.append((ep, e))
if failed:
pytest.fail(pprint.pformat(failed))
-
-
-def test_write_to_absolute_path_passes_when_subdir_of_root(tmp_path: Path) -> None:
- c = Configuration(root=tmp_path, write_to=tmp_path / "VERSION.py")
- v = meta("1.0", config=c)
- from setuptools_scm._get_version_impl import write_version_files
-
- with pytest.warns(DeprecationWarning, match=".*write_to=.* is a absolute.*"):
- write_version_files(c, "1.0", v)
- write_version_files(replace(c, write_to="VERSION.py"), "1.0", v)
- subdir = tmp_path / "subdir"
- subdir.mkdir()
- with pytest.raises(
- # todo: python version specific error list
- ValueError,
- match=r".*VERSION.py' .* .*subdir.*",
- ):
- write_version_files(replace(c, root=subdir), "1.0", v)
-
-
-@pytest.mark.parametrize(
- ("input", "expected"),
- [
- ("1.0", (1, 0)),
- ("1.0a2", (1, 0, "a2")),
- ("1.0.b2dev1", (1, 0, "b2", "dev1")),
- ("1.0.dev1", (1, 0, "dev1")),
- ],
-)
-def test_version_as_tuple(input: str, expected: Sequence[int | str]) -> None:
- from setuptools_scm._version_cls import _version_as_tuple
-
- assert _version_as_tuple(input) == expected
diff --git a/testing/test_version_inference.py b/setuptools-scm/testing_scm/test_version_inference.py
similarity index 89%
rename from testing/test_version_inference.py
rename to setuptools-scm/testing_scm/test_version_inference.py
index 967ab768..f216f214 100644
--- a/testing/test_version_inference.py
+++ b/setuptools-scm/testing_scm/test_version_inference.py
@@ -6,25 +6,37 @@
import pytest
from setuptools_scm._integration.pyproject_reading import PyProjectData
+from setuptools_scm._integration.version_inference import VersionAlreadySetWarning
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
+ tool_name="setuptools_scm",
+ is_required=True,
+ section_present=True,
+ project_present=True,
),
WITHOUT_TOOL_SECTION=PyProjectData.for_testing(
- is_required=True, section_present=False, project_present=True
+ tool_name="setuptools_scm",
+ is_required=True,
+ section_present=False,
+ project_present=True,
),
ONLY_REQUIRED=PyProjectData.for_testing(
- is_required=True, section_present=False, project_present=False
+ tool_name="setuptools_scm",
+ is_required=True,
+ section_present=False,
+ project_present=False,
),
WITHOUT_PROJECT=PyProjectData.for_testing(
- is_required=True, section_present=True, project_present=False
+ tool_name="setuptools_scm",
+ is_required=True,
+ section_present=True,
+ project_present=False,
),
)
@@ -36,12 +48,8 @@
)
-WARNING_PACKAGE = VersionInferenceWarning(
- message="version of test_package already set",
-)
-WARNING_NO_PACKAGE = VersionInferenceWarning(
- message="version of None already set",
-)
+WARNING_PACKAGE = VersionAlreadySetWarning(dist_name="test_package")
+WARNING_NO_PACKAGE = VersionAlreadySetWarning(dist_name=None)
NOOP = VersionInferenceNoOp()
@@ -53,7 +61,7 @@ def expect_config(
pyproject_data: PyProjectData = PYPROJECT.DEFAULT,
overrides: dict[str, Any] | None = None,
expected: type[VersionInferenceConfig]
- | VersionInferenceWarning
+ | VersionAlreadySetWarning
| VersionInferenceNoOp,
) -> None:
"""Helper to test get_version_inference_config and assert expected result type."""
@@ -73,7 +81,7 @@ def expect_config(
overrides=overrides,
)
else:
- assert isinstance(expected, (VersionInferenceNoOp, VersionInferenceWarning))
+ assert isinstance(expected, (VersionInferenceNoOp, VersionAlreadySetWarning))
expectation = expected
assert result == expectation
@@ -183,6 +191,7 @@ def test_tool_section_present(self) -> None:
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(
+ tool_name="setuptools_scm",
is_required=True,
section_present=False,
project_present=True,
@@ -198,6 +207,7 @@ def test_simple_extra_with_dynamic_version_infers(self) -> None:
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(
+ tool_name="setuptools_scm",
is_required=True,
section_present=False,
project_present=True,
@@ -213,6 +223,7 @@ def test_simple_extra_without_dynamic_version_no_infer(self) -> None:
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(
+ tool_name="setuptools_scm",
is_required=True,
section_present=False,
project_present=True,
@@ -228,6 +239,7 @@ def test_no_simple_extra_with_dynamic_version_no_infer(self) -> None:
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(
+ tool_name="setuptools_scm",
is_required=True,
section_present=False,
project_present=False,
@@ -242,6 +254,7 @@ def test_simple_extra_no_project_section_no_infer(self) -> None:
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(
+ tool_name="setuptools_scm",
is_required=True,
section_present=False,
project_present=True,
diff --git a/tox.ini b/setuptools-scm/tox.ini
similarity index 100%
rename from tox.ini
rename to setuptools-scm/tox.ini
diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py
deleted file mode 100644
index e265e859..00000000
--- a/src/setuptools_scm/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
-:copyright: 2010-2023 by Ronny Pfannschmidt
-:license: MIT
-"""
-
-from __future__ import annotations
-
-from ._config import DEFAULT_LOCAL_SCHEME
-from ._config import DEFAULT_VERSION_SCHEME
-from ._config import Configuration
-from ._get_version_impl import _get_version
-from ._get_version_impl import get_version
-from ._integration.dump_version import dump_version # soft deprecated
-from ._version_cls import NonNormalizedVersion
-from ._version_cls import Version
-from .version import ScmVersion
-
-# Public API
-__all__ = [
- "DEFAULT_LOCAL_SCHEME",
- "DEFAULT_VERSION_SCHEME",
- "Configuration",
- "NonNormalizedVersion",
- "ScmVersion",
- "Version",
- "_get_version",
- "dump_version",
- # soft deprecated imports, left for backward compatibility
- "get_version",
-]
diff --git a/src/setuptools_scm/_cli.py b/src/setuptools_scm/_cli.py
deleted file mode 100644
index 1f104f46..00000000
--- a/src/setuptools_scm/_cli.py
+++ /dev/null
@@ -1,291 +0,0 @@
-from __future__ import annotations
-
-import argparse
-import json
-import os
-import sys
-
-from pathlib import Path
-from typing import Any
-
-from setuptools_scm import Configuration
-from setuptools_scm._file_finders import find_files
-from setuptools_scm._get_version_impl import _get_version
-from setuptools_scm.discover import walk_potential_roots
-
-
-def main(args: list[str] | None = None) -> int:
- opts = _get_cli_opts(args)
- inferred_root: str = opts.root or "."
-
- pyproject = opts.config or _find_pyproject(inferred_root)
-
- try:
- config = Configuration.from_file(
- pyproject,
- root=(os.path.abspath(opts.root) if opts.root is not None else None),
- )
- except (LookupError, FileNotFoundError) as ex:
- # no pyproject.toml OR no [tool.setuptools_scm]
- print(
- f"Warning: could not use {os.path.relpath(pyproject)},"
- " using default configuration.\n"
- f" Reason: {ex}.",
- file=sys.stderr,
- )
- config = Configuration(root=inferred_root)
- 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:
- version = version.partition(".dev")[0]
-
- return command(opts, version, config)
-
-
-def _get_cli_opts(args: list[str] | None) -> argparse.Namespace:
- prog = "python -m setuptools_scm"
- desc = "Print project version according to SCM metadata"
- parser = argparse.ArgumentParser(prog, description=desc)
- # By default, help for `--help` starts with lower case, so we keep the pattern:
- parser.add_argument(
- "-r",
- "--root",
- default=None,
- help='directory managed by the SCM, default: inferred from config file, or "."',
- )
- parser.add_argument(
- "-c",
- "--config",
- default=None,
- metavar="PATH",
- help="path to 'pyproject.toml' with setuptools-scm config, "
- "default: looked up in the current or parent directories",
- )
- parser.add_argument(
- "--strip-dev",
- action="store_true",
- help="remove the dev/local parts of the version before printing the version",
- )
- parser.add_argument(
- "-N",
- "--no-version",
- action="store_true",
- help="do not include package version in the output",
- )
- output_formats = ["json", "plain", "key-value"]
- parser.add_argument(
- "-f",
- "--format",
- type=str.casefold,
- default="plain",
- help="specify output format",
- choices=output_formats,
- )
- parser.add_argument(
- "-q",
- "--query",
- type=str.casefold,
- nargs="*",
- help="display setuptools-scm settings according to query, "
- "e.g. dist_name, do not supply an argument in order to "
- "print a list of valid queries.",
- )
- parser.add_argument(
- "--force-write-version-files",
- action="store_true",
- help="trigger to write the content of the version files\n"
- "its recommended to use normal/editable installation instead)",
- )
- sub = parser.add_subparsers(title="extra commands", dest="command", metavar="")
- # 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)
-
-
-# flake8: noqa: C901
-def command(opts: argparse.Namespace, version: str, config: Configuration) -> int:
- data: dict[str, Any] = {}
-
- 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")
- opts.query = ["queries"]
- data["queries"] = ["files", *config.__dataclass_fields__]
-
- if opts.query is None:
- opts.query = []
-
- if not opts.no_version:
- data["version"] = version
-
- if "files" in opts.query:
- data["files"] = find_files(config.root)
-
- for q in opts.query:
- if q in ["files", "queries", "version"]:
- continue
-
- try:
- if q.startswith("_"):
- raise AttributeError()
- data[q] = getattr(config, q)
- except AttributeError:
- sys.stderr.write(f"Error: unknown query: '{q}'\n")
- return 1
-
- if opts.format == "json":
- print(json.dumps(data, indent=2))
-
- if opts.format == "plain":
- _print_plain(data)
-
- if opts.format == "key-value":
- _print_key_value(data)
-
- return 0
-
-
-def _print_plain(data: dict[str, Any]) -> None:
- version = data.pop("version", None)
- if version:
- print(version)
- files = data.pop("files", [])
- for file_ in files:
- print(file_)
- queries = data.pop("queries", [])
- for query in queries:
- print(query)
- if data:
- print("\n".join(data.values()))
-
-
-def _print_key_value(data: dict[str, Any]) -> None:
- for key, value in data.items():
- if isinstance(value, str):
- print(f"{key} = {value}")
- else:
- str_value = "\n ".join(value)
- print(f"{key} = {str_value}")
-
-
-def _find_pyproject(parent: str) -> str:
- for directory in walk_potential_roots(os.path.abspath(parent)):
- pyproject = os.path.join(directory, "pyproject.toml")
- if os.path.isfile(pyproject):
- return pyproject
-
- 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/_file_finders/pathtools.py b/src/setuptools_scm/_file_finders/pathtools.py
deleted file mode 100644
index 6de85089..00000000
--- a/src/setuptools_scm/_file_finders/pathtools.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from __future__ import annotations
-
-import os
-
-from setuptools_scm import _types as _t
-
-
-def norm_real(path: _t.PathT) -> str:
- return os.path.normcase(os.path.realpath(path))
diff --git a/src/setuptools_scm/_integration/toml.py b/src/setuptools_scm/_integration/toml.py
deleted file mode 100644
index 2253287c..00000000
--- a/src/setuptools_scm/_integration/toml.py
+++ /dev/null
@@ -1,69 +0,0 @@
-from __future__ import annotations
-
-import sys
-
-from pathlib import Path
-from typing import TYPE_CHECKING
-from typing import Any
-from typing import Callable
-from typing import Dict
-from typing import TypedDict
-from typing import cast
-
-if sys.version_info >= (3, 11):
- from tomllib import loads as load_toml
-else:
- from tomli import loads as load_toml
-
-if TYPE_CHECKING:
- if sys.version_info >= (3, 10):
- from typing import TypeAlias
- else:
- from typing_extensions import TypeAlias
-
-from .. import _log
-
-log = _log.log.getChild("toml")
-
-TOML_RESULT: TypeAlias = Dict[str, Any]
-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")
- except FileNotFoundError:
- if default is None:
- raise
- else:
- log.debug("%s missing, presuming default %r", path, default)
- return default
- else:
- 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):
- cheat: dict[str, Any]
-
-
-def load_toml_or_inline_map(data: str | None) -> dict[str, Any]:
- """
- load toml data - with a special hack if only a inline map is given
- """
- if not data:
- return {}
- 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/_log.py b/src/setuptools_scm/_log.py
deleted file mode 100644
index ea17f375..00000000
--- a/src/setuptools_scm/_log.py
+++ /dev/null
@@ -1,87 +0,0 @@
-"""
-logging helpers, supports vendoring
-"""
-
-from __future__ import annotations
-
-import contextlib
-import logging
-import os
-import sys
-
-from typing import IO
-from typing import Iterator
-from typing import Mapping
-
-log = logging.getLogger(__name__.rsplit(".", 1)[0])
-log.propagate = False
-
-
-class AlwaysStdErrHandler(logging.StreamHandler): # type: ignore[type-arg]
- def __init__(self) -> None:
- super().__init__(sys.stderr)
-
- @property
- def stream(self) -> IO[str]:
- return sys.stderr
-
- @stream.setter
- def stream(self, value: IO[str]) -> None:
- assert value is sys.stderr
-
-
-def make_default_handler() -> logging.Handler:
- try:
- from rich.console import Console
-
- console = Console(stderr=True)
- from rich.logging import RichHandler
-
- return RichHandler(console=console)
- except ImportError:
- last_resort = logging.lastResort
- assert last_resort is not None
- return last_resort
-
-
-_default_handler = make_default_handler()
-
-log.addHandler(_default_handler)
-
-
-def _default_log_level(_env: Mapping[str, str] = os.environ) -> int:
- val: str | None = _env.get("SETUPTOOLS_SCM_DEBUG")
- return logging.WARNING if val is None else logging.DEBUG
-
-
-log.setLevel(_default_log_level())
-
-
-@contextlib.contextmanager
-def defer_to_pytest() -> Iterator[None]:
- log.propagate = True
- old_level = log.level
- log.setLevel(logging.NOTSET)
- log.removeHandler(_default_handler)
- try:
- yield
- finally:
- log.addHandler(_default_handler)
- log.propagate = False
- log.setLevel(old_level)
-
-
-@contextlib.contextmanager
-def enable_debug(handler: logging.Handler = _default_handler) -> Iterator[None]:
- log.addHandler(handler)
- old_level = log.level
- log.setLevel(logging.DEBUG)
- old_handler_level = handler.level
- handler.setLevel(logging.DEBUG)
- try:
- yield
- finally:
- log.setLevel(old_level)
- handler.setLevel(old_handler_level)
- if handler is not _default_handler:
- log.removeHandler(handler)
diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py
deleted file mode 100644
index 4e06b7a7..00000000
--- a/src/setuptools_scm/_overrides.py
+++ /dev/null
@@ -1,298 +0,0 @@
-from __future__ import annotations
-
-import dataclasses
-import os
-
-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
-from . import version
-from ._integration.toml import load_toml_or_inline_map
-
-log = _log.log.getChild("overrides")
-
-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,
- 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 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
-
- # 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(
- config: _config.Configuration,
-) -> version.ScmVersion | None:
- """read a a overridden version from the environment
-
- tries ``SETUPTOOLS_SCM_PRETEND_VERSION``
- and ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_$UPPERCASE_DIST_NAME``
- """
- log.debug("dist name: %s", config.dist_name)
-
- pretended = read_named_env(name="PRETEND_VERSION", dist_name=config.dist_name)
-
- if pretended:
- return version.meta(tag=pretended, preformatted=True, config=config)
- else:
- return None
-
-
-def read_toml_overrides(dist_name: str | None) -> dict[str, Any]:
- data = read_named_env(name="OVERRIDES", dist_name=dist_name)
- return load_toml_or_inline_map(data)
diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py
deleted file mode 100644
index 4f8874fb..00000000
--- a/src/setuptools_scm/_types.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from __future__ import annotations
-
-import os
-
-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
-
- if sys.version_info >= (3, 10):
- from typing import TypeAlias
- else:
- 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]
-
-CMD_TYPE: TypeAlias = Union[Sequence[PathT], str]
-
-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/version.py b/src/setuptools_scm/version.py
deleted file mode 100644
index 77c26dc9..00000000
--- a/src/setuptools_scm/version.py
+++ /dev/null
@@ -1,583 +0,0 @@
-from __future__ import annotations
-
-import dataclasses
-import logging
-import os
-import re
-import warnings
-
-from datetime import date
-from datetime import datetime
-from datetime import timezone
-from typing import TYPE_CHECKING
-from typing import Any
-from typing import Callable
-from typing import Match
-
-from . import _entrypoints
-from . import _modify_version
-from ._node_utils import _format_node_for_output
-
-if TYPE_CHECKING:
- import sys
-
- if sys.version_info >= (3, 10):
- from typing import Concatenate
- from typing import ParamSpec
- else:
- from typing_extensions import Concatenate
- from typing_extensions import ParamSpec
-
- _P = ParamSpec("_P")
-
-from typing import TypedDict
-
-from . import _config
-from . import _version_cls as _v
-from ._version_cls import Version as PkgVersion
-from ._version_cls import _VersionT
-
-log = logging.getLogger(__name__)
-
-
-SEMVER_MINOR = 2
-SEMVER_PATCH = 3
-SEMVER_LEN = 3
-
-
-class _TagDict(TypedDict):
- version: str
- prefix: str
- suffix: str
-
-
-def _parse_version_tag(
- tag: str | object, config: _config.Configuration
-) -> _TagDict | None:
- match = config.tag_regex.match(str(tag))
-
- if match:
- key: str | int = 1 if len(match.groups()) == 1 else "version"
- full = match.group(0)
- log.debug("%r %r %s", tag, config.tag_regex, match)
- log.debug(
- "key %s data %s, %s, %r", key, match.groupdict(), match.groups(), full
- )
-
- 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)
-
- return None
-
-
-def callable_or_entrypoint(group: str, callable_or_name: str | Any) -> Any:
- log.debug("ep %r %r", group, callable_or_name)
-
- if callable(callable_or_name):
- return callable_or_name
-
- from ._entrypoints import _get_ep
-
- return _get_ep(group, callable_or_name)
-
-
-def tag_to_version(
- tag: _VersionT | str, config: _config.Configuration
-) -> _VersionT | None:
- """
- take a tag that might be prefixed with a keyword and return only the version part
- """
- log.debug("tag %s", tag)
-
- tag_dict = _parse_version_tag(tag, config)
- if tag_dict is None or not tag_dict.get("version", None):
- warnings.warn(f"tag {tag!r} no version found")
- return None
-
- version_str = tag_dict["version"]
- log.debug("version pre parse %s", version_str)
-
- # 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
-
- # 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
-
-
-def _source_epoch_or_utc_now() -> datetime:
- if "SOURCE_DATE_EPOCH" in os.environ:
- date_epoch = int(os.environ["SOURCE_DATE_EPOCH"])
- return datetime.fromtimestamp(date_epoch, timezone.utc)
- else:
- return datetime.now(timezone.utc)
-
-
-@dataclasses.dataclass
-class ScmVersion:
- """represents a parsed version from scm"""
-
- tag: _v.Version | _v.NonNormalizedVersion
- """the related tag or preformatted version"""
- config: _config.Configuration
- """the configuration used to parse the version"""
- distance: int = 0
- """the number of commits since the tag"""
- node: str | None = None
- """the shortened node id"""
- dirty: bool = False
- """whether the working copy had uncommitted changes"""
- preformatted: bool = False
- """whether the version string was preformatted"""
- branch: str | None = None
- """the branch name if any"""
- node_date: date | None = None
- """the date of the commit if available"""
- time: datetime = dataclasses.field(default_factory=_source_epoch_or_utc_now)
- """the current time or source epoch time
- only set for unit-testing version schemes
- for real usage it must be `now(utc)` or `SOURCE_EPOCH`
- """
-
- @property
- 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""
- )
-
- def format_with(self, fmt: str, **kw: object) -> str:
- """format a given format string with attributes of this object"""
- return fmt.format(
- time=self.time,
- tag=self.tag,
- distance=self.distance,
- node=_format_node_for_output(self.node),
- dirty=self.dirty,
- branch=self.branch,
- node_date=self.node_date,
- **kw,
- )
-
- def format_choice(self, clean_format: str, dirty_format: str, **kw: object) -> str:
- """given `clean_format` and `dirty_format`
-
- choose one based on `self.dirty` and format it using `self.format_with`"""
-
- return self.format_with(dirty_format if self.dirty else clean_format, **kw)
-
- def format_next_version(
- self,
- guess_next: Callable[Concatenate[ScmVersion, _P], str],
- fmt: str = "{guessed}.dev{distance}",
- *k: _P.args,
- **kw: _P.kwargs,
- ) -> str:
- guessed = guess_next(self, *k, **kw)
- return self.format_with(fmt, guessed=guessed)
-
-
-def _parse_tag(
- tag: _VersionT | str, preformatted: bool, config: _config.Configuration
-) -> _VersionT:
- if preformatted:
- # 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
- return version
- else:
- return tag
-
-
-def meta(
- tag: str | _VersionT,
- *,
- distance: int = 0,
- dirty: bool = False,
- node: str | None = None,
- preformatted: bool = False,
- branch: str | None = None,
- config: _config.Configuration,
- node_date: date | None = None,
- time: datetime | None = None,
-) -> ScmVersion:
- 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}"
- scm_version = ScmVersion(
- parsed_version,
- distance=distance,
- node=node,
- dirty=dirty,
- preformatted=preformatted,
- branch=branch,
- 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:
- version = _modify_version.strip_local(str(tag_version.tag))
- return _modify_version._bump_dev(version) or _modify_version._bump_regex(version)
-
-
-def guess_next_dev_version(version: ScmVersion) -> str:
- if version.exact:
- return version.format_with("{tag}")
- else:
- return version.format_next_version(guess_next_version)
-
-
-def guess_next_simple_semver(
- version: ScmVersion, retain: int, increment: bool = True
-) -> str:
- if isinstance(version.tag, _v.Version):
- parts = list(version.tag.release[:retain])
- else:
- try:
- parts = [int(i) for i in str(version.tag).split(".")[:retain]]
- except ValueError:
- raise ValueError(f"{version} can't be parsed as numeric version") from None
- while len(parts) < retain:
- parts.append(0)
- if increment:
- parts[-1] += 1
- while len(parts) < SEMVER_LEN:
- parts.append(0)
- return ".".join(str(i) for i in parts)
-
-
-def simplified_semver_version(version: ScmVersion) -> str:
- if version.exact:
- return guess_next_simple_semver(version, retain=SEMVER_LEN, increment=False)
- elif version.branch is not None and "feature" in version.branch:
- return version.format_next_version(
- guess_next_simple_semver, retain=SEMVER_MINOR
- )
- else:
- return version.format_next_version(
- guess_next_simple_semver, retain=SEMVER_PATCH
- )
-
-
-def release_branch_semver_version(version: ScmVersion) -> str:
- if version.exact:
- return version.format_with("{tag}")
- if version.branch is not None:
- # Does the branch name (stripped of namespace) parse as a version?
- branch_ver_data = _parse_version_tag(
- version.branch.split("/")[-1], version.config
- )
- if branch_ver_data is not None:
- branch_ver = branch_ver_data["version"]
- if branch_ver[0] == "v":
- # Allow branches that start with 'v', similar to Version.
- branch_ver = branch_ver[1:]
- # Does the branch version up to the minor part match the tag? If not it
- # might be like, an issue number or something and not a version number, so
- # we only want to use it if it matches.
- tag_ver_up_to_minor = str(version.tag).split(".")[:SEMVER_MINOR]
- branch_ver_up_to_minor = branch_ver.split(".")[:SEMVER_MINOR]
- if branch_ver_up_to_minor == tag_ver_up_to_minor:
- # We're in a release/maintenance branch, next is a patch/rc/beta bump:
- return version.format_next_version(guess_next_version)
- # We're in a development branch, next is a minor bump:
- return version.format_next_version(guess_next_simple_semver, retain=SEMVER_MINOR)
-
-
-def release_branch_semver(version: ScmVersion) -> str:
- warnings.warn(
- "release_branch_semver is deprecated and will be removed in the future. "
- "Use release_branch_semver_version instead",
- category=DeprecationWarning,
- stacklevel=2,
- )
- return release_branch_semver_version(version)
-
-
-def only_version(version: ScmVersion) -> str:
- return version.format_with("{tag}")
-
-
-def no_guess_dev_version(version: ScmVersion) -> str:
- if version.exact:
- return version.format_with("{tag}")
- else:
- return version.format_next_version(_modify_version._dont_guess_next_version)
-
-
-_DATE_REGEX = re.compile(
- r"""
- ^(?P
- (?P[vV]?)
- (?P\d{2}|\d{4})(?:\.\d{1,2}){2})
- (?:\.(?P\d*))?$
- """,
- re.VERBOSE,
-)
-
-
-def date_ver_match(ver: str) -> Match[str] | None:
- return _DATE_REGEX.match(ver)
-
-
-def guess_next_date_ver(
- version: ScmVersion,
- node_date: date | None = None,
- date_fmt: str | None = None,
- version_cls: type | None = None,
-) -> str:
- """
- same-day -> patch +1
- other-day -> today
-
- distance is always added as .devX
- """
- match = date_ver_match(str(version.tag))
- if match is None:
- warnings.warn(
- f"{version} does not correspond to a valid versioning date, "
- "assuming legacy version"
- )
- if date_fmt is None:
- date_fmt = "%y.%m.%d"
- else:
- # deduct date format if not provided
- if date_fmt is None:
- date_fmt = "%Y.%m.%d" if len(match.group("year")) == 4 else "%y.%m.%d"
- if prefix := match.group("prefix"):
- if not date_fmt.startswith(prefix):
- date_fmt = prefix + date_fmt
-
- today = version.time.date()
- head_date = node_date or today
- # compute patch
- if match is None:
- # 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)
- .replace(tzinfo=timezone.utc)
- .date()
- )
- if tag_date == head_date:
- 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 (only for actual date tags, not legacy)
- warnings.warn(
- f"your previous tag ({tag_date}) is ahead your node date ({head_date})"
- )
- patch = 0
- next_version = "{node_date:{date_fmt}}.{patch}".format(
- node_date=head_date, date_fmt=date_fmt, patch=patch
- )
- # rely on the Version object to ensure consistency (e.g. remove leading 0s)
- if version_cls is None:
- version_cls = PkgVersion
- next_version = str(version_cls(next_version))
- return next_version
-
-
-def calver_by_date(version: ScmVersion) -> str:
- if version.exact and not version.dirty:
- return version.format_with("{tag}")
- # TODO: move the release-X check to a new scheme
- if version.branch is not None and version.branch.startswith("release-"):
- branch_ver = _parse_version_tag(version.branch.split("-")[-1], version.config)
- if branch_ver is not None:
- ver = branch_ver["version"]
- match = date_ver_match(ver)
- if match:
- return ver
- return version.format_next_version(
- guess_next_date_ver,
- node_date=version.node_date,
- version_cls=version.config.version_cls,
- )
-
-
-def get_local_node_and_date(version: ScmVersion) -> str:
- return _modify_version._format_local_with_time(version, time_format="%Y%m%d")
-
-
-def get_local_node_and_timestamp(version: ScmVersion) -> str:
- return _modify_version._format_local_with_time(version, time_format="%Y%m%d%H%M%S")
-
-
-def get_local_dirty_tag(version: ScmVersion) -> str:
- return version.format_choice("", "+dirty")
-
-
-def get_no_local_node(version: ScmVersion) -> str:
- return ""
-
-
-def postrelease_version(version: ScmVersion) -> str:
- if version.exact:
- return version.format_with("{tag}")
- else:
- 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:
- 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_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)
-
- # 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/src/vcs_versioning_workspace/__init__.py b/src/vcs_versioning_workspace/__init__.py
new file mode 100644
index 00000000..d422aa93
--- /dev/null
+++ b/src/vcs_versioning_workspace/__init__.py
@@ -0,0 +1 @@
+"""Workspace automation tools for setuptools-scm monorepo."""
diff --git a/src/vcs_versioning_workspace/create_release_proposal.py b/src/vcs_versioning_workspace/create_release_proposal.py
new file mode 100644
index 00000000..b448d1e5
--- /dev/null
+++ b/src/vcs_versioning_workspace/create_release_proposal.py
@@ -0,0 +1,333 @@
+#!/usr/bin/env python3
+"""Unified release proposal script for setuptools-scm monorepo."""
+
+import argparse
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+from github import Github
+from github.Repository import Repository
+from vcs_versioning._config import Configuration
+from vcs_versioning._get_version_impl import ( # type: ignore[attr-defined]
+ _format_version,
+ parse_version,
+)
+from vcs_versioning._version_schemes._towncrier import get_release_version
+
+
+def find_fragments(project_dir: Path) -> list[Path]:
+ """Find changelog fragments in a project directory."""
+ changelog_dir = project_dir / "changelog.d"
+
+ if not changelog_dir.exists():
+ return []
+
+ fragments = []
+ for entry in changelog_dir.iterdir():
+ if not entry.is_file():
+ continue
+
+ # Skip template, README, and .gitkeep files
+ if entry.name in ("template.md", "README.md", ".gitkeep"):
+ continue
+
+ # Fragment naming: {number}.{type}.md
+ parts = entry.name.split(".")
+ if len(parts) >= 2 and entry.suffix == ".md":
+ fragments.append(entry)
+
+ return fragments
+
+
+def get_next_version(project_dir: Path, repo_root: Path) -> str | None:
+ """Get the next version for a project using vcs-versioning API.
+
+ Uses get_release_version() to produce clean versions without .devN suffix.
+ """
+ try:
+ # Load configuration from project's pyproject.toml
+ # All project-specific settings (tag_regex, fallback_version, etc.) are in the config files
+ # Override local_scheme to get clean version strings
+ pyproject = project_dir / "pyproject.toml"
+ config = Configuration.from_file(pyproject, local_scheme="no-local-version")
+
+ # Get the ScmVersion object
+ scm_version = parse_version(config)
+ if scm_version is None:
+ print(f"ERROR: Could not parse version for {project_dir}", file=sys.stderr)
+ return None
+
+ # Use get_release_version for clean version (no .devN suffix)
+ version_string = get_release_version(scm_version)
+ if version_string is None:
+ # No fragments found, fall back to standard formatting
+ version_string = _format_version(scm_version)
+
+ # Extract just the public version (X.Y.Z)
+ return version_string.split("+")[0] # Remove local part if present
+
+ except Exception as e:
+ print(f"Error determining version: {e}", file=sys.stderr)
+ return None
+
+
+def run_towncrier(project_dir: Path, version: str, *, draft: bool = False) -> bool:
+ """Run towncrier build for a project."""
+ try:
+ cmd = ["uv", "run", "towncrier", "build", "--version", version]
+ if draft:
+ cmd.append("--draft")
+ else:
+ cmd.append("--yes")
+
+ result = subprocess.run(
+ cmd,
+ cwd=project_dir,
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+
+ if result.returncode != 0:
+ print(f"Towncrier failed: {result.stderr}", file=sys.stderr)
+ return False
+
+ return True
+
+ except Exception as e:
+ print(f"Error running towncrier: {e}", file=sys.stderr)
+ return False
+
+
+def check_existing_pr(repo: Repository, source_branch: str) -> tuple[str, int | None]:
+ """
+ Check for existing release PR.
+
+ Returns:
+ Tuple of (release_branch, pr_number)
+ """
+ release_branch = f"release/{source_branch}"
+ repo_owner = repo.owner.login
+
+ try:
+ # PRs target the same branch they came from (main→main, develop→develop)
+ pulls = repo.get_pulls(
+ state="open", base=source_branch, head=f"{repo_owner}:{release_branch}"
+ )
+
+ for pr in pulls:
+ print(f"Found existing release PR #{pr.number}")
+ return release_branch, pr.number
+
+ print("No existing release PR found, will create new")
+ return release_branch, None
+
+ except Exception as e:
+ print(f"Error checking for PR: {e}", file=sys.stderr)
+ return release_branch, None
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Create release proposal")
+ parser.add_argument(
+ "--event",
+ help="GitHub event type (push or pull_request)",
+ )
+ parser.add_argument(
+ "--branch",
+ help="Source branch name (defaults to current branch)",
+ )
+ args = parser.parse_args()
+
+ # Get environment variables
+ token = os.environ.get("GITHUB_TOKEN")
+ repo_name = os.environ.get("GITHUB_REPOSITORY")
+
+ # Determine source branch
+ if args.branch:
+ source_branch = args.branch
+ else:
+ # Get current branch from git
+ try:
+ result = subprocess.run(
+ ["git", "branch", "--show-current"],
+ capture_output=True,
+ text=True,
+ check=True,
+ )
+ source_branch = result.stdout.strip()
+ print(f"Using current branch: {source_branch}")
+ except subprocess.CalledProcessError:
+ print("ERROR: Could not determine current branch", file=sys.stderr)
+ sys.exit(1)
+
+ is_pr = args.event == "pull_request" if args.event else False
+
+ # GitHub integration is optional
+ github_mode = bool(token and repo_name)
+
+ if github_mode:
+ # Type narrowing: when github_mode is True, both token and repo_name are not None
+ assert token is not None
+ assert repo_name is not None
+ print(f"GitHub mode: enabled (repo: {repo_name})")
+ # Initialize GitHub API
+ gh = Github(token)
+ repo = gh.get_repo(repo_name)
+
+ # Check for existing PR (skip for pull_request events)
+ if not is_pr:
+ release_branch, existing_pr_number = check_existing_pr(repo, source_branch)
+ else:
+ release_branch = f"release/{source_branch}"
+ existing_pr_number = None
+ print(
+ f"[PR VALIDATION MODE] Validating release for branch: {source_branch}"
+ )
+ else:
+ print("GitHub mode: disabled (missing GITHUB_TOKEN or GITHUB_REPOSITORY)")
+ release_branch = f"release/{source_branch}"
+ existing_pr_number = None
+
+ repo_root = Path.cwd()
+ projects = {
+ "setuptools-scm": repo_root / "setuptools-scm",
+ "vcs-versioning": repo_root / "vcs-versioning",
+ }
+
+ # Detect which projects have fragments
+ to_release = {}
+ for project_name, project_path in projects.items():
+ fragments = find_fragments(project_path)
+ to_release[project_name] = len(fragments) > 0
+
+ if to_release[project_name]:
+ print(f"Found {len(fragments)} fragment(s) for {project_name}")
+ else:
+ print(f"No fragments found for {project_name}")
+
+ # Exit if no projects have fragments
+ if not any(to_release.values()):
+ print("No changelog fragments found in any project, skipping release")
+
+ # Write GitHub Step Summary (if in GitHub mode)
+ if github_mode:
+ github_summary = os.environ.get("GITHUB_STEP_SUMMARY")
+ if github_summary:
+ with open(github_summary, "a") as f:
+ f.write("## Release Proposal\n\n")
+ f.write("ℹ️ No changelog fragments to process\n")
+
+ sys.exit(0)
+
+ # Prepare releases
+ releases = []
+ labels = []
+
+ for project_name in ["setuptools-scm", "vcs-versioning"]:
+ if not to_release[project_name]:
+ continue
+
+ print(f"\nPreparing {project_name} release...")
+ project_dir = projects[project_name]
+
+ # Get next version
+ version = get_next_version(project_dir, repo_root)
+ if not version:
+ print(
+ f"ERROR: Failed to determine version for {project_name}",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ print(f"{project_name} next version: {version}")
+
+ # Run towncrier (draft mode for local runs)
+ if not run_towncrier(project_dir, version, draft=not github_mode):
+ print(f"ERROR: Towncrier build failed for {project_name}", file=sys.stderr)
+ sys.exit(1)
+
+ releases.append(f"{project_name} v{version}")
+ labels.append(f"release:{project_name}")
+
+ if not releases:
+ print("ERROR: No releases were prepared", file=sys.stderr)
+ sys.exit(1)
+
+ releases_str = ", ".join(releases)
+ print(f"\nSuccessfully prepared releases: {releases_str}")
+
+ # Write GitHub Actions outputs (if in GitHub mode)
+ if github_mode:
+ github_output = os.environ.get("GITHUB_OUTPUT")
+ if github_output:
+ with open(github_output, "a") as f:
+ f.write(f"release_branch={release_branch}\n")
+ f.write(f"releases={releases_str}\n")
+ f.write(f"labels={','.join(labels)}\n")
+ # PR targets the same branch it came from
+ f.write(f"pr_base={source_branch}\n")
+
+ # Prepare PR content for workflow to use
+ pr_title = f"Release: {releases_str}"
+ pr_body = f"""## Release Proposal
+
+This PR prepares the following releases:
+{releases_str}
+
+**Source branch:** {source_branch}
+
+### Changes
+- Updated CHANGELOG.md with towncrier fragments
+- Removed processed fragments from changelog.d/
+
+### Review Checklist
+- [ ] Changelog entries are accurate
+- [ ] Version numbers are correct
+- [ ] All tests pass
+
+**Merging this PR will automatically create tags and trigger PyPI uploads.**"""
+
+ # Write outputs for workflow
+ if github_output:
+ with open(github_output, "a") as f:
+ # Write PR metadata (multiline strings need special encoding)
+ f.write(f"pr_title={pr_title}\n")
+ # For multiline, use GitHub Actions multiline syntax
+ f.write(f"pr_body<= (3, 11):
- from typing import Self
-else:
- from typing_extensions import Self
-
-from .wd_wrapper import WorkDir
-
-
-def pytest_configure() -> None:
- # 2009-02-13T23:31:30+00:00
- os.environ["SOURCE_DATE_EPOCH"] = "1234567890"
- os.environ["SETUPTOOLS_SCM_DEBUG"] = "1"
-
-
-VERSION_PKGS = ["setuptools", "setuptools_scm", "packaging", "build", "wheel"]
-
-
-def pytest_report_header() -> list[str]:
- from importlib.metadata import version
-
- res = []
- for pkg in VERSION_PKGS:
- pkg_version = version(pkg)
- path = __import__(pkg).__file__
- 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
-
-
-def pytest_addoption(parser: Any) -> None:
- group = parser.getgroup("setuptools_scm")
- group.addoption(
- "--test-legacy", dest="scm_test_virtualenv", default=False, action="store_true"
- )
-
-
-class DebugMode(contextlib.AbstractContextManager): # type: ignore[type-arg]
- from setuptools_scm import _log as __module
-
- def __init__(self) -> None:
- self.__stack = contextlib.ExitStack()
-
- def __enter__(self) -> Self:
- self.enable()
- return self
-
- def __exit__(
- self,
- exc_type: type[BaseException] | None,
- exc_val: BaseException | None,
- exc_tb: TracebackType | None,
- ) -> None:
- self.disable()
-
- def enable(self) -> None:
- self.__stack.enter_context(self.__module.defer_to_pytest())
-
- def disable(self) -> None:
- self.__stack.close()
-
-
-@pytest.fixture(autouse=True)
-def debug_mode() -> Iterator[DebugMode]:
- with DebugMode() as debug_mode:
- yield debug_mode
-
-
-@pytest.fixture
-def wd(tmp_path: Path) -> WorkDir:
- target_wd = tmp_path.resolve() / "wd"
- target_wd.mkdir()
- 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()
- path_git = tmp_path / "repo_git"
- path_git.mkdir()
-
- wd = WorkDir(path_git)
- 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}"
-
- path_hg = tmp_path / "repo_hg"
- run(["hg", "clone", path_git, path_hg, "--config", "extensions.hggit="], tmp_path)
- assert path_hg.exists()
-
- with open(path_hg / ".hg/hgrc", "a") as file:
- file.write("[extensions]\nhggit =\n")
-
- wd_hg = WorkDir(path_hg)
- wd_hg.add_command = "hg add ."
- wd_hg.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"'
-
- return wd_hg, wd
diff --git a/testing/test_internal_log_level.py b/testing/test_internal_log_level.py
deleted file mode 100644
index 68ce8e0b..00000000
--- a/testing/test_internal_log_level.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from __future__ import annotations
-
-import logging
-
-from setuptools_scm import _log
-
-
-def test_log_levels_when_set() -> None:
- assert _log._default_log_level({"SETUPTOOLS_SCM_DEBUG": ""}) == logging.DEBUG
- assert _log._default_log_level({"SETUPTOOLS_SCM_DEBUG": "INFO"}) == logging.DEBUG
- assert _log._default_log_level({"SETUPTOOLS_SCM_DEBUG": "3"}) == logging.DEBUG
-
-
-def test_log_levels_when_unset() -> None:
- assert _log._default_log_level({}) == logging.WARNING
diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py
deleted file mode 100644
index 1f5efbe7..00000000
--- a/testing/wd_wrapper.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from __future__ import annotations
-
-import itertools
-
-from pathlib import Path
-from typing import Any
-
-
-class WorkDir:
- """a simple model for a"""
-
- commit_command: str
- signed_commit_command: str
- add_command: str
-
- def __repr__(self) -> str:
- return f""
-
- def __init__(self, cwd: Path) -> None:
- self.cwd = cwd
- self.__counter = itertools.count()
-
- 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, timeout=timeout).stdout
-
- def write(self, name: str, content: str | bytes) -> Path:
- path = self.cwd / name
- if isinstance(content, bytes):
- path.write_bytes(content)
- else:
- path.write_text(content, encoding="utf-8")
- return path
-
- def _reason(self, given_reason: str | None) -> str:
- if given_reason is None:
- return f"number-{next(self.__counter)}"
- else:
- return given_reason
-
- def add_and_commit(
- self, reason: str | None = None, signed: bool = False, **kwargs: object
- ) -> None:
- self(self.add_command)
- self.commit(reason=reason, signed=signed, **kwargs)
-
- def commit(self, reason: str | None = None, signed: bool = False) -> None:
- reason = self._reason(reason)
- self(
- self.commit_command if not signed else self.signed_commit_command,
- reason=reason,
- )
-
- def commit_testfile(self, reason: str | None = None, signed: bool = False) -> None:
- reason = self._reason(reason)
- self.write("test.txt", f"test {reason}")
- self(self.add_command)
- self.commit(reason=reason, signed=signed)
-
- def get_version(self, **kw: Any) -> str:
- __tracebackhide__ = True
- from setuptools_scm import get_version
-
- version = get_version(root=self.cwd, fallback_root=self.cwd, **kw)
- print(self.cwd.name, version, sep=": ")
- return version
diff --git a/uv.lock b/uv.lock
index 9b55bbaf..6e3e790d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,34 +1,22 @@
version = 1
revision = 3
-requires-python = ">=3.8"
+requires-python = ">=3.10"
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'",
+ "python_full_version >= '3.12'",
+ "python_full_version < '3.12'",
]
-[[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" },
+[manifest]
+members = [
+ "setuptools-scm",
+ "vcs-versioning",
+ "vcs-versioning-workspace",
]
[[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" },
@@ -36,62 +24,22 @@ wheels = [
[[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"
+version = "6.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/d6/6c/57418c4404cd22fe6275b8301ca2b46a8cdaa8157938017a9ae0b3edf363/bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6", size = 26641, upload-time = "2024-09-28T21:41:22.017Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" },
+ { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" },
+ { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" },
]
[[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" },
@@ -99,148 +47,193 @@ wheels = [
[[package]]
name = "build"
-version = "1.2.2.post1"
+version = "1.3.0"
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 = "importlib-metadata", marker = "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" }
+sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544, upload-time = "2025-08-01T21:27:09.268Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382, upload-time = "2025-08-01T21:27:07.844Z" },
]
[[package]]
name = "certifi"
-version = "2025.7.14"
+version = "2025.11.12"
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" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
]
[[package]]
-name = "click"
-version = "8.1.8"
+name = "cffi"
+version = "2.0.0"
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'" },
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
+ { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
+ { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
+ { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
+ { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
+ { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
+ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
]
-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 = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
+ { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
+ { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "click"
-version = "8.2.1"
+version = "8.3.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'" },
+ { name = "colorama", marker = "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" }
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
@@ -253,65 +246,192 @@ wheels = [
]
[[package]]
-name = "exceptiongroup"
-version = "1.3.0"
+name = "coverage"
+version = "7.12.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/4a/0dc3de1c172d35abe512332cfdcc43211b6ebce629e4cc42e6cd25ed8f4d/coverage-7.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b", size = 217409, upload-time = "2025-11-18T13:31:53.122Z" },
+ { url = "https://files.pythonhosted.org/packages/01/c3/086198b98db0109ad4f84241e8e9ea7e5fb2db8c8ffb787162d40c26cc76/coverage-7.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c", size = 217927, upload-time = "2025-11-18T13:31:54.458Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/5f/34614dbf5ce0420828fc6c6f915126a0fcb01e25d16cf141bf5361e6aea6/coverage-7.12.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832", size = 244678, upload-time = "2025-11-18T13:31:55.805Z" },
+ { url = "https://files.pythonhosted.org/packages/55/7b/6b26fb32e8e4a6989ac1d40c4e132b14556131493b1d06bc0f2be169c357/coverage-7.12.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa", size = 246507, upload-time = "2025-11-18T13:31:57.05Z" },
+ { url = "https://files.pythonhosted.org/packages/06/42/7d70e6603d3260199b90fb48b537ca29ac183d524a65cc31366b2e905fad/coverage-7.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73", size = 248366, upload-time = "2025-11-18T13:31:58.362Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/4a/d86b837923878424c72458c5b25e899a3c5ca73e663082a915f5b3c4d749/coverage-7.12.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb", size = 245366, upload-time = "2025-11-18T13:31:59.572Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/c2/2adec557e0aa9721875f06ced19730fdb7fc58e31b02b5aa56f2ebe4944d/coverage-7.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e", size = 246408, upload-time = "2025-11-18T13:32:00.784Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/4b/8bd1f1148260df11c618e535fdccd1e5aaf646e55b50759006a4f41d8a26/coverage-7.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777", size = 244416, upload-time = "2025-11-18T13:32:01.963Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/13/3a248dd6a83df90414c54a4e121fd081fb20602ca43955fbe1d60e2312a9/coverage-7.12.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553", size = 244681, upload-time = "2025-11-18T13:32:03.408Z" },
+ { url = "https://files.pythonhosted.org/packages/76/30/aa833827465a5e8c938935f5d91ba055f70516941078a703740aaf1aa41f/coverage-7.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d", size = 245300, upload-time = "2025-11-18T13:32:04.686Z" },
+ { url = "https://files.pythonhosted.org/packages/38/24/f85b3843af1370fb3739fa7571819b71243daa311289b31214fe3e8c9d68/coverage-7.12.0-cp310-cp310-win32.whl", hash = "sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef", size = 220008, upload-time = "2025-11-18T13:32:05.806Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/a2/c7da5b9566f7164db9eefa133d17761ecb2c2fde9385d754e5b5c80f710d/coverage-7.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022", size = 220943, upload-time = "2025-11-18T13:32:07.166Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" },
+ { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" },
+ { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" },
+ { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" },
+ { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" },
+ { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" },
+ { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" },
+ { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" },
+ { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" },
+ { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" },
+ { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" },
+ { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" },
+ { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" },
+ { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" },
+ { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" },
+ { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" },
+ { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" },
+ { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" },
+ { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" },
+ { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" },
+ { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" },
+ { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" },
+ { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" },
+ { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" },
+ { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" },
+ { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" },
+ { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" },
+ { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" },
+ { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" },
+ { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" },
+ { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" },
+ { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" },
+ { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" },
+ { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" },
+ { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" },
+ { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "cryptography"
+version = "46.0.3"
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'" },
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "typing-extensions", marker = "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" },
+sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
+ { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
+ { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
+ { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
+ { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
+ { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
+ { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
+ { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
+ { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
+ { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
+ { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
+ { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
+ { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
+ { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" },
+ { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" },
+ { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" },
]
[[package]]
-name = "flake8"
-version = "5.0.4"
+name = "exceptiongroup"
+version = "1.3.1"
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'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.12'" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
-name = "flake8"
-version = "7.1.2"
+name = "execnet"
+version = "2.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" }
+sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
]
[[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'" },
+ { name = "mccabe" },
+ { name = "pycodestyle" },
+ { name = "pyflakes" },
]
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 = [
@@ -332,73 +452,43 @@ wheels = [
[[package]]
name = "griffe"
-version = "1.4.0"
+version = "1.15.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'" },
+ { name = "colorama" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" },
]
[[package]]
-name = "griffe"
-version = "1.8.0"
+name = "griffe-public-wildcard-imports"
+version = "0.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 = "colorama", marker = "python_full_version >= '3.9'" },
+ { name = "griffe" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/20/ca4fe29389ce3e9967def02172ae5025d460ea5e1d3587d61c5bb67704a0/griffe_public_wildcard_imports-0.3.0.tar.gz", hash = "sha256:51d17cd1f8d88d59eb7fbbe78dd6d2fcaf49d9e5c72f0bc686aec10f31add1a7", size = 25088, upload-time = "2025-11-08T23:09:23.016Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/95/50/28777bb61fa99b468098d52d2a20729720fc2f303859dccb3a743595fc58/griffe_public_wildcard_imports-0.3.0-py3-none-any.whl", hash = "sha256:77c3d5bf3179c4008a7fb8fe655d5ede7d387095ffd6a0049b4d6c83d158f9fd", size = 5250, upload-time = "2025-11-08T23:09:21.766Z" },
]
[[package]]
name = "idna"
-version = "3.10"
+version = "3.11"
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" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[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'" },
+ { name = "zipp", marker = "python_full_version < '3.12'" },
]
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 = [
@@ -407,11 +497,11 @@ wheels = [
[[package]]
name = "iniconfig"
-version = "2.1.0"
+version = "2.3.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" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
@@ -419,8 +509,7 @@ 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'" },
+ { name = "markupsafe" },
]
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 = [
@@ -428,183 +517,182 @@ wheels = [
]
[[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" },
+name = "librt"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/37/c3/cdff3c10e2e608490dc0a310ccf11ba777b3943ad4fcead2a2ade98c21e1/librt-0.6.3.tar.gz", hash = "sha256:c724a884e642aa2bbad52bb0203ea40406ad742368a5f90da1b220e970384aae", size = 54209, upload-time = "2025-11-29T14:01:56.058Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a6/84/859df8db21dedab2538ddfbe1d486dda3eb66a98c6ad7ba754a99e25e45e/librt-0.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:45660d26569cc22ed30adf583389d8a0d1b468f8b5e518fcf9bfe2cd298f9dd1", size = 27294, upload-time = "2025-11-29T14:00:35.053Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/01/ec3971cf9c4f827f17de6729bdfdbf01a67493147334f4ef8fac68936e3a/librt-0.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:54f3b2177fb892d47f8016f1087d21654b44f7fc4cf6571c1c6b3ea531ab0fcf", size = 27635, upload-time = "2025-11-29T14:00:36.496Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/f9/3efe201df84dd26388d2e0afa4c4dc668c8e406a3da7b7319152faf835a1/librt-0.6.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c5b31bed2c2f2fa1fcb4815b75f931121ae210dc89a3d607fb1725f5907f1437", size = 81768, upload-time = "2025-11-29T14:00:37.451Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/13/f63e60bc219b17f3d8f3d13423cd4972e597b0321c51cac7bfbdd5e1f7b9/librt-0.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f8ed5053ef9fb08d34f1fd80ff093ccbd1f67f147633a84cf4a7d9b09c0f089", size = 85884, upload-time = "2025-11-29T14:00:38.433Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/42/0068f14f39a79d1ce8a19d4988dd07371df1d0a7d3395fbdc8a25b1c9437/librt-0.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f0e4bd9bcb0ee34fa3dbedb05570da50b285f49e52c07a241da967840432513", size = 85830, upload-time = "2025-11-29T14:00:39.418Z" },
+ { url = "https://files.pythonhosted.org/packages/14/1c/87f5af3a9e6564f09e50c72f82fc3057fd42d1facc8b510a707d0438c4ad/librt-0.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8f89c8d20dfa648a3f0a56861946eb00e5b00d6b00eea14bc5532b2fcfa8ef1", size = 88086, upload-time = "2025-11-29T14:00:40.555Z" },
+ { url = "https://files.pythonhosted.org/packages/05/e5/22153b98b88a913b5b3f266f12e57df50a2a6960b3f8fcb825b1a0cfe40a/librt-0.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecc2c526547eacd20cb9fbba19a5268611dbc70c346499656d6cf30fae328977", size = 86470, upload-time = "2025-11-29T14:00:41.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/3c/ea1edb587799b1edcc22444e0630fa422e32d7aaa5bfb5115b948acc2d1c/librt-0.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fbedeb9b48614d662822ee514567d2d49a8012037fc7b4cd63f282642c2f4b7d", size = 89079, upload-time = "2025-11-29T14:00:42.882Z" },
+ { url = "https://files.pythonhosted.org/packages/73/ad/50bb4ae6b07c9f3ab19653e0830a210533b30eb9a18d515efb5a2b9d0c7c/librt-0.6.3-cp310-cp310-win32.whl", hash = "sha256:0765b0fe0927d189ee14b087cd595ae636bef04992e03fe6dfdaa383866c8a46", size = 19820, upload-time = "2025-11-29T14:00:44.211Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/12/7426ee78f3b1dbe11a90619d54cb241ca924ca3c0ff9ade3992178e9b440/librt-0.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:8c659f9fb8a2f16dc4131b803fa0144c1dadcb3ab24bb7914d01a6da58ae2457", size = 21332, upload-time = "2025-11-29T14:00:45.427Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/80/bc60fd16fe24910bf5974fb914778a2e8540cef55385ab2cb04a0dfe42c4/librt-0.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:61348cc488b18d1b1ff9f3e5fcd5ac43ed22d3e13e862489d2267c2337285c08", size = 27285, upload-time = "2025-11-29T14:00:46.626Z" },
+ { url = "https://files.pythonhosted.org/packages/88/3c/26335536ed9ba097c79cffcee148393592e55758fe76d99015af3e47a6d0/librt-0.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64645b757d617ad5f98c08e07620bc488d4bced9ced91c6279cec418f16056fa", size = 27629, upload-time = "2025-11-29T14:00:47.863Z" },
+ { url = "https://files.pythonhosted.org/packages/af/fd/2dcedeacfedee5d2eda23e7a49c1c12ce6221b5d58a13555f053203faafc/librt-0.6.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:26b8026393920320bb9a811b691d73c5981385d537ffc5b6e22e53f7b65d4122", size = 82039, upload-time = "2025-11-29T14:00:49.131Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ff/6aa11914b83b0dc2d489f7636942a8e3322650d0dba840db9a1b455f3caa/librt-0.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d998b432ed9ffccc49b820e913c8f327a82026349e9c34fa3690116f6b70770f", size = 86560, upload-time = "2025-11-29T14:00:50.403Z" },
+ { url = "https://files.pythonhosted.org/packages/76/a1/d25af61958c2c7eb978164aeba0350719f615179ba3f428b682b9a5fdace/librt-0.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e18875e17ef69ba7dfa9623f2f95f3eda6f70b536079ee6d5763ecdfe6cc9040", size = 86494, upload-time = "2025-11-29T14:00:51.383Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/4b/40e75d3b258c801908e64b39788f9491635f9554f8717430a491385bd6f2/librt-0.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a218f85081fc3f70cddaed694323a1ad7db5ca028c379c214e3a7c11c0850523", size = 88914, upload-time = "2025-11-29T14:00:52.688Z" },
+ { url = "https://files.pythonhosted.org/packages/97/6d/0070c81aba8a169224301c75fb5fb6c3c25ca67e6ced086584fc130d5a67/librt-0.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ef42ff4edd369e84433ce9b188a64df0837f4f69e3d34d3b34d4955c599d03f", size = 86944, upload-time = "2025-11-29T14:00:53.768Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/94/809f38887941b7726692e0b5a083dbdc87dbb8cf893e3b286550c5f0b129/librt-0.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e0f2b79993fec23a685b3e8107ba5f8675eeae286675a216da0b09574fa1e47", size = 89852, upload-time = "2025-11-29T14:00:54.71Z" },
+ { url = "https://files.pythonhosted.org/packages/58/a3/b0e5b1cda675b91f1111d8ba941da455d8bfaa22f4d2d8963ba96ccb5b12/librt-0.6.3-cp311-cp311-win32.whl", hash = "sha256:fd98cacf4e0fabcd4005c452cb8a31750258a85cab9a59fb3559e8078da408d7", size = 19948, upload-time = "2025-11-29T14:00:55.989Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/73/70011c2b37e3be3ece3affd3abc8ebe5cda482b03fd6b3397906321a901e/librt-0.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:e17b5b42c8045867ca9d1f54af00cc2275198d38de18545edaa7833d7e9e4ac8", size = 21406, upload-time = "2025-11-29T14:00:56.874Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ee/119aa759290af6ca0729edf513ca390c1afbeae60f3ecae9b9d56f25a8a9/librt-0.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:87597e3d57ec0120a3e1d857a708f80c02c42ea6b00227c728efbc860f067c45", size = 20875, upload-time = "2025-11-29T14:00:57.752Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/2c/b59249c566f98fe90e178baf59e83f628d6c38fb8bc78319301fccda0b5e/librt-0.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74418f718083009108dc9a42c21bf2e4802d49638a1249e13677585fcc9ca176", size = 27841, upload-time = "2025-11-29T14:00:58.925Z" },
+ { url = "https://files.pythonhosted.org/packages/40/e8/9db01cafcd1a2872b76114c858f81cc29ce7ad606bc102020d6dabf470fb/librt-0.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:514f3f363d1ebc423357d36222c37e5c8e6674b6eae8d7195ac9a64903722057", size = 27844, upload-time = "2025-11-29T14:01:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/59/4d/da449d3a7d83cc853af539dee42adc37b755d7eea4ad3880bacfd84b651d/librt-0.6.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cf1115207a5049d1f4b7b4b72de0e52f228d6c696803d94843907111cbf80610", size = 84091, upload-time = "2025-11-29T14:01:01.118Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/6c/f90306906fb6cc6eaf4725870f0347115de05431e1f96d35114392d31fda/librt-0.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8ba80cdcea04bea7b78fcd4925bfbf408961e9d8397d2ee5d3ec121e20c08c", size = 88239, upload-time = "2025-11-29T14:01:02.11Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/ae/473ce7b423cfac2cb503851a89d9d2195bf615f534d5912bf86feeebbee7/librt-0.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4018904c83eab49c814e2494b4e22501a93cdb6c9f9425533fe693c3117126f9", size = 88815, upload-time = "2025-11-29T14:01:03.114Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/6d/934df738c87fb9617cabefe4891eece585a06abe6def25b4bca3b174429d/librt-0.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8983c5c06ac9c990eac5eb97a9f03fe41dc7e9d7993df74d9e8682a1056f596c", size = 90598, upload-time = "2025-11-29T14:01:04.071Z" },
+ { url = "https://files.pythonhosted.org/packages/72/89/eeaa124f5e0f431c2b39119550378ae817a4b1a3c93fd7122f0639336fff/librt-0.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7769c579663a6f8dbf34878969ac71befa42067ce6bf78e6370bf0d1194997c", size = 88603, upload-time = "2025-11-29T14:01:05.02Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/ed/c60b3c1cfc27d709bc0288af428ce58543fcb5053cf3eadbc773c24257f5/librt-0.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d3c9a07eafdc70556f8c220da4a538e715668c0c63cabcc436a026e4e89950bf", size = 92112, upload-time = "2025-11-29T14:01:06.304Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ab/f56169be5f716ef4ab0277be70bcb1874b4effc262e655d85b505af4884d/librt-0.6.3-cp312-cp312-win32.whl", hash = "sha256:38320386a48a15033da295df276aea93a92dfa94a862e06893f75ea1d8bbe89d", size = 20127, upload-time = "2025-11-29T14:01:07.283Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/8d/222750ce82bf95125529eaab585ac7e2829df252f3cfc05d68792fb1dd2c/librt-0.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:c0ecf4786ad0404b072196b5df774b1bb23c8aacdcacb6c10b4128bc7b00bd01", size = 21545, upload-time = "2025-11-29T14:01:08.184Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c9/f731ddcfb72f446a92a8674c6b8e1e2242773cce43a04f41549bd8b958ff/librt-0.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:9f2a6623057989ebc469cd9cc8fe436c40117a0147627568d03f84aef7854c55", size = 20946, upload-time = "2025-11-29T14:01:09.384Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/aa/3055dd440f8b8b3b7e8624539a0749dd8e1913e978993bcca9ce7e306231/librt-0.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e716f9012148a81f02f46a04fc4c663420c6fbfeacfac0b5e128cf43b4413d3", size = 27874, upload-time = "2025-11-29T14:01:10.615Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/93/226d7dd455eaa4c26712b5ccb2dfcca12831baa7f898c8ffd3a831e29fda/librt-0.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:669ff2495728009a96339c5ad2612569c6d8be4474e68f3f3ac85d7c3261f5f5", size = 27852, upload-time = "2025-11-29T14:01:11.535Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/8b/db9d51191aef4e4cc06285250affe0bb0ad8b2ed815f7ca77951655e6f02/librt-0.6.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:349b6873ebccfc24c9efd244e49da9f8a5c10f60f07575e248921aae2123fc42", size = 84264, upload-time = "2025-11-29T14:01:12.461Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/53/297c96bda3b5a73bdaf748f1e3ae757edd29a0a41a956b9c10379f193417/librt-0.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c74c26736008481c9f6d0adf1aedb5a52aff7361fea98276d1f965c0256ee70", size = 88432, upload-time = "2025-11-29T14:01:13.405Z" },
+ { url = "https://files.pythonhosted.org/packages/54/3a/c005516071123278e340f22de72fa53d51e259d49215295c212da16c4dc2/librt-0.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:408a36ddc75e91918cb15b03460bdc8a015885025d67e68c6f78f08c3a88f522", size = 89014, upload-time = "2025-11-29T14:01:14.373Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/9b/ea715f818d926d17b94c80a12d81a79e95c44f52848e61e8ca1ff29bb9a9/librt-0.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e61ab234624c9ffca0248a707feffe6fac2343758a36725d8eb8a6efef0f8c30", size = 90807, upload-time = "2025-11-29T14:01:15.377Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/fc/4e2e4c87e002fa60917a8e474fd13c4bac9a759df82be3778573bb1ab954/librt-0.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:324462fe7e3896d592b967196512491ec60ca6e49c446fe59f40743d08c97917", size = 88890, upload-time = "2025-11-29T14:01:16.633Z" },
+ { url = "https://files.pythonhosted.org/packages/70/7f/c7428734fbdfd4db3d5b9237fc3a857880b2ace66492836f6529fef25d92/librt-0.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36b2ec8c15030002c7f688b4863e7be42820d7c62d9c6eece3db54a2400f0530", size = 92300, upload-time = "2025-11-29T14:01:17.658Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/0c/738c4824fdfe74dc0f95d5e90ef9e759d4ecf7fd5ba964d54a7703322251/librt-0.6.3-cp313-cp313-win32.whl", hash = "sha256:25b1b60cb059471c0c0c803e07d0dfdc79e41a0a122f288b819219ed162672a3", size = 20159, upload-time = "2025-11-29T14:01:18.61Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/95/93d0e61bc617306ecf4c54636b5cbde4947d872563565c4abdd9d07a39d3/librt-0.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:10a95ad074e2a98c9e4abc7f5b7d40e5ecbfa84c04c6ab8a70fabf59bd429b88", size = 21484, upload-time = "2025-11-29T14:01:19.506Z" },
+ { url = "https://files.pythonhosted.org/packages/10/23/abd7ace79ab54d1dbee265f13529266f686a7ce2d21ab59a992f989009b6/librt-0.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:17000df14f552e86877d67e4ab7966912224efc9368e998c96a6974a8d609bf9", size = 20935, upload-time = "2025-11-29T14:01:20.415Z" },
+ { url = "https://files.pythonhosted.org/packages/83/14/c06cb31152182798ed98be73f54932ab984894f5a8fccf9b73130897a938/librt-0.6.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8e695f25d1a425ad7a272902af8ab8c8d66c1998b177e4b5f5e7b4e215d0c88a", size = 27566, upload-time = "2025-11-29T14:01:21.609Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/b1/ce83ca7b057b06150519152f53a0b302d7c33c8692ce2f01f669b5a819d9/librt-0.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e84a4121a7ae360ca4da436548a9c1ca8ca134a5ced76c893cc5944426164bd", size = 27753, upload-time = "2025-11-29T14:01:22.558Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/ec/739a885ef0a2839b6c25f1b01c99149d2cb6a34e933ffc8c051fcd22012e/librt-0.6.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05f385a414de3f950886ea0aad8f109650d4b712cf9cc14cc17f5f62a9ab240b", size = 83178, upload-time = "2025-11-29T14:01:23.555Z" },
+ { url = "https://files.pythonhosted.org/packages/db/bd/dc18bb1489d48c0911b9f4d72eae2d304ea264e215ba80f1e6ba4a9fc41d/librt-0.6.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36a8e337461150b05ca2c7bdedb9e591dfc262c5230422cea398e89d0c746cdc", size = 87266, upload-time = "2025-11-29T14:01:24.532Z" },
+ { url = "https://files.pythonhosted.org/packages/94/f3/d0c5431b39eef15e48088b2d739ad84b17c2f1a22c0345c6d4c4a42b135e/librt-0.6.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcbe48f6a03979384f27086484dc2a14959be1613cb173458bd58f714f2c48f3", size = 87623, upload-time = "2025-11-29T14:01:25.798Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/15/9a52e90834e4bd6ee16cdbaf551cb32227cbaad27398391a189c489318bc/librt-0.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4bca9e4c260233fba37b15c4ec2f78aa99c1a79fbf902d19dd4a763c5c3fb751", size = 89436, upload-time = "2025-11-29T14:01:26.769Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/8a/a7e78e46e8486e023c50f21758930ef4793999115229afd65de69e94c9cc/librt-0.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760c25ed6ac968e24803eb5f7deb17ce026902d39865e83036bacbf5cf242aa8", size = 87540, upload-time = "2025-11-29T14:01:27.756Z" },
+ { url = "https://files.pythonhosted.org/packages/49/01/93799044a1cccac31f1074b07c583e181829d240539657e7f305ae63ae2a/librt-0.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4a93a353ccff20df6e34fa855ae8fd788832c88f40a9070e3ddd3356a9f0e", size = 90597, upload-time = "2025-11-29T14:01:29.35Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/29/00c7f58b8f8eb1bad6529ffb6c9cdcc0890a27dac59ecda04f817ead5277/librt-0.6.3-cp314-cp314-win32.whl", hash = "sha256:cb92741c2b4ea63c09609b064b26f7f5d9032b61ae222558c55832ec3ad0bcaf", size = 18955, upload-time = "2025-11-29T14:01:30.325Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/13/2739e6e197a9f751375a37908a6a5b0bff637b81338497a1bcb5817394da/librt-0.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:fdcd095b1b812d756fa5452aca93b962cf620694c0cadb192cec2bb77dcca9a2", size = 20263, upload-time = "2025-11-29T14:01:31.287Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/73/393868fc2158705ea003114a24e73bb10b03bda31e9ad7b5c5ec6575338b/librt-0.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:822ca79e28720a76a935c228d37da6579edef048a17cd98d406a2484d10eda78", size = 19575, upload-time = "2025-11-29T14:01:32.229Z" },
+ { url = "https://files.pythonhosted.org/packages/48/6d/3c8ff3dec21bf804a205286dd63fd28dcdbe00b8dd7eb7ccf2e21a40a0b0/librt-0.6.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:078cd77064d1640cb7b0650871a772956066174d92c8aeda188a489b58495179", size = 28732, upload-time = "2025-11-29T14:01:33.165Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/90/e214b8b4aa34ed3d3f1040719c06c4d22472c40c5ef81a922d5af7876eb4/librt-0.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5cc22f7f5c0cc50ed69f4b15b9c51d602aabc4500b433aaa2ddd29e578f452f7", size = 29065, upload-time = "2025-11-29T14:01:34.088Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/90/ef61ed51f0a7770cc703422d907a757bbd8811ce820c333d3db2fd13542a/librt-0.6.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:14b345eb7afb61b9fdcdfda6738946bd11b8e0f6be258666b0646af3b9bb5916", size = 93703, upload-time = "2025-11-29T14:01:35.057Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ae/c30bb119c35962cbe9a908a71da99c168056fc3f6e9bbcbc157d0b724d89/librt-0.6.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d46aa46aa29b067f0b8b84f448fd9719aaf5f4c621cc279164d76a9dc9ab3e8", size = 98890, upload-time = "2025-11-29T14:01:36.031Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/96/47a4a78d252d36f072b79d592df10600d379a895c3880c8cbd2ac699f0ad/librt-0.6.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b51ba7d9d5d9001494769eca8c0988adce25d0a970c3ba3f2eb9df9d08036fc", size = 98255, upload-time = "2025-11-29T14:01:37.058Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/28/779b5cc3cd9987683884eb5f5672e3251676bebaaae6b7da1cf366eb1da1/librt-0.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ced0925a18fddcff289ef54386b2fc230c5af3c83b11558571124bfc485b8c07", size = 100769, upload-time = "2025-11-29T14:01:38.413Z" },
+ { url = "https://files.pythonhosted.org/packages/28/d7/771755e57c375cb9d25a4e106f570607fd856e2cb91b02418db1db954796/librt-0.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6bac97e51f66da2ca012adddbe9fd656b17f7368d439de30898f24b39512f40f", size = 98580, upload-time = "2025-11-29T14:01:39.459Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/ec/8b157eb8fbc066339a2f34b0aceb2028097d0ed6150a52e23284a311eafe/librt-0.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b2922a0e8fa97395553c304edc3bd36168d8eeec26b92478e292e5d4445c1ef0", size = 101706, upload-time = "2025-11-29T14:01:40.474Z" },
+ { url = "https://files.pythonhosted.org/packages/82/a8/4aaead9a06c795a318282aebf7d3e3e578fa889ff396e1b640c3be4c7806/librt-0.6.3-cp314-cp314t-win32.whl", hash = "sha256:f33462b19503ba68d80dac8a1354402675849259fb3ebf53b67de86421735a3a", size = 19465, upload-time = "2025-11-29T14:01:41.77Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/61/b7e6a02746c1731670c19ba07d86da90b1ae45d29e405c0b5615abf97cde/librt-0.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:04f8ce401d4f6380cfc42af0f4e67342bf34c820dae01343f58f472dbac75dcf", size = 21042, upload-time = "2025-11-29T14:01:42.865Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/3d/72cc9ec90bb80b5b1a65f0bb74a0f540195837baaf3b98c7fa4a7aa9718e/librt-0.6.3-cp314-cp314t-win_arm64.whl", hash = "sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0", size = 20246, upload-time = "2025-11-29T14:01:44.13Z" },
]
[[package]]
name = "markdown"
-version = "3.8.2"
+version = "3.10"
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" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" },
]
[[package]]
name = "markdown-it-py"
-version = "3.0.0"
+version = "4.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" }
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[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" },
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
+ { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
+ { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
+ { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
+ { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
+ { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
+ { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
+ { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
+ { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
+ { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
+ { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
+ { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
+ { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
+ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
+ { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
+ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
@@ -639,24 +727,18 @@ 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 = "click" },
{ 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 = "markdown" },
+ { name = "markupsafe" },
{ 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 = "pyyaml-env-tag" },
{ 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" }
@@ -666,39 +748,28 @@ wheels = [
[[package]]
name = "mkdocs-autorefs"
-version = "1.2.0"
+version = "1.4.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'",
-]
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'" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" },
]
[[package]]
-name = "mkdocs-autorefs"
-version = "1.4.2"
+name = "mkdocs-entangled-plugin"
+version = "0.2.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 = "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" },
]
-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" }
+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/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" },
+ { 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]]
@@ -706,11 +777,8 @@ 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 = "platformdirs" },
{ 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" }
@@ -720,62 +788,37 @@ wheels = [
[[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"
+version = "7.2.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 = "mkdocs", marker = "python_full_version >= '3.9'" },
- { name = "wcmatch", version = "10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "mkdocs" },
+ { name = "wcmatch" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/90/10/b0b75ac42f4613556a808eee2dad3efe7a7d5079349aa5b9229d863e829f/mkdocs_include_markdown_plugin-7.2.0.tar.gz", hash = "sha256:4a67a91ade680dc0e15f608e5b6343bec03372ffa112c40a4254c1bfb10f42f3", size = 25509, upload-time = "2025-09-28T21:50:50.41Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/ba/f9/783338d1d7fd548c7635728b67a0f8f96d9e6c265aa61c51356c03597767/mkdocs_include_markdown_plugin-7.2.0-py3-none-any.whl", hash = "sha256:d56cdaeb2d113fb66ed0fe4fb7af1da889926b0b9872032be24e19bbb09c9f5b", size = 29548, upload-time = "2025-09-28T21:50:49.373Z" },
]
[[package]]
name = "mkdocs-material"
-version = "9.6.15"
+version = "9.7.0"
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 = "backrefs" },
{ 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 = "markdown" },
{ 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 = "pymdown-extensions" },
{ 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" }
+sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" },
]
[[package]]
@@ -789,143 +832,85 @@ wheels = [
[[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"
+version = "1.0.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'" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
+ { name = "mkdocs-autorefs" },
+ { name = "pymdown-extensions" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/e5/13/10bbf9d56565fd91b91e6f5a8cd9b9d8a2b101c4e8ad6eeafa35a706301d/mkdocstrings-1.0.0.tar.gz", hash = "sha256:351a006dbb27aefce241ade110d3cd040c1145b7a3eb5fd5ac23f03ed67f401a", size = 101086, upload-time = "2025-11-27T15:39:40.534Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/ec/fc/80aa31b79133634721cf7855d37b76ea49773599214896f2ff10be03de2a/mkdocstrings-1.0.0-py3-none-any.whl", hash = "sha256:4c50eb960bff6e05dfc631f6bc00dfabffbcb29c5ff25f676d64daae05ed82fa", size = 35135, upload-time = "2025-11-27T15:39:39.301Z" },
]
[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" },
+ { name = "mkdocstrings-python" },
]
[[package]]
name = "mkdocstrings-python"
-version = "1.16.12"
+version = "2.0.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 = "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'" },
+ { name = "griffe" },
+ { name = "mkdocs-autorefs" },
+ { name = "mkdocstrings" },
+ { name = "typing-extensions", marker = "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" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/0d/dab7b08ca7e5a38b033cd83565bb0f95f05e8f3df7bc273e793c2ad3576e/mkdocstrings_python-2.0.0.tar.gz", hash = "sha256:4d872290f595221740a304bebca5b3afa4beafe84cc6fd27314d52dc3fbb4676", size = 199113, upload-time = "2025-11-27T16:44:44.894Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/79/de/063481352688c3a1468c51c10b6cfb858d5e35dfef8323d9c83c4f2faa03/mkdocstrings_python-2.0.0-py3-none-any.whl", hash = "sha256:1d552dda109d47e4fddecbb1f06f9a86699c1b073e8b166fba89eeef0a0ffec6", size = 104803, upload-time = "2025-11-27T16:44:43.441Z" },
]
[[package]]
name = "mypy"
-version = "1.13.0"
+version = "1.19.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "librt" },
{ name = "mypy-extensions" },
+ { name = "pathspec" },
{ 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" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" },
+ { url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" },
+ { url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" },
+ { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" },
+ { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" },
+ { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" },
+ { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" },
+ { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" },
+ { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" },
+ { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" },
+ { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" },
+ { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" },
+ { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" },
+ { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" },
]
[[package]]
@@ -966,159 +951,72 @@ wheels = [
[[package]]
name = "pip"
-version = "25.0.1"
+version = "25.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/70/53/b309b4a497b09655cb7e07088966881a57d082f48ac3cb54ea729fd2c6cf/pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea", size = 1950850, upload-time = "2025-02-09T17:14:04.423Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014, upload-time = "2025-10-25T00:55:41.394Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622, upload-time = "2025-10-25T00:55:39.247Z" },
]
[[package]]
name = "platformdirs"
-version = "4.3.8"
+version = "4.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/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
]
[[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 = "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 = "pyflakes"
-version = "2.5.0"
+name = "pycparser"
+version = "2.23"
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" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
name = "pyflakes"
-version = "3.2.0"
+version = "3.4.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" }
+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/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" },
+ { 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 = "pyflakes"
-version = "3.4.0"
+name = "pygithub"
+version = "2.8.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 = "pyjwt", extra = ["crypto"] },
+ { name = "pynacl" },
+ { name = "requests" },
+ { name = "typing-extensions" },
+ { name = "urllib3" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/c1/74/e560bdeffea72ecb26cff27f0fad548bbff5ecc51d6a155311ea7f9e4c4c/pygithub-2.8.1.tar.gz", hash = "sha256:341b7c78521cb07324ff670afd1baa2bf5c286f8d9fd302c1798ba594a5400c9", size = 2246994, upload-time = "2025-09-02T17:41:54.674Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/07/ba/7049ce39f653f6140aac4beb53a5aaf08b4407b6a3019aae394c1c5244ff/pygithub-2.8.1-py3-none-any.whl", hash = "sha256:23a0a5bca93baef082e03411bf0ce27204c32be8bfa7abc92fe4a3e132936df0", size = 432709, upload-time = "2025-09-02T17:41:52.947Z" },
]
[[package]]
@@ -1131,38 +1029,61 @@ wheels = [
]
[[package]]
-name = "pymdown-extensions"
-version = "10.15"
+name = "pyjwt"
+version = "2.10.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 = "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" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
+]
+
+[package.optional-dependencies]
+crypto = [
+ { name = "cryptography" },
]
[[package]]
name = "pymdown-extensions"
-version = "10.16"
+version = "10.17.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 = "pyyaml", marker = "python_full_version >= '3.9'" },
+ { name = "markdown" },
+ { name = "pyyaml" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/25/6d/af5378dbdb379fddd9a277f8b9888c027db480cde70028669ebd009d642a/pymdown_extensions-10.17.2.tar.gz", hash = "sha256:26bb3d7688e651606260c90fb46409fbda70bf9fdc3623c7868643a1aeee4713", size = 847344, upload-time = "2025-11-26T15:43:57.004Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/93/78/b93cb80bd673bdc9f6ede63d8eb5b4646366953df15667eb3603be57a2b1/pymdown_extensions-10.17.2-py3-none-any.whl", hash = "sha256:bffae79a2e8b9e44aef0d813583a8fea63457b7a23643a43988055b7b79b4992", size = 266556, upload-time = "2025-11-26T15:43:55.162Z" },
+]
+
+[[package]]
+name = "pynacl"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b2/46/aeca065d227e2265125aea590c9c47fbf5786128c9400ee0eb7c88931f06/pynacl-1.6.1.tar.gz", hash = "sha256:8d361dac0309f2b6ad33b349a56cd163c98430d409fa503b10b70b3ad66eaa1d", size = 3506616, upload-time = "2025-11-10T16:02:13.195Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/75/d6/4b2dca33ed512de8f54e5c6074aa06eaeb225bfbcd9b16f33a414389d6bd/pynacl-1.6.1-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:7d7c09749450c385301a3c20dca967a525152ae4608c0a096fe8464bfc3df93d", size = 389109, upload-time = "2025-11-10T16:01:28.79Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/30/e8dbb8ff4fa2559bbbb2187ba0d0d7faf728d17cb8396ecf4a898b22d3da/pynacl-1.6.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc734c1696ffd49b40f7c1779c89ba908157c57345cf626be2e0719488a076d3", size = 808254, upload-time = "2025-11-10T16:01:37.839Z" },
+ { url = "https://files.pythonhosted.org/packages/44/f9/f5449c652f31da00249638dbab065ad4969c635119094b79b17c3a4da2ab/pynacl-1.6.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3cd787ec1f5c155dc8ecf39b1333cfef41415dc96d392f1ce288b4fe970df489", size = 1407365, upload-time = "2025-11-10T16:01:40.454Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/2f/9aa5605f473b712065c0a193ebf4ad4725d7a245533f0cd7e5dcdbc78f35/pynacl-1.6.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b35d93ab2df03ecb3aa506be0d3c73609a51449ae0855c2e89c7ed44abde40b", size = 843842, upload-time = "2025-11-10T16:01:30.524Z" },
+ { url = "https://files.pythonhosted.org/packages/32/8d/748f0f6956e207453da8f5f21a70885fbbb2e060d5c9d78e0a4a06781451/pynacl-1.6.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dece79aecbb8f4640a1adbb81e4aa3bfb0e98e99834884a80eb3f33c7c30e708", size = 1445559, upload-time = "2025-11-10T16:01:33.663Z" },
+ { url = "https://files.pythonhosted.org/packages/78/d0/2387f0dcb0e9816f38373999e48db4728ed724d31accdd4e737473319d35/pynacl-1.6.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c2228054f04bf32d558fb89bb99f163a8197d5a9bf4efa13069a7fa8d4b93fc3", size = 825791, upload-time = "2025-11-10T16:01:34.823Z" },
+ { url = "https://files.pythonhosted.org/packages/18/3d/ef6fb7eb072aaf15f280bc66f26ab97e7fc9efa50fb1927683013ef47473/pynacl-1.6.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:2b12f1b97346f177affcdfdc78875ff42637cb40dcf79484a97dae3448083a78", size = 1410843, upload-time = "2025-11-10T16:01:36.401Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/fb/23824a017526850ee7d8a1cc4cd1e3e5082800522c10832edbbca8619537/pynacl-1.6.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e735c3a1bdfde3834503baf1a6d74d4a143920281cb724ba29fb84c9f49b9c48", size = 801140, upload-time = "2025-11-10T16:01:42.013Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/d1/ebc6b182cb98603a35635b727d62f094bc201bf610f97a3bb6357fe688d2/pynacl-1.6.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3384a454adf5d716a9fadcb5eb2e3e72cd49302d1374a60edc531c9957a9b014", size = 1371966, upload-time = "2025-11-10T16:01:43.297Z" },
+ { url = "https://files.pythonhosted.org/packages/49/41/3cfb3b4f3519f6ff62bf71bf1722547644bcfb1b05b8fdbdc300249ba113/pynacl-1.6.1-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:a6f9fd6d6639b1e81115c7f8ff16b8dedba1e8098d2756275d63d208b0e32021", size = 387591, upload-time = "2025-11-10T16:01:49.1Z" },
+ { url = "https://files.pythonhosted.org/packages/18/21/b8a6563637799f617a3960f659513eccb3fcc655d5fc2be6e9dc6416826f/pynacl-1.6.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e49a3f3d0da9f79c1bec2aa013261ab9fa651c7da045d376bd306cf7c1792993", size = 798866, upload-time = "2025-11-10T16:01:55.688Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/6c/dc38033bc3ea461e05ae8f15a81e0e67ab9a01861d352ae971c99de23e7c/pynacl-1.6.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7713f8977b5d25f54a811ec9efa2738ac592e846dd6e8a4d3f7578346a841078", size = 1398001, upload-time = "2025-11-10T16:01:57.101Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/05/3ec0796a9917100a62c5073b20c4bce7bf0fea49e99b7906d1699cc7b61b/pynacl-1.6.1-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a3becafc1ee2e5ea7f9abc642f56b82dcf5be69b961e782a96ea52b55d8a9fc", size = 834024, upload-time = "2025-11-10T16:01:50.228Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/b7/ae9982be0f344f58d9c64a1c25d1f0125c79201634efe3c87305ac7cb3e3/pynacl-1.6.1-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ce50d19f1566c391fedc8dc2f2f5be265ae214112ebe55315e41d1f36a7f0a9", size = 1436766, upload-time = "2025-11-10T16:01:51.886Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/51/b2ccbf89cf3025a02e044dd68a365cad593ebf70f532299f2c047d2b7714/pynacl-1.6.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:543f869140f67d42b9b8d47f922552d7a967e6c116aad028c9bfc5f3f3b3a7b7", size = 817275, upload-time = "2025-11-10T16:01:53.351Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/6c/dd9ee8214edf63ac563b08a9b30f98d116942b621d39a751ac3256694536/pynacl-1.6.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a2bb472458c7ca959aeeff8401b8efef329b0fc44a89d3775cffe8fad3398ad8", size = 1401891, upload-time = "2025-11-10T16:01:54.587Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/c1/97d3e1c83772d78ee1db3053fd674bc6c524afbace2bfe8d419fd55d7ed1/pynacl-1.6.1-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3206fa98737fdc66d59b8782cecc3d37d30aeec4593d1c8c145825a345bba0f0", size = 772291, upload-time = "2025-11-10T16:01:58.111Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/ca/691ff2fe12f3bb3e43e8e8df4b806f6384593d427f635104d337b8e00291/pynacl-1.6.1-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:53543b4f3d8acb344f75fd4d49f75e6572fce139f4bfb4815a9282296ff9f4c0", size = 1370839, upload-time = "2025-11-10T16:01:59.252Z" },
+ { url = "https://files.pythonhosted.org/packages/30/27/06fe5389d30391fce006442246062cc35773c84fbcad0209fbbf5e173734/pynacl-1.6.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:319de653ef84c4f04e045eb250e6101d23132372b0a61a7acf91bac0fda8e58c", size = 791371, upload-time = "2025-11-10T16:02:01.075Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/7a/e2bde8c9d39074a5aa046c7d7953401608d1f16f71e237f4bef3fb9d7e49/pynacl-1.6.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:262a8de6bba4aee8a66f5edf62c214b06647461c9b6b641f8cd0cb1e3b3196fe", size = 1363031, upload-time = "2025-11-10T16:02:02.656Z" },
]
[[package]]
@@ -1176,46 +1097,34 @@ wheels = [
[[package]]
name = "pytest"
-version = "8.3.5"
+version = "9.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 = "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'" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
]
[[package]]
-name = "pytest"
-version = "8.4.1"
+name = "pytest-cov"
+version = "7.0.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' 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'" },
+ { name = "coverage", extra = ["toml"] },
+ { name = "pluggy" },
+ { name = "pytest" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
@@ -1223,8 +1132,7 @@ 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'" },
+ { name = "pytest" },
]
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 = [
@@ -1232,113 +1140,100 @@ wheels = [
]
[[package]]
-name = "python-dateutil"
-version = "2.9.0.post0"
+name = "pytest-xdist"
+version = "3.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "six" },
+ { name = "execnet" },
+ { name = "pytest" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
]
[[package]]
-name = "pytz"
-version = "2025.2"
+name = "python-dateutil"
+version = "2.9.0.post0"
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" }
+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/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
+ { 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 = "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" },
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[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'" },
+ { name = "pyyaml" },
]
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 = [
@@ -1347,82 +1242,62 @@ wheels = [
[[package]]
name = "requests"
-version = "2.32.4"
+version = "2.32.5"
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'" },
+ { name = "urllib3" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "rich"
-version = "13.9.4"
+version = "14.2.0"
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" }
+sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
]
[[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" },
+version = "0.14.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324, upload-time = "2025-11-28T20:55:10.525Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475, upload-time = "2025-11-28T20:54:54.569Z" },
+ { url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613, upload-time = "2025-11-28T20:55:17.54Z" },
+ { url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458, upload-time = "2025-11-28T20:55:26.128Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412, upload-time = "2025-11-28T20:55:28.602Z" },
+ { url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949, upload-time = "2025-11-28T20:55:33.265Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470, upload-time = "2025-11-28T20:55:08.203Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621, upload-time = "2025-11-28T20:55:30.837Z" },
+ { url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817, upload-time = "2025-11-28T20:55:24.107Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549, upload-time = "2025-11-28T20:55:06.08Z" },
+ { url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389, upload-time = "2025-11-28T20:55:35.617Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679, upload-time = "2025-11-28T20:55:01.472Z" },
+ { url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677, upload-time = "2025-11-28T20:55:19.933Z" },
+ { url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392, upload-time = "2025-11-28T20:55:21.967Z" },
+ { url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230, upload-time = "2025-11-28T20:55:12.749Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397, upload-time = "2025-11-28T20:54:56.896Z" },
+ { url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751, upload-time = "2025-11-28T20:55:03.47Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726, upload-time = "2025-11-28T20:54:59.121Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" },
]
[[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" },
@@ -1430,14 +1305,13 @@ wheels = [
[[package]]
name = "setuptools-scm"
-source = { editable = "." }
+source = { editable = "setuptools-scm" }
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 = "setuptools" },
{ 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.*'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+ { name = "vcs-versioning" },
]
[package.optional-dependencies]
@@ -1447,32 +1321,27 @@ rich = [
[package.dev-dependencies]
docs = [
+ { name = "griffe-public-wildcard-imports" },
{ name = "mkdocs" },
- { 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-entangled-plugin" },
+ { name = "mkdocs-include-markdown-plugin" },
{ 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 = "mkdocstrings", extra = ["python"] },
{ 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 = "flake8" },
+ { name = "griffe" },
+ { name = "griffe-public-wildcard-imports" },
+ { name = "mypy", marker = "implementation_name != 'pypy'" },
+ { name = "pip" },
+ { name = "pytest" },
{ name = "pytest-timeout" },
+ { name = "pytest-xdist" },
{ 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" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
[package.metadata]
@@ -1481,13 +1350,16 @@ requires-dist = [
{ 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'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+ { name = "vcs-versioning", editable = "vcs-versioning" },
]
provides-extras = ["rich", "simple", "toml"]
[package.metadata.requires-dev]
docs = [
+ { name = "griffe-public-wildcard-imports" },
{ name = "mkdocs" },
+ { name = "mkdocs-entangled-plugin" },
{ name = "mkdocs-include-markdown-plugin" },
{ name = "mkdocs-material" },
{ name = "mkdocstrings", extras = ["python"] },
@@ -1497,14 +1369,15 @@ test = [
{ name = "build" },
{ name = "flake8" },
{ name = "griffe" },
- { name = "mypy", specifier = "~=1.13.0" },
+ { name = "griffe-public-wildcard-imports" },
+ { name = "mypy", marker = "implementation_name != 'pypy'" },
{ name = "pip" },
{ name = "pytest" },
{ name = "pytest-timeout" },
+ { name = "pytest-xdist" },
{ name = "rich" },
{ name = "ruff" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
- { name = "wheel" },
]
[[package]]
@@ -1518,192 +1391,219 @@ wheels = [
[[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 = "typing-extensions"
-version = "4.13.2"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" },
+ { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" },
+ { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" },
+ { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" },
+ { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" },
+ { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" },
+ { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" },
+ { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" },
+ { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" },
+ { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" },
+ { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" },
+ { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" },
+ { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" },
+ { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" },
+ { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" },
+ { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" },
+ { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" },
+ { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" },
+ { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" },
+ { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" },
+ { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" },
+ { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" },
+ { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" },
+ { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" },
+ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
+]
+
+[[package]]
+name = "towncrier"
+version = "25.8.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 = "click" },
+ { name = "jinja2" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/c2/eb/5bf25a34123698d3bbab39c5bc5375f8f8bcbcc5a136964ade66935b8b9d/towncrier-25.8.0.tar.gz", hash = "sha256:eef16d29f831ad57abb3ae32a0565739866219f1ebfbdd297d32894eb9940eb1", size = 76322, upload-time = "2025-08-30T11:41:55.393Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/42/06/8ba22ec32c74ac1be3baa26116e3c28bc0e76a5387476921d20b6fdade11/towncrier-25.8.0-py3-none-any.whl", hash = "sha256:b953d133d98f9aeae9084b56a3563fd2519dfc6ec33f61c9cd2c61ff243fb513", size = 65101, upload-time = "2025-08-30T11:41:53.644Z" },
]
[[package]]
-name = "typing-extensions"
-version = "4.14.1"
+name = "types-setuptools"
+version = "80.9.0.20250822"
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" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/bd/1e5f949b7cb740c9f0feaac430e301b8f1c5f11a81e26324299ea671a237/types_setuptools-80.9.0.20250822.tar.gz", hash = "sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965", size = 41296, upload-time = "2025-08-22T03:02:08.771Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/b6/2d/475bf15c1cdc172e7a0d665b6e373ebfb1e9bf734d3f2f543d668b07a142/types_setuptools-80.9.0.20250822-py3-none-any.whl", hash = "sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3", size = 63179, upload-time = "2025-08-22T03:02:07.643Z" },
]
[[package]]
-name = "urllib3"
-version = "2.2.3"
+name = "typing-extensions"
+version = "4.15.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/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
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" },
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[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" },
+name = "vcs-versioning"
+source = { editable = "vcs-versioning" }
+dependencies = [
+ { name = "packaging" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-[[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'",
+[package.dev-dependencies]
+test = [
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "pytest-xdist" },
]
-dependencies = [
- { name = "bracex", version = "2.5.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
+
+[package.metadata]
+requires-dist = [
+ { name = "packaging", specifier = ">=20" },
+ { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-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.metadata.requires-dev]
+test = [
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "pytest-xdist" },
]
[[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.*'",
-]
+name = "vcs-versioning-workspace"
+version = "0.1+private"
+source = { editable = "." }
dependencies = [
- { name = "bracex", version = "2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "setuptools-scm" },
+ { name = "vcs-versioning" },
]
-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.dev-dependencies]
+docs = [
+ { name = "griffe-public-wildcard-imports" },
+ { name = "mkdocs" },
+ { name = "mkdocs-include-markdown-plugin" },
+ { name = "mkdocs-material" },
+ { name = "mkdocstrings", extra = ["python"] },
+ { name = "pygments" },
+]
+release = [
+ { name = "pygithub", marker = "sys_platform != 'win32'" },
+ { name = "towncrier" },
+]
+typing = [
+ { name = "types-setuptools" },
]
+[package.metadata]
+requires-dist = [
+ { name = "setuptools-scm", editable = "setuptools-scm" },
+ { name = "vcs-versioning", editable = "vcs-versioning" },
+]
+
+[package.metadata.requires-dev]
+docs = [
+ { name = "griffe-public-wildcard-imports" },
+ { name = "mkdocs" },
+ { name = "mkdocs-include-markdown-plugin" },
+ { name = "mkdocs-material" },
+ { name = "mkdocstrings", extras = ["python"], specifier = ">=0.29" },
+ { name = "pygments" },
+]
+release = [
+ { name = "pygithub", marker = "sys_platform != 'win32'", specifier = ">=2.0.0" },
+ { name = "towncrier", specifier = ">=23.11.0" },
+]
+typing = [{ name = "types-setuptools" }]
+
[[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" },
+name = "watchdog"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" },
+ { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" },
+ { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
+ { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
+ { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
+ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
+ { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
+ { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
+ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
+ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
]
[[package]]
-name = "zipp"
-version = "3.20.2"
+name = "wcmatch"
+version = "10.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 = "bracex" },
]
-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" }
+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/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200, upload-time = "2024-09-13T13:44:14.38Z" },
+ { 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 = "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" },
diff --git a/vcs-versioning/CHANGELOG.md b/vcs-versioning/CHANGELOG.md
new file mode 100644
index 00000000..f0dec6e4
--- /dev/null
+++ b/vcs-versioning/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog
+
+
+
+## 0.1.1
+
+Initial release of vcs-versioning as a separate package extracted from setuptools-scm.
+
diff --git a/nextgen/vcs-versioning/LICENSE.txt b/vcs-versioning/LICENSE.txt
similarity index 100%
rename from nextgen/vcs-versioning/LICENSE.txt
rename to vcs-versioning/LICENSE.txt
diff --git a/nextgen/vcs-versioning/README.md b/vcs-versioning/README.md
similarity index 100%
rename from nextgen/vcs-versioning/README.md
rename to vcs-versioning/README.md
diff --git a/vcs-versioning/_own_version_of_vcs_versioning.py b/vcs-versioning/_own_version_of_vcs_versioning.py
new file mode 100644
index 00000000..d8ee6b7c
--- /dev/null
+++ b/vcs-versioning/_own_version_of_vcs_versioning.py
@@ -0,0 +1,84 @@
+"""
+Version helper for vcs-versioning package.
+
+This module allows vcs-versioning to use VCS metadata for its own version,
+with the tag prefix 'vcs-versioning-'.
+
+Used by hatchling's code version source.
+"""
+
+from __future__ import annotations
+
+import logging
+import os
+from collections.abc import Callable
+
+from vcs_versioning import Configuration
+from vcs_versioning import _types as _t
+from vcs_versioning._backends import _git as git
+from vcs_versioning._backends import _hg as hg
+from vcs_versioning._fallbacks import fallback_version, parse_pkginfo
+from vcs_versioning._get_version_impl import get_version
+from vcs_versioning._version_schemes import (
+ ScmVersion,
+ get_local_node_and_date,
+ get_no_local_node,
+ guess_next_dev_version,
+)
+
+log = logging.getLogger("vcs_versioning")
+
+# Try these parsers in order for vcs-versioning's own version
+try_parse: list[Callable[[_t.PathT, Configuration], ScmVersion | None]] = [
+ parse_pkginfo,
+ git.parse,
+ hg.parse,
+ git.parse_archival,
+ hg.parse_archival,
+ fallback_version, # Last resort: use fallback_version from config
+]
+
+
+def parse(root: str, config: Configuration) -> ScmVersion | None:
+ for maybe_parse in try_parse:
+ try:
+ parsed = maybe_parse(root, config)
+ except OSError as e:
+ log.warning("parse with %s failed with: %s", maybe_parse, e)
+ else:
+ if parsed is not None:
+ return parsed
+ return None
+
+
+def _get_version() -> str:
+ """Get version from VCS with vcs-versioning- tag prefix."""
+ # Use no-local-version if VCS_VERSIONING_NO_LOCAL is set (for CI uploads)
+ local_scheme = (
+ get_no_local_node
+ if os.environ.get("VCS_VERSIONING_NO_LOCAL")
+ else get_local_node_and_date
+ )
+
+ # __file__ is nextgen/vcs-versioning/_own_version_helper.py
+ # pyproject.toml is in nextgen/vcs-versioning/pyproject.toml
+ pyproject_path = os.path.join(os.path.dirname(__file__), "pyproject.toml")
+
+ # root is the git repo root (../..)
+ # fallback_root is the vcs-versioning package dir (.)
+ # relative_to anchors to pyproject.toml
+ # fallback_version is used when no vcs-versioning- tags exist yet
+ return get_version(
+ root="../..",
+ fallback_root=".",
+ relative_to=pyproject_path,
+ parse=parse,
+ version_scheme=guess_next_dev_version,
+ local_scheme=local_scheme,
+ tag_regex=r"^vcs-versioning-(?P[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$",
+ git_describe_command="git describe --dirty --tags --long --match 'vcs-versioning-*'",
+ fallback_version="0.1.1+pre.tag",
+ )
+
+
+__version__: str = _get_version()
diff --git a/vcs-versioning/changelog.d/.gitkeep b/vcs-versioning/changelog.d/.gitkeep
new file mode 100644
index 00000000..c199cfd6
--- /dev/null
+++ b/vcs-versioning/changelog.d/.gitkeep
@@ -0,0 +1,8 @@
+# Changelog fragments directory
+# Add your changelog fragments here following the naming convention:
+# {issue_number}.{type}.md
+#
+# Where type is one of: feature, bugfix, deprecation, removal, doc, misc
+#
+# Example: 123.feature.md
+
diff --git a/vcs-versioning/changelog.d/README.md b/vcs-versioning/changelog.d/README.md
new file mode 100644
index 00000000..3ea52129
--- /dev/null
+++ b/vcs-versioning/changelog.d/README.md
@@ -0,0 +1,32 @@
+# Changelog Fragments
+
+This directory contains changelog fragments that will be assembled into the CHANGELOG.md file during release.
+
+## Fragment Types
+
+- **feature**: New features or enhancements
+- **bugfix**: Bug fixes
+- **deprecation**: Deprecation warnings
+- **removal**: Removed features (breaking changes)
+- **doc**: Documentation improvements
+- **misc**: Internal changes, refactoring, etc.
+
+## Naming Convention
+
+Fragments should be named: `{issue_number}.{type}.md`
+
+Examples:
+- `123.feature.md` - New feature related to issue #123
+- `456.bugfix.md` - Bug fix for issue #456
+- `789.doc.md` - Documentation update for issue #789
+
+## Content
+
+Each fragment should contain a brief description of the change:
+
+```markdown
+Add support for custom version schemes via plugin system
+```
+
+Do not include issue numbers in the content - they will be added automatically.
+
diff --git a/vcs-versioning/changelog.d/cli-package.misc.md b/vcs-versioning/changelog.d/cli-package.misc.md
new file mode 100644
index 00000000..4615653a
--- /dev/null
+++ b/vcs-versioning/changelog.d/cli-package.misc.md
@@ -0,0 +1,2 @@
+Converted _cli module into a package with improved structure. Archival templates moved to resource files. Added CliNamespace for typed arguments.
+
diff --git a/vcs-versioning/changelog.d/cli-typesafety.misc.md b/vcs-versioning/changelog.d/cli-typesafety.misc.md
new file mode 100644
index 00000000..f032bc70
--- /dev/null
+++ b/vcs-versioning/changelog.d/cli-typesafety.misc.md
@@ -0,0 +1,2 @@
+Improved CLI type safety with OutputData TypedDict and better type annotations throughout CLI handling.
+
diff --git a/vcs-versioning/changelog.d/env-reader.feature.md b/vcs-versioning/changelog.d/env-reader.feature.md
new file mode 100644
index 00000000..15f3763f
--- /dev/null
+++ b/vcs-versioning/changelog.d/env-reader.feature.md
@@ -0,0 +1,2 @@
+Add EnvReader class for structured reading of environment variable overrides with tool prefixes and distribution-specific variants (e.g., SETUPTOOLS_SCM_PRETEND vs VCS_VERSIONING_PRETEND).
+
diff --git a/vcs-versioning/changelog.d/initial-release.feature.md b/vcs-versioning/changelog.d/initial-release.feature.md
new file mode 100644
index 00000000..57cad615
--- /dev/null
+++ b/vcs-versioning/changelog.d/initial-release.feature.md
@@ -0,0 +1,2 @@
+Initial release of vcs-versioning as a standalone package. Core version inference logic extracted from setuptools-scm for reuse by other build backends and tools.
+
diff --git a/vcs-versioning/changelog.d/integrator-api.feature.md b/vcs-versioning/changelog.d/integrator-api.feature.md
new file mode 100644
index 00000000..cc1bcf61
--- /dev/null
+++ b/vcs-versioning/changelog.d/integrator-api.feature.md
@@ -0,0 +1,2 @@
+Add experimental integrator workflow API for composable configuration building. Allows build backends to progressively build Configuration objects from pyproject.toml, distribution metadata, and manual overrides.
+
diff --git a/vcs-versioning/changelog.d/modernize-types.misc.md b/vcs-versioning/changelog.d/modernize-types.misc.md
new file mode 100644
index 00000000..439f2e4f
--- /dev/null
+++ b/vcs-versioning/changelog.d/modernize-types.misc.md
@@ -0,0 +1,2 @@
+Modernized type annotations to Python 3.10+ syntax throughout codebase. Generated version files now use modern `tuple[int | str, ...]` syntax with `from __future__ import annotations`.
+
diff --git a/vcs-versioning/changelog.d/overrides-validation.misc.md b/vcs-versioning/changelog.d/overrides-validation.misc.md
new file mode 100644
index 00000000..70d7c933
--- /dev/null
+++ b/vcs-versioning/changelog.d/overrides-validation.misc.md
@@ -0,0 +1,2 @@
+Enhanced GlobalOverrides: env_reader is now a required validated field. additional_loggers changed from string to tuple of logger instances for better type safety.
+
diff --git a/vcs-versioning/changelog.d/py310.feature.md b/vcs-versioning/changelog.d/py310.feature.md
new file mode 100644
index 00000000..7b60acb5
--- /dev/null
+++ b/vcs-versioning/changelog.d/py310.feature.md
@@ -0,0 +1,2 @@
+Requires Python 3.10 or newer. Modern type annotations and language features used throughout.
+
diff --git a/vcs-versioning/changelog.d/template.md b/vcs-versioning/changelog.d/template.md
new file mode 100644
index 00000000..41a46689
--- /dev/null
+++ b/vcs-versioning/changelog.d/template.md
@@ -0,0 +1,21 @@
+{% for section, _ in sections.items() %}
+{% set underline = underlines[0] %}{% if section %}{{section}}
+{{ underline * section|length }}{% set underline = underlines[1] %}
+
+{% endif %}
+{% if sections[section] %}
+{% for category, val in definitions.items() if category in sections[section]%}
+
+### {{ definitions[category]['name'] }}
+
+{% for text, values in sections[section][category].items() %}
+- {{ text }} ({{ values|join(', ') }})
+{% endfor %}
+
+{% endfor %}
+{% else %}
+No significant changes.
+
+{% endif %}
+{% endfor %}
+
diff --git a/vcs-versioning/changelog.d/towncrier-scheme.feature.md b/vcs-versioning/changelog.d/towncrier-scheme.feature.md
new file mode 100644
index 00000000..9f8bcc9d
--- /dev/null
+++ b/vcs-versioning/changelog.d/towncrier-scheme.feature.md
@@ -0,0 +1,2 @@
+Add towncrier-fragments version scheme that infers version bumps based on changelog fragment types (feature=minor, bugfix=patch, removal=major).
+
diff --git a/vcs-versioning/pyproject.toml b/vcs-versioning/pyproject.toml
new file mode 100644
index 00000000..f54f892d
--- /dev/null
+++ b/vcs-versioning/pyproject.toml
@@ -0,0 +1,170 @@
+[build-system]
+build-backend = "hatchling.build"
+requires = [
+ "hatchling",
+ "packaging>=20",
+ 'typing-extensions; python_version < "3.11"',
+]
+
+[project]
+name = "vcs-versioning"
+description = "the blessed package to manage your versions by vcs metadata"
+readme = "README.md"
+keywords = [
+]
+license = "MIT"
+authors = [
+ { name = "Ronny Pfannschmidt", email = "opensource@ronnypfannschmidt.de" },
+]
+requires-python = ">=3.10"
+classifiers = [
+ "Development Status :: 1 - Planning",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+]
+dynamic = [
+ "version",
+]
+dependencies = [
+ "packaging>=20",
+ 'tomli>=1; python_version < "3.11"',
+ 'typing-extensions; python_version < "3.11"',
+]
+
+[dependency-groups]
+test = [
+ "pytest",
+ "pytest-cov",
+ "pytest-xdist",
+]
+
+[project.urls]
+Documentation = "https://github.com/pypa/setuptools-scm#readme"
+Issues = "https://github.com/pypa/setuptools-scm/issues"
+Source = "https://github.com/pypa/setuptools-scm"
+
+[project.scripts]
+"vcs-versioning" = "vcs_versioning._cli:main"
+
+[project.entry-points."setuptools_scm.parse_scm"]
+".git" = "vcs_versioning._backends._git:parse"
+".hg" = "vcs_versioning._backends._hg:parse"
+
+[project.entry-points."setuptools_scm.parse_scm_fallback"]
+".git_archival.txt" = "vcs_versioning._backends._git:parse_archival"
+".hg_archival.txt" = "vcs_versioning._backends._hg:parse_archival"
+"PKG-INFO" = "vcs_versioning._fallbacks:parse_pkginfo"
+"pyproject.toml" = "vcs_versioning._fallbacks:fallback_version"
+"setup.py" = "vcs_versioning._fallbacks:fallback_version"
+
+[project.entry-points."setuptools_scm.local_scheme"]
+dirty-tag = "vcs_versioning._version_schemes:get_local_dirty_tag"
+no-local-version = "vcs_versioning._version_schemes:get_no_local_node"
+node-and-date = "vcs_versioning._version_schemes:get_local_node_and_date"
+node-and-timestamp = "vcs_versioning._version_schemes:get_local_node_and_timestamp"
+
+[project.entry-points."setuptools_scm.version_scheme"]
+"calver-by-date" = "vcs_versioning._version_schemes:calver_by_date"
+"guess-next-dev" = "vcs_versioning._version_schemes:guess_next_dev_version"
+"no-guess-dev" = "vcs_versioning._version_schemes:no_guess_dev_version"
+"only-version" = "vcs_versioning._version_schemes:only_version"
+"post-release" = "vcs_versioning._version_schemes:postrelease_version"
+"python-simplified-semver" = "vcs_versioning._version_schemes:simplified_semver_version"
+"release-branch-semver" = "vcs_versioning._version_schemes:release_branch_semver_version"
+"towncrier-fragments" = "vcs_versioning._version_schemes._towncrier:version_from_fragments"
+
+[tool.hatch.version]
+source = "code"
+path = "_own_version_of_vcs_versioning.py"
+search-paths = ["src"]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/vcs_versioning"]
+
+[tool.hatch.envs.default]
+dependencies = [
+ "pytest",
+ "pytest-cov",
+]
+[tool.hatch.envs.default.scripts]
+cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=vcs_versioning --cov=tests {args}"
+no-cov = "cov --no-cov {args}"
+
+[[tool.hatch.envs.test.matrix]]
+python = [ "38", "39", "310", "311", "312", "313" ]
+
+[tool.vcs-versioning]
+root = ".."
+version_scheme = "towncrier-fragments"
+tag_regex = "^vcs-versioning-(?Pv?\\d+(?:\\.\\d+){0,2}[^\\+]*)(?:\\+.*)?$"
+fallback_version = "0.1.0"
+scm.git.describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "vcs-versioning-*"]
+
+[tool.coverage.run]
+branch = true
+parallel = true
+omit = [
+ "vcs_versioning/__about__.py",
+]
+
+[tool.coverage.report]
+exclude_lines = [
+ "no cov",
+ "if __name__ == .__main__.:",
+ "if TYPE_CHECKING:",
+]
+
+[tool.pytest.ini_options]
+testpaths = ["testing_vcs"]
+python_files = ["test_*.py"]
+addopts = ["-ra", "--strict-markers", "-p", "vcs_versioning.test_api"]
+markers = [
+ "issue: marks tests related to specific issues",
+ "skip_commit: allows to skip committing in the helpers",
+]
+
+[tool.uv]
+default-groups = ["test"]
+
+[tool.towncrier]
+directory = "changelog.d"
+filename = "CHANGELOG.md"
+start_string = "\n"
+template = "changelog.d/template.md"
+title_format = "## {version} ({project_date})"
+issue_format = "[#{issue}](https://github.com/pypa/setuptools-scm/issues/{issue})"
+underlines = ["", "", ""]
+
+[[tool.towncrier.type]]
+directory = "removal"
+name = "Removed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "deprecation"
+name = "Deprecated"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "feature"
+name = "Added"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "bugfix"
+name = "Fixed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "doc"
+name = "Documentation"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "misc"
+name = "Miscellaneous"
+showcontent = true
diff --git a/vcs-versioning/src/vcs_versioning/__init__.py b/vcs-versioning/src/vcs_versioning/__init__.py
new file mode 100644
index 00000000..53184038
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/__init__.py
@@ -0,0 +1,110 @@
+"""VCS-based versioning for Python packages
+
+Core functionality for version management based on VCS metadata.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+# Public API exports
+from ._config import DEFAULT_LOCAL_SCHEME, DEFAULT_VERSION_SCHEME, Configuration
+from ._pyproject_reading import PyProjectData
+from ._scm_version import ScmVersion
+from ._version_cls import NonNormalizedVersion, Version
+from ._version_inference import infer_version_string
+
+
+def build_configuration_from_pyproject(
+ pyproject_data: PyProjectData,
+ *,
+ dist_name: str | None = None,
+ **integrator_overrides: Any,
+) -> Configuration:
+ """Build Configuration from PyProjectData with full workflow.
+
+ EXPERIMENTAL API for integrators.
+
+ This helper orchestrates the complete configuration building workflow:
+ 1. Extract config from pyproject_data.section
+ 2. Determine dist_name (argument > pyproject.project_name)
+ 3. Apply integrator overrides (override config file)
+ 4. Apply environment TOML overrides (highest priority)
+ 5. Create and validate Configuration instance
+
+ Integrators create PyProjectData themselves:
+
+ Example 1 - From file:
+ >>> from vcs_versioning import PyProjectData, build_configuration_from_pyproject
+ >>> from vcs_versioning.overrides import GlobalOverrides
+ >>>
+ >>> with GlobalOverrides.from_env("HATCH_VCS", dist_name="my-pkg"):
+ ... pyproject = PyProjectData.from_file("pyproject.toml")
+ ... config = build_configuration_from_pyproject(
+ ... pyproject_data=pyproject,
+ ... dist_name="my-pkg",
+ ... )
+
+ Example 2 - Manual composition:
+ >>> from pathlib import Path
+ >>> from vcs_versioning import PyProjectData, build_configuration_from_pyproject
+ >>>
+ >>> pyproject = PyProjectData(
+ ... path=Path("pyproject.toml"),
+ ... tool_name="vcs-versioning",
+ ... project={"name": "my-pkg"},
+ ... section={"local_scheme": "no-local-version"},
+ ... is_required=True,
+ ... section_present=True,
+ ... project_present=True,
+ ... build_requires=[],
+ ... )
+ >>> config = build_configuration_from_pyproject(
+ ... pyproject_data=pyproject,
+ ... version_scheme="release-branch-semver", # Integrator override
+ ... )
+
+ Args:
+ pyproject_data: Parsed pyproject data (integrator creates this)
+ dist_name: Distribution name (overrides pyproject_data.project_name)
+ **integrator_overrides: Integrator-provided config overrides
+ (override config file, but overridden by env)
+
+ Returns:
+ Configured Configuration instance ready for version inference
+
+ Priority order (highest to lowest):
+ 1. Environment TOML overrides (TOOL_OVERRIDES_FOR_DIST, TOOL_OVERRIDES)
+ 2. Integrator **overrides arguments
+ 3. pyproject_data.section configuration
+ 4. Configuration defaults
+
+ This allows integrators to provide their own transformations
+ while still respecting user environment variable overrides.
+ """
+ from ._integrator_helpers import build_configuration_from_pyproject_internal
+
+ return build_configuration_from_pyproject_internal(
+ pyproject_data=pyproject_data,
+ dist_name=dist_name,
+ **integrator_overrides,
+ )
+
+
+__all__ = [
+ "DEFAULT_LOCAL_SCHEME",
+ "DEFAULT_VERSION_SCHEME",
+ "Configuration",
+ "NonNormalizedVersion",
+ "PyProjectData",
+ "ScmVersion",
+ "Version",
+ "build_configuration_from_pyproject",
+ "infer_version_string",
+]
+
+# Experimental API markers for documentation
+__experimental__ = [
+ "PyProjectData",
+ "build_configuration_from_pyproject",
+]
diff --git a/src/setuptools_scm/__main__.py b/vcs-versioning/src/vcs_versioning/__main__.py
similarity index 100%
rename from src/setuptools_scm/__main__.py
rename to vcs-versioning/src/vcs_versioning/__main__.py
diff --git a/nextgen/vcs-versioning/vcs_versioning/__about__.py b/vcs-versioning/src/vcs_versioning/_backends/__init__.py
similarity index 50%
rename from nextgen/vcs-versioning/vcs_versioning/__about__.py
rename to vcs-versioning/src/vcs_versioning/_backends/__init__.py
index eba4921f..4fc1cea6 100644
--- a/nextgen/vcs-versioning/vcs_versioning/__about__.py
+++ b/vcs-versioning/src/vcs_versioning/_backends/__init__.py
@@ -1,3 +1,3 @@
-from __future__ import annotations
+"""VCS backends (private module)"""
-__version__ = "0.0.1"
+from __future__ import annotations
diff --git a/src/setuptools_scm/git.py b/vcs-versioning/src/vcs_versioning/_backends/_git.py
similarity index 94%
rename from src/setuptools_scm/git.py
rename to vcs-versioning/src/vcs_versioning/_backends/_git.py
index 966ab69c..ebff0ea2 100644
--- a/src/setuptools_scm/git.py
+++ b/vcs-versioning/src/vcs_versioning/_backends/_git.py
@@ -7,32 +7,25 @@
import shlex
import sys
import warnings
-
-from datetime import date
-from datetime import datetime
-from datetime import timezone
+from collections.abc import Callable, Sequence
+from datetime import date, datetime, timezone
from enum import Enum
from os.path import samefile
from pathlib import Path
from typing import TYPE_CHECKING
-from typing import Callable
-from typing import Sequence
-
-from . import Configuration
-from . import _types as _t
-from . import discover
-from ._run_cmd import CompletedProcess as _CompletedProcess
-from ._run_cmd import require_command as _require_command
-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
+
+from .. import _discover as discover
+from .. import _types as _t
+from .._config import Configuration
+from .._integration import data_from_mime
+from .._run_cmd import CompletedProcess as _CompletedProcess
+from .._run_cmd import require_command as _require_command
+from .._run_cmd import run as _run
+from .._scm_version import ScmVersion, meta, tag_to_version
+from ._scm_workdir import Workdir, get_latest_file_mtime
if TYPE_CHECKING:
- from . import hg_git
+ from . import _hg_git as hg_git
log = logging.getLogger(__name__)
REF_TAG_RE = re.compile(r"(?<=\btag: )([^,]+)\b")
@@ -93,7 +86,7 @@ def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdir | None:
real_wd = os.fspath(wd)
else:
str_wd = os.fspath(wd)
- from ._compat import strip_path_suffix
+ from .._compat import strip_path_suffix
real_wd = strip_path_suffix(str_wd, real_wd)
log.debug("real root %s", real_wd)
@@ -199,13 +192,15 @@ def default_describe(self) -> _CompletedProcess:
def warn_on_shallow(wd: GitWorkdir) -> None:
"""experimental, may change at any time"""
if wd.is_shallow():
- warnings.warn(f'"{wd.path}" is shallow and may cause errors')
+ warnings.warn(f'"{wd.path}" is shallow and may cause errors', stacklevel=2)
def fetch_on_shallow(wd: GitWorkdir) -> None:
"""experimental, may change at any time"""
if wd.is_shallow():
- warnings.warn(f'"{wd.path}" was shallow, git fetch was used to rectify')
+ warnings.warn(
+ f'"{wd.path}" was shallow, git fetch was used to rectify', stacklevel=2
+ )
wd.fetch_shallow()
@@ -424,7 +419,7 @@ def archival_to_version(
log.debug("data %s", data)
archival_describe = data.get("describe-name", DESCRIBE_UNSUPPORTED)
if DESCRIBE_UNSUPPORTED in archival_describe:
- warnings.warn("git archive did not support describe output")
+ warnings.warn("git archive did not support describe output", stacklevel=2)
else:
tag, number, node, _ = _git_parse_describe(archival_describe)
return meta(
@@ -442,7 +437,9 @@ def archival_to_version(
if node is None:
return None
elif "$FORMAT" in node.upper():
- warnings.warn("unprocessed git archival found (no export subst applied)")
+ warnings.warn(
+ "unprocessed git archival found (no export subst applied)", stacklevel=2
+ )
return None
else:
return meta("0.0", node=node, config=config)
diff --git a/src/setuptools_scm/hg.py b/vcs-versioning/src/vcs_versioning/_backends/_hg.py
similarity index 92%
rename from src/setuptools_scm/hg.py
rename to vcs-versioning/src/vcs_versioning/_backends/_hg.py
index 42320516..38d7a807 100644
--- a/src/setuptools_scm/hg.py
+++ b/vcs-versioning/src/vcs_versioning/_backends/_hg.py
@@ -3,33 +3,27 @@
import datetime
import logging
import os
-
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
-
-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
+from .. import _types as _t
+from .._config import Configuration
+from .._integration import data_from_mime
+from .._run_cmd import CompletedProcess
+from .._run_cmd import require_command as _require_command
+from .._run_cmd import run as _run
+from .._scm_version import ScmVersion, meta, tag_to_version
+from .._version_cls import Version
+from ._scm_workdir import Workdir, get_latest_file_mtime
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")
+ """Get the hg command from override context or environment."""
+ from ..overrides import get_hg_command
+
+ return get_hg_command()
def run_hg(args: list[str], cwd: _t.PathT, **kwargs: Any) -> CompletedProcess:
@@ -268,8 +262,8 @@ def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None:
if line.startswith("default ="):
path = Path(line.split()[2])
if path.name.endswith(".git") or (path / ".git").exists():
- from .git import _git_parse_inner
- from .hg_git import GitWorkdirHgClient
+ from ._git import _git_parse_inner
+ from ._hg_git import GitWorkdirHgClient
wd_hggit = GitWorkdirHgClient.from_potential_worktree(root)
if wd_hggit:
diff --git a/src/setuptools_scm/hg_git.py b/vcs-versioning/src/vcs_versioning/_backends/_hg_git.py
similarity index 96%
rename from src/setuptools_scm/hg_git.py
rename to vcs-versioning/src/vcs_versioning/_backends/_hg_git.py
index 3e91b20f..a186dbaa 100644
--- a/src/setuptools_scm/hg_git.py
+++ b/vcs-versioning/src/vcs_versioning/_backends/_hg_git.py
@@ -2,17 +2,15 @@
import logging
import os
-
from contextlib import suppress
from datetime import date
from pathlib import Path
-from . import _types as _t
-from ._run_cmd import CompletedProcess as _CompletedProcess
-from .git import GitWorkdir
-from .hg import HgWorkdir
-from .hg import run_hg
-from .scm_workdir import get_latest_file_mtime
+from .. import _types as _t
+from .._run_cmd import CompletedProcess as _CompletedProcess
+from ._git import GitWorkdir
+from ._hg import HgWorkdir, run_hg
+from ._scm_workdir import get_latest_file_mtime
log = logging.getLogger(__name__)
diff --git a/src/setuptools_scm/scm_workdir.py b/vcs-versioning/src/vcs_versioning/_backends/_scm_workdir.py
similarity index 89%
rename from src/setuptools_scm/scm_workdir.py
rename to vcs-versioning/src/vcs_versioning/_backends/_scm_workdir.py
index b3ca7aa8..683adeb6 100644
--- a/src/setuptools_scm/scm_workdir.py
+++ b/vcs-versioning/src/vcs_versioning/_backends/_scm_workdir.py
@@ -1,15 +1,12 @@
from __future__ import annotations
import logging
-
from dataclasses import dataclass
-from datetime import date
-from datetime import datetime
-from datetime import timezone
+from datetime import date, datetime, timezone
from pathlib import Path
-from ._config import Configuration
-from .version import ScmVersion
+from .._config import Configuration
+from .._scm_version import ScmVersion
log = logging.getLogger(__name__)
diff --git a/vcs-versioning/src/vcs_versioning/_cli/__init__.py b/vcs-versioning/src/vcs_versioning/_cli/__init__.py
new file mode 100644
index 00000000..39bd5028
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_cli/__init__.py
@@ -0,0 +1,224 @@
+from __future__ import annotations
+
+import json
+import os
+import sys
+from collections.abc import Iterable
+from importlib.resources import files
+from pathlib import Path
+from typing import TypedDict
+
+from vcs_versioning._overrides import ConfigOverridesDict
+
+from .. import _discover as discover
+from .._config import Configuration
+from .._get_version_impl import _get_version
+from .._pyproject_reading import PyProjectData
+from ._args import CliNamespace, get_cli_parser
+
+
+class OutputData(TypedDict, ConfigOverridesDict, total=False):
+ version: str
+ files: list[str]
+ queries: list[str]
+
+
+def _get_version_for_cli(config: Configuration, opts: CliNamespace) -> str:
+ """Get version string for CLI output, handling special cases and exceptions."""
+ if opts.no_version:
+ return "0.0.0+no-version-was-requested.fake-version"
+
+ 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:
+ version = version.partition(".dev")[0]
+
+ return version
+
+
+def main(
+ args: list[str] | None = None, *, _given_pyproject_data: PyProjectData | None = None
+) -> int:
+ from ..overrides import GlobalOverrides
+
+ # Apply global overrides for the entire CLI execution
+ # Logging is automatically configured when entering the context
+ with GlobalOverrides.from_env("SETUPTOOLS_SCM"):
+ parser = get_cli_parser("python -m vcs_versioning")
+ opts = parser.parse_args(args, namespace=CliNamespace())
+ inferred_root: str = opts.root or "."
+
+ pyproject = opts.config or _find_pyproject(inferred_root)
+
+ try:
+ config = Configuration.from_file(
+ pyproject,
+ root=(os.path.abspath(opts.root) if opts.root is not None else None),
+ pyproject_data=_given_pyproject_data,
+ )
+ except (LookupError, FileNotFoundError) as ex:
+ # no pyproject.toml OR no [tool.setuptools_scm]
+ print(
+ f"Warning: could not use {os.path.relpath(pyproject)},"
+ " using default configuration.\n"
+ f" Reason: {ex}.",
+ file=sys.stderr,
+ )
+ config = Configuration(root=inferred_root)
+
+ version = _get_version_for_cli(config, opts)
+ return command(opts, version, config)
+
+
+# flake8: noqa: C901
+def command(opts: CliNamespace, version: str, config: Configuration) -> int:
+ data: OutputData = {}
+
+ 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")
+ opts.query = ["queries"]
+ data["queries"] = ["files", *config.__dataclass_fields__]
+
+ if opts.query is None:
+ opts.query = []
+
+ if not opts.no_version:
+ data["version"] = version
+
+ if "files" in opts.query:
+ from .._file_finders import find_files
+
+ data["files"] = find_files(config.root)
+
+ for q in opts.query:
+ if q in ["files", "queries", "version"]:
+ continue
+
+ try:
+ if q.startswith("_"):
+ raise AttributeError()
+ data[q] = getattr(config, q) # type: ignore[literal-required]
+ except AttributeError:
+ sys.stderr.write(f"Error: unknown query: '{q}'\n")
+ return 1
+
+ PRINT_FUNCTIONS[opts.format](data)
+
+ return 0
+
+
+def print_json(data: OutputData) -> None:
+ print(json.dumps(data, indent=2))
+
+
+def print_plain(data: OutputData) -> None:
+ version = data.pop("version", None)
+ if version:
+ print(version)
+ files = data.pop("files", [])
+ for file_ in files:
+ print(file_)
+ queries = data.pop("queries", [])
+ for query in queries:
+ print(query)
+ if data:
+ print("\n".join(map(str, data.values())))
+
+
+def print_key_value(data: OutputData) -> None:
+ for key, value in data.items():
+ if isinstance(value, str):
+ print(f"{key} = {value}")
+ else:
+ assert isinstance(value, Iterable)
+ str_value = "\n ".join(map(str, value))
+ print(f"{key} = {str_value}")
+
+
+PRINT_FUNCTIONS = {
+ "json": print_json,
+ "plain": print_plain,
+ "key-value": print_key_value,
+}
+
+
+def _find_pyproject(parent: str) -> str:
+ for directory in discover.walk_potential_roots(os.path.abspath(parent)):
+ pyproject = os.path.join(directory, "pyproject.toml")
+ if os.path.isfile(pyproject):
+ return pyproject
+
+ return os.path.abspath(
+ "pyproject.toml"
+ ) # use default name to trigger the default errors
+
+
+def _create_archival_file(opts: CliNamespace, 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
+
+ # archival_template is guaranteed to be set by required mutually exclusive group
+ assert opts.archival_template is not None
+
+ # Load template content from package resources
+ content = files(__package__).joinpath(opts.archival_template).read_text("utf-8")
+
+ # Print appropriate message based on template
+ if opts.archival_template == "git_archival_stable.txt":
+ print("Creating stable .git_archival.txt (recommended for releases)")
+ elif opts.archival_template == "git_archival_full.txt":
+ 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
diff --git a/vcs-versioning/src/vcs_versioning/_cli/_args.py b/vcs-versioning/src/vcs_versioning/_cli/_args.py
new file mode 100644
index 00000000..086e996d
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_cli/_args.py
@@ -0,0 +1,107 @@
+from __future__ import annotations
+
+import argparse
+
+
+class CliNamespace(argparse.Namespace):
+ """Typed namespace for CLI arguments."""
+
+ # Main arguments
+ root: str | None
+ config: str | None
+ strip_dev: bool
+ no_version: bool
+ format: str
+ query: list[str] | None
+ force_write_version_files: bool
+ command: str | None
+
+ # create-archival-file subcommand arguments
+ archival_template: str | None
+ force: bool
+
+
+def get_cli_parser(prog: str) -> argparse.ArgumentParser:
+ desc = "Print project version according to SCM metadata"
+ parser = argparse.ArgumentParser(prog, description=desc)
+ # By default, help for `--help` starts with lower case, so we keep the pattern:
+ parser.add_argument(
+ "-r",
+ "--root",
+ default=None,
+ help='directory managed by the SCM, default: inferred from config file, or "."',
+ )
+ parser.add_argument(
+ "-c",
+ "--config",
+ default=None,
+ metavar="PATH",
+ help="path to 'pyproject.toml' with setuptools-scm config, "
+ "default: looked up in the current or parent directories",
+ )
+ parser.add_argument(
+ "--strip-dev",
+ action="store_true",
+ help="remove the dev/local parts of the version before printing the version",
+ )
+ parser.add_argument(
+ "-N",
+ "--no-version",
+ action="store_true",
+ help="do not include package version in the output",
+ )
+ output_formats = ["json", "plain", "key-value"]
+ parser.add_argument(
+ "-f",
+ "--format",
+ type=str.casefold,
+ default="plain",
+ help="specify output format",
+ choices=output_formats,
+ )
+ parser.add_argument(
+ "-q",
+ "--query",
+ type=str.casefold,
+ nargs="*",
+ help="display setuptools-scm settings according to query, "
+ "e.g. dist_name, do not supply an argument in order to "
+ "print a list of valid queries.",
+ )
+ parser.add_argument(
+ "--force-write-version-files",
+ action="store_true",
+ help="trigger to write the content of the version files\n"
+ "its recommended to use normal/editable installation instead)",
+ )
+ sub = parser.add_subparsers(title="extra commands", dest="command", metavar="")
+ # 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_const",
+ const="git_archival_stable.txt",
+ dest="archival_template",
+ help="create stable archival file (recommended, no branch names)",
+ )
+ archival_group.add_argument(
+ "--full",
+ action="store_const",
+ const="git_archival_full.txt",
+ dest="archival_template",
+ 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
diff --git a/vcs-versioning/src/vcs_versioning/_cli/git_archival_full.txt b/vcs-versioning/src/vcs_versioning/_cli/git_archival_full.txt
new file mode 100644
index 00000000..1ef6ba5c
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_cli/git_archival_full.txt
@@ -0,0 +1,7 @@
+# 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/vcs-versioning/src/vcs_versioning/_cli/git_archival_stable.txt b/vcs-versioning/src/vcs_versioning/_cli/git_archival_stable.txt
new file mode 100644
index 00000000..2b181ff6
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_cli/git_archival_stable.txt
@@ -0,0 +1,4 @@
+node: $Format:%H$
+node-date: $Format:%cI$
+describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
+
diff --git a/src/setuptools_scm/_compat.py b/vcs-versioning/src/vcs_versioning/_compat.py
similarity index 75%
rename from src/setuptools_scm/_compat.py
rename to vcs-versioning/src/vcs_versioning/_compat.py
index 4e9e301f..25a071a6 100644
--- a/src/setuptools_scm/_compat.py
+++ b/vcs-versioning/src/vcs_versioning/_compat.py
@@ -2,6 +2,12 @@
from __future__ import annotations
+import os
+from typing import TypeAlias
+
+# Path type for accepting both strings and PathLike objects
+PathT: TypeAlias = os.PathLike[str] | str
+
def normalize_path_for_assertion(path: str) -> str:
"""Normalize path separators for cross-platform assertions.
@@ -63,3 +69,23 @@ def assert_path_endswith(
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)
+
+
+def norm_real(path: PathT) -> str:
+ """Normalize and resolve a path (combining normcase and realpath).
+
+ This combines os.path.normcase() and os.path.realpath() to produce
+ a canonical path string that is normalized for the platform and has
+ all symbolic links resolved.
+
+ Args:
+ path: The path to normalize and resolve
+
+ Returns:
+ The normalized, resolved absolute path
+
+ Examples:
+ >>> norm_real("/path/to/../to/file.txt") # doctest: +SKIP
+ '/path/to/file.txt'
+ """
+ return os.path.normcase(os.path.realpath(path))
diff --git a/src/setuptools_scm/_config.py b/vcs-versioning/src/vcs_versioning/_config.py
similarity index 90%
rename from src/setuptools_scm/_config.py
rename to vcs-versioning/src/vcs_versioning/_config.py
index 49fac2a4..e2db2216 100644
--- a/src/setuptools_scm/_config.py
+++ b/vcs-versioning/src/vcs_versioning/_config.py
@@ -3,32 +3,25 @@
from __future__ import annotations
import dataclasses
+import logging
import os
import re
import warnings
-
from pathlib import Path
-from typing import TYPE_CHECKING
-from typing import Any
-from typing import Pattern
-from typing import Protocol
+from re import Pattern
+from typing import TYPE_CHECKING, Any, Protocol
if TYPE_CHECKING:
- from . import git
+ from ._backends 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,
-)
-from ._integration.pyproject_reading import read_pyproject as _read_pyproject
from ._overrides import read_toml_overrides
+from ._pyproject_reading import PyProjectData, get_args_for_pyproject, read_pyproject
from ._version_cls import Version as _Version
from ._version_cls import _validate_version_cls
-from ._version_cls import _VersionT
+from ._version_cls import _Version as _VersionAlias
-log = _log.log.getChild("config")
+log = logging.getLogger(__name__)
def _is_called_from_dataclasses() -> bool:
@@ -107,11 +100,11 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:
return regex
-def _get_default_git_pre_parse() -> git.GitPreParse:
+def _get_default_git_pre_parse() -> _git.GitPreParse:
"""Get the default git pre_parse enum value"""
- from . import git
+ from ._backends import _git
- return git.GitPreParse.WARN_ON_SHALLOW
+ return _git.GitPreParse.WARN_ON_SHALLOW
class ParseFunction(Protocol):
@@ -129,13 +122,15 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
and not os.path.commonpath([root, relative_to]) == root
):
warnings.warn(
- f"absolute root path '{root}' overrides relative_to '{relative_to}'"
+ f"absolute root path '{root}' overrides relative_to '{relative_to}'",
+ stacklevel=2,
)
if os.path.isdir(relative_to):
warnings.warn(
"relative_to is expected to be a file,"
f" its the directory {relative_to}\n"
- "assuming the parent directory was passed"
+ "assuming the parent directory was passed",
+ stacklevel=2,
)
log.debug("dir %s", relative_to)
root = os.path.join(relative_to, root)
@@ -149,7 +144,7 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
class GitConfiguration:
"""Git-specific configuration options"""
- pre_parse: git.GitPreParse = dataclasses.field(
+ pre_parse: _git.GitPreParse = dataclasses.field(
default_factory=lambda: _get_default_git_pre_parse()
)
describe_command: _t.CMD_TYPE | None = None
@@ -161,12 +156,12 @@ def from_data(cls, data: dict[str, Any]) -> GitConfiguration:
# Convert string pre_parse values to enum instances
if "pre_parse" in git_data and isinstance(git_data["pre_parse"], str):
- from . import git
+ from ._backends import _git
try:
- git_data["pre_parse"] = git.GitPreParse(git_data["pre_parse"])
+ git_data["pre_parse"] = _git.GitPreParse(git_data["pre_parse"])
except ValueError as e:
- valid_options = [option.value for option in git.GitPreParse]
+ 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)}"
@@ -215,7 +210,7 @@ class Configuration:
)
dist_name: str | None = None
- version_cls: type[_VersionT] = _Version
+ version_cls: type[_VersionAlias] = _Version
search_parent_directories: bool = False
parent: _t.PathT | None = None
@@ -285,8 +280,8 @@ def from_file(
"""
if pyproject_data is None:
- pyproject_data = _read_pyproject(Path(name))
- args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs)
+ pyproject_data = read_pyproject(Path(name))
+ args = get_args_for_pyproject(pyproject_data, dist_name, kwargs)
args.update(read_toml_overrides(args["dist_name"]))
relative_to = args.pop("relative_to", name)
diff --git a/src/setuptools_scm/discover.py b/vcs-versioning/src/vcs_versioning/_discover.py
similarity index 88%
rename from src/setuptools_scm/discover.py
rename to vcs-versioning/src/vcs_versioning/_discover.py
index e8208ca4..91e6a368 100644
--- a/src/setuptools_scm/discover.py
+++ b/vcs-versioning/src/vcs_versioning/_discover.py
@@ -1,22 +1,16 @@
from __future__ import annotations
+import logging
import os
-
+from collections.abc import Iterable, Iterator
+from importlib.metadata import EntryPoint
from pathlib import Path
-from typing import TYPE_CHECKING
-from typing import Iterable
-from typing import Iterator
from . import _entrypoints
-from . import _log
from . import _types as _t
from ._config import Configuration
-if TYPE_CHECKING:
- from ._entrypoints import im
-
-
-log = _log.log.getChild("discover")
+log = logging.getLogger(__name__)
def walk_potential_roots(root: _t.PathT, search_parents: bool = True) -> Iterator[Path]:
@@ -53,7 +47,7 @@ def match_entrypoint(root: _t.PathT, name: str) -> bool:
def iter_matching_entrypoints(
root: _t.PathT, entrypoint: str, config: Configuration
-) -> Iterable[im.EntryPoint]:
+) -> Iterable[EntryPoint]:
"""
Consider different entry-points in ``root`` and optionally its parents.
:param root: File path.
diff --git a/src/setuptools_scm/_integration/dump_version.py b/vcs-versioning/src/vcs_versioning/_dump_version.py
similarity index 59%
rename from src/setuptools_scm/_integration/dump_version.py
rename to vcs-versioning/src/vcs_versioning/_dump_version.py
index 06081c9f..3b5c2b2b 100644
--- a/src/setuptools_scm/_integration/dump_version.py
+++ b/vcs-versioning/src/vcs_versioning/_dump_version.py
@@ -1,21 +1,26 @@
+"""Core functionality for writing version information to files."""
+
from __future__ import annotations
+import logging
import warnings
-
from pathlib import Path
+from typing import TYPE_CHECKING
-from .. import _types as _t
-from .._log import log as parent_log
-from .._version_cls import _version_as_tuple
-from ..version import ScmVersion
+from ._version_cls import _version_as_tuple
-log = parent_log.getChild("dump_version")
+if TYPE_CHECKING:
+ from . import _types as _t
+ from ._scm_version import ScmVersion
+log = logging.getLogger(__name__)
-TEMPLATES = {
+
+DEFAULT_TEMPLATES = {
".py": """\
-# file generated by setuptools-scm
+# file generated by vcs-versioning
# don't change, don't track in version control
+from __future__ import annotations
__all__ = [
"__version__",
@@ -26,23 +31,12 @@
"commit_id",
]
-TYPE_CHECKING = False
-if TYPE_CHECKING:
- from typing import Tuple
- 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_tuple__: tuple[int | str, ...]
+version_tuple: tuple[int | str, ...]
+commit_id: str | None
+__commit_id__: str | None
__version__ = version = {version!r}
__version_tuple__ = version_tuple = {version_tuple!r}
@@ -53,38 +47,34 @@
}
-def dump_version(
- root: _t.PathT,
- version: str,
- write_to: _t.PathT,
- template: str | None = None,
- scm_version: ScmVersion | None = None,
-) -> None:
- assert isinstance(version, str)
- root = Path(root)
- write_to = Path(write_to)
- if write_to.is_absolute():
- # trigger warning on escape
- write_to.relative_to(root)
- warnings.warn(
- f"{write_to=!s} is a absolute path,"
- " please switch to using a relative version file",
- DeprecationWarning,
- )
- target = write_to
- else:
- target = Path(root).joinpath(write_to)
- write_version_to_path(
- target, template=template, version=version, scm_version=scm_version
- )
+class DummyScmVersion:
+ """Placeholder for when no ScmVersion is available."""
+
+ @property
+ def short_node(self) -> str | None:
+ return None
def _validate_template(target: Path, template: str | None) -> str:
+ """Validate and return the template to use for writing the version file.
+
+ Args:
+ target: The target file path
+ template: User-provided template or None to use default
+
+ Returns:
+ The template string to use
+
+ Raises:
+ ValueError: If no suitable template is found
+ """
if template == "":
- warnings.warn(f"{template=} looks like a error, using default instead")
+ warnings.warn(
+ f"{template=} looks like a error, using default instead", stacklevel=2
+ )
template = None
if template is None:
- template = TEMPLATES.get(target.suffix)
+ template = DEFAULT_TEMPLATES.get(target.suffix)
if template is None:
raise ValueError(
@@ -95,18 +85,20 @@ 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 = None,
) -> None:
+ """Write version information to a file using a template.
+
+ Args:
+ target: The target file path to write to
+ template: Template string or None to use default based on file extension
+ version: The version string to write
+ scm_version: Optional ScmVersion object for additional metadata
+ """
final_template = _validate_template(target, template)
log.debug("dump %s into %s", version, target)
version_tuple = _version_as_tuple(version)
@@ -126,3 +118,39 @@ def write_version_to_path(
)
target.write_text(content, encoding="utf-8")
+
+
+def dump_version(
+ root: _t.PathT,
+ version: str,
+ write_to: _t.PathT,
+ template: str | None = None,
+ scm_version: ScmVersion | None = None,
+) -> None:
+ """Write version information to a file relative to root.
+
+ Args:
+ root: The root directory (project root)
+ version: The version string to write
+ write_to: The target file path (relative to root or absolute)
+ template: Template string or None to use default
+ scm_version: Optional ScmVersion object for additional metadata
+ """
+ assert isinstance(version, str)
+ root = Path(root)
+ write_to = Path(write_to)
+ if write_to.is_absolute():
+ # trigger warning on escape
+ write_to.relative_to(root)
+ warnings.warn(
+ f"{write_to=!s} is a absolute path,"
+ " please switch to using a relative version file",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ target = write_to
+ else:
+ target = Path(root).joinpath(write_to)
+ write_version_to_path(
+ target, template=template, version=version, scm_version=scm_version
+ )
diff --git a/src/setuptools_scm/_entrypoints.py b/vcs-versioning/src/vcs_versioning/_entrypoints.py
similarity index 66%
rename from src/setuptools_scm/_entrypoints.py
rename to vcs-versioning/src/vcs_versioning/_entrypoints.py
index 74a18a7d..7a6c49e0 100644
--- a/src/setuptools_scm/_entrypoints.py
+++ b/vcs-versioning/src/vcs_versioning/_entrypoints.py
@@ -1,15 +1,10 @@
from __future__ import annotations
-import sys
-
-from typing import TYPE_CHECKING
-from typing import Any
-from typing import Callable
-from typing import Iterator
-from typing import cast
-
-from . import _log
-from . import version
+import logging
+from collections.abc import Callable, Iterator
+from importlib import metadata as im
+from importlib.metadata import entry_points
+from typing import TYPE_CHECKING, Any, cast
__all__ = [
"entry_points",
@@ -17,43 +12,21 @@
]
if TYPE_CHECKING:
from . import _types as _t
- from ._config import Configuration
- from ._config import ParseFunction
-
-from importlib import metadata as im
-
-log = _log.log.getChild("entrypoints")
-
-
-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
-
- 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:
+ from ._config import Configuration, ParseFunction
+ from ._scm_version import ScmVersion
- 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)
+log = logging.getLogger(__name__)
def version_from_entrypoint(
config: Configuration, *, entrypoint: str, root: _t.PathT
-) -> version.ScmVersion | None:
- from .discover import iter_matching_entrypoints
+) -> ScmVersion | None:
+ from ._discover import iter_matching_entrypoints
log.debug("version_from_ep %s in %s", entrypoint, root)
for ep in iter_matching_entrypoints(root, entrypoint, config):
fn: ParseFunction = ep.load()
- maybe_version: version.ScmVersion | None = fn(root, config=config)
+ maybe_version: ScmVersion | None = fn(root, config=config)
log.debug("%s found %r", ep, maybe_version)
if maybe_version is not None:
return maybe_version
@@ -82,7 +55,7 @@ def _iter_version_schemes(
entrypoint: str,
scheme_value: _t.VERSION_SCHEMES,
_memo: set[object] | None = None,
-) -> Iterator[Callable[[version.ScmVersion], str]]:
+) -> Iterator[Callable[[ScmVersion], str]]:
if _memo is None:
_memo = set()
if isinstance(scheme_value, str):
@@ -92,7 +65,7 @@ def _iter_version_schemes(
or _get_from_object_reference_str(scheme_value, entrypoint),
)
- if isinstance(scheme_value, (list, tuple)):
+ if isinstance(scheme_value, list | tuple):
for variant in scheme_value:
if variant not in _memo:
_memo.add(variant)
@@ -102,7 +75,7 @@ def _iter_version_schemes(
def _call_version_scheme(
- version: version.ScmVersion,
+ version: ScmVersion,
entrypoint: str,
given_value: _t.VERSION_SCHEMES,
default: str | None = None,
diff --git a/src/setuptools_scm/fallbacks.py b/vcs-versioning/src/vcs_versioning/_fallbacks.py
similarity index 88%
rename from src/setuptools_scm/fallbacks.py
rename to vcs-versioning/src/vcs_versioning/_fallbacks.py
index 45a75351..b2b89450 100644
--- a/src/setuptools_scm/fallbacks.py
+++ b/vcs-versioning/src/vcs_versioning/_fallbacks.py
@@ -2,17 +2,14 @@
import logging
import os
-
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import _types as _t
-from . import Configuration
-from .integration import data_from_mime
-from .version import ScmVersion
-from .version import meta
-from .version import tag_to_version
+from ._config import Configuration
+from ._integration import data_from_mime
+from ._scm_version import ScmVersion, meta, tag_to_version
log = logging.getLogger(__name__)
diff --git a/src/setuptools_scm/_file_finders/__init__.py b/vcs-versioning/src/vcs_versioning/_file_finders/__init__.py
similarity index 76%
rename from src/setuptools_scm/_file_finders/__init__.py
rename to vcs-versioning/src/vcs_versioning/_file_finders/__init__.py
index e19afc81..6d653c11 100644
--- a/src/setuptools_scm/_file_finders/__init__.py
+++ b/vcs-versioning/src/vcs_versioning/_file_finders/__init__.py
@@ -1,25 +1,15 @@
from __future__ import annotations
+import logging
import os
+from collections.abc import Callable
+from typing import TypeGuard
-from typing import TYPE_CHECKING
-from typing import Callable
-
-from .. import _log
from .. import _types as _t
+from .._compat import norm_real
from .._entrypoints import entry_points
-from .pathtools import norm_real
-
-if TYPE_CHECKING:
- import sys
-
- if sys.version_info >= (3, 10):
- from typing import TypeGuard
- else:
- from typing_extensions import TypeGuard
-
-log = _log.log.getChild("file_finder")
+log = logging.getLogger("vcs_versioning.file_finder")
def scm_find_files(
@@ -28,7 +18,7 @@ def scm_find_files(
scm_dirs: set[str],
force_all_files: bool = False,
) -> list[str]:
- """ setuptools compatible file finder that follows symlinks
+ """Core file discovery logic that follows symlinks
- path: the root directory from which to search
- scm_files: set of scm controlled files and symlinks
@@ -39,9 +29,6 @@ def scm_find_files(
scm_files and scm_dirs must be absolute with symlinks resolved (realpath),
with normalized case (normcase)
-
- Spec here: https://setuptools.pypa.io/en/latest/userguide/extension.html#\
- adding-support-for-revision-control-systems
"""
realpath = norm_real(path)
seen: set[str] = set()
@@ -86,21 +73,33 @@ def _link_not_in_scm(n: str, realdirpath: str = realdirpath) -> bool:
def is_toplevel_acceptable(toplevel: str | None) -> TypeGuard[str]:
- """ """
+ """Check if a VCS toplevel directory is acceptable (not in ignore list)"""
+ import os
+
if toplevel is None:
return False
- ignored: list[str] = os.environ.get("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", "").split(
- os.pathsep
+ # Use the env_reader from the active GlobalOverrides context
+ # This ensures we respect the current environment configuration
+ from ..overrides import get_active_overrides
+
+ overrides = get_active_overrides()
+ ignored_raw = overrides.env_reader.read(
+ "IGNORE_VCS_ROOTS", split=os.pathsep, default=[]
)
- ignored = [os.path.normcase(p) for p in ignored]
+ ignored = [os.path.normcase(p) for p in ignored_raw]
- log.debug("toplevel: %r\n ignored %s", toplevel, ignored)
+ log.debug(
+ "toplevel: %r\n ignored %s",
+ toplevel,
+ ignored,
+ )
return toplevel not in ignored
def find_files(path: _t.PathT = "") -> list[str]:
+ """Discover files using registered file finder entry points"""
eps = [
*entry_points(group="setuptools_scm.files_command"),
*entry_points(group="setuptools_scm.files_command_fallback"),
@@ -111,3 +110,6 @@ def find_files(path: _t.PathT = "") -> list[str]:
if res:
return res
return []
+
+
+__all__ = ["scm_find_files", "is_toplevel_acceptable", "find_files"]
diff --git a/src/setuptools_scm/_file_finders/git.py b/vcs-versioning/src/vcs_versioning/_file_finders/_git.py
similarity index 92%
rename from src/setuptools_scm/_file_finders/git.py
rename to vcs-versioning/src/vcs_versioning/_file_finders/_git.py
index 4379c21a..151e6d90 100644
--- a/src/setuptools_scm/_file_finders/git.py
+++ b/vcs-versioning/src/vcs_versioning/_file_finders/_git.py
@@ -4,15 +4,13 @@
import os
import subprocess
import tarfile
-
from typing import IO
from .. import _types as _t
+from .._compat import norm_real, strip_path_suffix
+from .._integration import data_from_mime
from .._run_cmd import run as _run
-from ..integration import data_from_mime
-from . import is_toplevel_acceptable
-from . import scm_find_files
-from .pathtools import norm_real
+from . import is_toplevel_acceptable, scm_find_files
log = logging.getLogger(__name__)
@@ -39,8 +37,6 @@ 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
- 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)
@@ -97,6 +93,7 @@ def _git_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]:
def git_find_files(path: _t.PathT = "") -> list[str]:
+ """Find files tracked in a Git repository"""
toplevel = _git_toplevel(os.fspath(path))
if not is_toplevel_acceptable(toplevel):
return []
@@ -108,6 +105,7 @@ def git_find_files(path: _t.PathT = "") -> list[str]:
def git_archive_find_files(path: _t.PathT = "") -> list[str]:
+ """Find files in a Git archive (all files, since archive already filtered)"""
# This function assumes that ``path`` is obtained from a git archive
# and therefore all the files that should be ignored were already removed.
archival = os.path.join(path, ".git_archival.txt")
@@ -122,3 +120,6 @@ def git_archive_find_files(path: _t.PathT = "") -> list[str]:
log.warning("git archive detected - fallback to listing all files")
return scm_find_files(path, set(), set(), force_all_files=True)
+
+
+__all__ = ["git_find_files", "git_archive_find_files"]
diff --git a/src/setuptools_scm/_file_finders/hg.py b/vcs-versioning/src/vcs_versioning/_file_finders/_hg.py
similarity index 84%
rename from src/setuptools_scm/_file_finders/hg.py
rename to vcs-versioning/src/vcs_versioning/_file_finders/_hg.py
index 182429c3..43e50261 100644
--- a/src/setuptools_scm/_file_finders/hg.py
+++ b/vcs-versioning/src/vcs_versioning/_file_finders/_hg.py
@@ -5,11 +5,10 @@
import subprocess
from .. import _types as _t
-from .._file_finders import is_toplevel_acceptable
-from .._file_finders import scm_find_files
-from ..hg import run_hg
-from ..integration import data_from_mime
-from .pathtools import norm_real
+from .._backends._hg import run_hg
+from .._compat import norm_real
+from .._integration import data_from_mime
+from . import is_toplevel_acceptable, scm_find_files
log = logging.getLogger(__name__)
@@ -47,6 +46,7 @@ def _hg_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]:
def hg_find_files(path: str = "") -> list[str]:
+ """Find files tracked in a Mercurial repository"""
toplevel = _hg_toplevel(path)
if not is_toplevel_acceptable(toplevel):
return []
@@ -56,6 +56,7 @@ def hg_find_files(path: str = "") -> list[str]:
def hg_archive_find_files(path: _t.PathT = "") -> list[str]:
+ """Find files in a Mercurial archive (all files, since archive already filtered)"""
# This function assumes that ``path`` is obtained from a mercurial archive
# and therefore all the files that should be ignored were already removed.
archival = os.path.join(path, ".hg_archival.txt")
@@ -70,3 +71,6 @@ def hg_archive_find_files(path: _t.PathT = "") -> list[str]:
log.warning("hg archive detected - fallback to listing all files")
return scm_find_files(path, set(), set(), force_all_files=True)
+
+
+__all__ = ["hg_find_files", "hg_archive_find_files"]
diff --git a/src/setuptools_scm/_get_version_impl.py b/vcs-versioning/src/vcs_versioning/_get_version_impl.py
similarity index 84%
rename from src/setuptools_scm/_get_version_impl.py
rename to vcs-versioning/src/vcs_versioning/_get_version_impl.py
index 31bc9c39..44ef3582 100644
--- a/src/setuptools_scm/_get_version_impl.py
+++ b/vcs-versioning/src/vcs_versioning/_get_version_impl.py
@@ -4,27 +4,23 @@
import logging
import re
import warnings
-
from pathlib import Path
-from typing import Any
-from typing import NoReturn
-from typing import Pattern
+from re import Pattern
+from typing import Any, NoReturn
-from . import _config
-from . import _entrypoints
-from . import _run_cmd
+from . import _config, _entrypoints, _run_cmd
from . import _types as _t
from ._config import Configuration
from ._overrides import _read_pretended_version_for
+from ._scm_version import ScmVersion
from ._version_cls import _validate_version_cls
-from .version import ScmVersion
-from .version import format_version as _format_version
+from ._version_schemes import format_version as _format_version
EMPTY_TAG_REGEX_DEPRECATION = DeprecationWarning(
"empty regex for tag regex is invalid, using default"
)
-_log = logging.getLogger(__name__)
+log = logging.getLogger(__name__)
def parse_scm_version(config: Configuration) -> ScmVersion | None:
@@ -44,7 +40,7 @@ def parse_scm_version(config: Configuration) -> ScmVersion | None:
root=config.absolute_root,
)
except _run_cmd.CommandNotFoundError as e:
- _log.exception("command %s not found while parsing the scm, using fallbacks", e)
+ log.exception("command %s not found while parsing the scm, using fallbacks", e)
return None
@@ -74,7 +70,7 @@ def write_version_files(
config: Configuration, version: str, scm_version: ScmVersion
) -> None:
if config.write_to is not None:
- from ._integration.dump_version import dump_version
+ from ._dump_version import dump_version
dump_version(
root=config.root,
@@ -84,7 +80,7 @@ def write_version_files(
template=config.write_to_template,
)
if config.version_file:
- from ._integration.dump_version import write_version_to_path
+ from ._dump_version import write_version_to_path
version_file = Path(config.version_file)
assert not version_file.is_absolute(), f"{version_file=}"
@@ -112,6 +108,7 @@ def _get_version(
"force_write_version_files ought to be set,"
" presuming the legacy True value",
DeprecationWarning,
+ stacklevel=2,
)
if force_write_version_files:
@@ -130,7 +127,7 @@ def _find_scm_in_parents(config: Configuration) -> Path | None:
searching_config = dataclasses.replace(config, search_parent_directories=True)
- from .discover import iter_matching_entrypoints
+ from ._discover import iter_matching_entrypoints
for _ep in iter_matching_entrypoints(
config.absolute_root, "setuptools_scm.parse_scm", searching_config
@@ -154,18 +151,32 @@ def _version_missing(config: Configuration) -> NoReturn:
if scm_parent is not None:
# Found an SCM repository in a parent directory
+ # Get tool-specific names for error messages
+ from .overrides import get_active_overrides
+
+ overrides = get_active_overrides()
+ tool = overrides.tool
+
+ # Generate appropriate examples based on tool
+ if tool == "SETUPTOOLS_SCM":
+ api_example = "setuptools_scm.get_version(relative_to=__file__)"
+ tool_section = "[tool.setuptools_scm]"
+ else:
+ api_example = "vcs_versioning.get_version(relative_to=__file__)"
+ tool_section = "[tool.vcs-versioning]"
+
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"1. Use the 'relative_to' parameter to specify the file as reference:\n"
+ f" {api_example}\n\n"
f"2. Enable parent directory search in your configuration:\n"
- f" [tool.setuptools_scm]\n"
+ f" {tool_section}\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" {tool_section}\n"
f' root = "{scm_parent}"\n\n'
"For more information, see: https://setuptools-scm.readthedocs.io/en/latest/config/"
)
@@ -240,9 +251,14 @@ def get_version(
def parse_tag_regex(tag_regex: str | Pattern[str]) -> Pattern[str]:
+ """Pre-validate and convert tag_regex to Pattern before Configuration.
+
+ This ensures get_version() emits the deprecation warning for empty strings
+ before Configuration.__post_init__ runs.
+ """
if isinstance(tag_regex, str):
if tag_regex == "":
- warnings.warn(EMPTY_TAG_REGEX_DEPRECATION)
+ warnings.warn(EMPTY_TAG_REGEX_DEPRECATION, stacklevel=3)
return _config.DEFAULT_TAG_REGEX
else:
return re.compile(tag_regex)
diff --git a/src/setuptools_scm/integration.py b/vcs-versioning/src/vcs_versioning/_integration.py
similarity index 99%
rename from src/setuptools_scm/integration.py
rename to vcs-versioning/src/vcs_versioning/_integration.py
index b15d74a6..238deab2 100644
--- a/src/setuptools_scm/integration.py
+++ b/vcs-versioning/src/vcs_versioning/_integration.py
@@ -2,7 +2,6 @@
import logging
import textwrap
-
from pathlib import Path
from . import _types as _t
diff --git a/vcs-versioning/src/vcs_versioning/_integrator_helpers.py b/vcs-versioning/src/vcs_versioning/_integrator_helpers.py
new file mode 100644
index 00000000..04a1ed31
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_integrator_helpers.py
@@ -0,0 +1,113 @@
+"""Internal helpers for integrators to build configurations.
+
+This module provides substantial orchestration functions for building
+Configuration instances with proper override priority handling.
+
+Public API is exposed through __init__.py with restrictions.
+"""
+
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+ from ._config import Configuration
+ from ._pyproject_reading import PyProjectData
+
+log = logging.getLogger(__name__)
+
+
+def build_configuration_from_pyproject_internal(
+ pyproject_data: PyProjectData,
+ *,
+ dist_name: str | None = None,
+ **integrator_overrides: Any,
+) -> Configuration:
+ """Build Configuration with complete workflow orchestration.
+
+ This is a substantial helper that orchestrates the complete configuration
+ building workflow with proper priority handling.
+
+ Orchestration steps:
+ 1. Extract base config from pyproject_data.section
+ 2. Determine dist_name (argument > pyproject.project_name)
+ 3. Merge integrator overrides (override config file)
+ 4. Read and apply env TOML overrides (highest priority)
+ 5. Build Configuration with proper validation
+
+ Priority order (highest to lowest):
+ 1. Environment TOML overrides (TOOL_OVERRIDES_FOR_DIST, TOOL_OVERRIDES)
+ 2. Integrator **integrator_overrides arguments
+ 3. pyproject_data.section configuration
+ 4. Configuration defaults
+
+ Args:
+ pyproject_data: Parsed pyproject data from PyProjectData.from_file() or manual composition
+ dist_name: Distribution name for env var lookups (overrides pyproject_data.project_name)
+ **integrator_overrides: Integrator-provided config overrides
+ (override config file, but overridden by env)
+
+ Returns:
+ Configured Configuration instance ready for version inference
+
+ Example:
+ >>> from vcs_versioning import PyProjectData
+ >>> from vcs_versioning._integrator_helpers import build_configuration_from_pyproject_internal
+ >>>
+ >>> pyproject = PyProjectData.from_file(
+ ... "pyproject.toml",
+ ... _tool_names=["setuptools_scm", "vcs-versioning"]
+ ... )
+ >>> config = build_configuration_from_pyproject_internal(
+ ... pyproject_data=pyproject,
+ ... dist_name="my-package",
+ ... local_scheme="no-local-version", # Integrator override
+ ... )
+ """
+ # Import here to avoid circular dependencies
+ from ._config import Configuration
+ from ._overrides import read_toml_overrides
+ from ._pyproject_reading import get_args_for_pyproject
+
+ # Step 1: Get base config from pyproject section
+ # This also handles dist_name resolution
+ log.debug(
+ "Building configuration from pyproject at %s (tool: %s)",
+ pyproject_data.path,
+ pyproject_data.tool_name,
+ )
+
+ config_data = get_args_for_pyproject(
+ pyproject_data,
+ dist_name=dist_name,
+ kwargs={},
+ )
+
+ # Step 2: dist_name is now determined (from arg, config, or project.name)
+ actual_dist_name = config_data.get("dist_name")
+ log.debug("Resolved dist_name: %s", actual_dist_name)
+
+ # Step 3: Merge integrator overrides (middle priority - override config file)
+ if integrator_overrides:
+ log.debug(
+ "Applying integrator overrides: %s", list(integrator_overrides.keys())
+ )
+ config_data.update(integrator_overrides)
+
+ # Step 4: Apply environment TOML overrides (highest priority)
+ env_overrides = read_toml_overrides(actual_dist_name)
+ if env_overrides:
+ log.debug("Applying environment TOML overrides: %s", list(env_overrides.keys()))
+ config_data.update(env_overrides)
+
+ # Step 5: Build Configuration with validation
+ relative_to = pyproject_data.path
+ log.debug("Building Configuration with relative_to=%s", relative_to)
+
+ return Configuration.from_data(relative_to=relative_to, data=config_data)
+
+
+__all__ = [
+ "build_configuration_from_pyproject_internal",
+]
diff --git a/vcs-versioning/src/vcs_versioning/_log.py b/vcs-versioning/src/vcs_versioning/_log.py
new file mode 100644
index 00000000..989a50eb
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_log.py
@@ -0,0 +1,137 @@
+"""
+logging helpers, supports vendoring
+"""
+
+from __future__ import annotations
+
+import contextlib
+import logging
+from collections.abc import Iterator
+
+
+def make_default_handler() -> logging.Handler:
+ try:
+ from rich.console import Console
+
+ console = Console(stderr=True)
+ from rich.logging import RichHandler
+
+ return RichHandler(console=console)
+ except ImportError:
+ last_resort = logging.lastResort
+ assert last_resort is not None
+ return last_resort
+
+
+def _get_all_scm_loggers(
+ additional_loggers: list[logging.Logger] | None = None,
+) -> list[logging.Logger]:
+ """Get all SCM-related loggers that need configuration.
+
+ Always configures vcs_versioning logger.
+ If additional_loggers is provided, also configures those loggers.
+ If not provided, tries to get them from active GlobalOverrides context.
+ """
+ loggers = [logging.getLogger("vcs_versioning")]
+
+ if additional_loggers is not None:
+ loggers.extend(additional_loggers)
+ else:
+ # Try to get additional loggers from active overrides context
+ try:
+ from .overrides import _active_overrides
+
+ overrides = _active_overrides.get()
+ if overrides is not None:
+ loggers.extend(overrides.additional_loggers)
+ except ImportError:
+ # During early initialization, overrides module might not be available yet
+ pass
+
+ return loggers
+
+
+_default_handler: logging.Handler | None = None
+
+
+def _configure_loggers(
+ log_level: int, additional_loggers: list[logging.Logger] | None = None
+) -> None:
+ """Internal function to configure SCM-related loggers.
+
+ This is called automatically by GlobalOverrides.__enter__().
+ Do not call directly - use GlobalOverrides context manager instead.
+
+ Args:
+ log_level: Logging level constant from logging module
+ additional_loggers: Optional list of additional logger instances to configure
+ """
+ global _default_handler
+
+ if _default_handler is None:
+ _default_handler = make_default_handler()
+
+ for logger in _get_all_scm_loggers(additional_loggers):
+ if not logger.handlers:
+ logger.addHandler(_default_handler)
+ logger.setLevel(log_level)
+ logger.propagate = False
+
+
+# The vcs_versioning root logger
+# Note: This is created on import, but configured lazily via configure_logging()
+log = logging.getLogger("vcs_versioning")
+
+
+@contextlib.contextmanager
+def defer_to_pytest() -> Iterator[None]:
+ """Configure all SCM loggers to propagate to pytest's log capture."""
+ loggers = _get_all_scm_loggers()
+ old_states = []
+
+ for logger in loggers:
+ old_states.append((logger, logger.propagate, logger.level, logger.handlers[:]))
+ logger.propagate = True
+ logger.setLevel(logging.NOTSET)
+ # Remove all handlers
+ for handler in logger.handlers[:]:
+ logger.removeHandler(handler)
+
+ try:
+ yield
+ finally:
+ for logger, old_propagate, old_level, old_handlers in old_states:
+ for handler in old_handlers:
+ logger.addHandler(handler)
+ logger.propagate = old_propagate
+ logger.setLevel(old_level)
+
+
+@contextlib.contextmanager
+def enable_debug(handler: logging.Handler | None = None) -> Iterator[None]:
+ """Enable debug logging for all SCM loggers."""
+ global _default_handler
+ if handler is None:
+ if _default_handler is None:
+ _default_handler = make_default_handler()
+ handler = _default_handler
+
+ loggers = _get_all_scm_loggers()
+ old_states = []
+
+ for logger in loggers:
+ old_states.append((logger, logger.level))
+ logger.addHandler(handler)
+ logger.setLevel(logging.DEBUG)
+
+ old_handler_level = handler.level
+ handler.setLevel(logging.DEBUG)
+
+ try:
+ yield
+ finally:
+ handler.setLevel(old_handler_level)
+ for logger, old_level in old_states:
+ logger.setLevel(old_level)
+ if handler is not _default_handler:
+ logger.removeHandler(handler)
diff --git a/src/setuptools_scm/_modify_version.py b/vcs-versioning/src/vcs_versioning/_modify_version.py
similarity index 100%
rename from src/setuptools_scm/_modify_version.py
rename to vcs-versioning/src/vcs_versioning/_modify_version.py
diff --git a/src/setuptools_scm/_node_utils.py b/vcs-versioning/src/vcs_versioning/_node_utils.py
similarity index 100%
rename from src/setuptools_scm/_node_utils.py
rename to vcs-versioning/src/vcs_versioning/_node_utils.py
diff --git a/vcs-versioning/src/vcs_versioning/_overrides.py b/vcs-versioning/src/vcs_versioning/_overrides.py
new file mode 100644
index 00000000..be9909c8
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_overrides.py
@@ -0,0 +1,285 @@
+"""Internal implementation details for the overrides module.
+
+This module contains private helpers and functions used internally
+by vcs_versioning. Public API is exposed via the overrides module.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+import logging
+import os
+from collections.abc import Mapping
+from datetime import date, datetime
+from difflib import get_close_matches
+from re import Pattern
+from typing import Any, TypedDict, get_type_hints
+
+from packaging.utils import canonicalize_name
+
+from . import _config
+from . import _types as _t
+from ._scm_version import ScmVersion, meta # noqa: F401 - for type checking
+from ._version_cls import Version as _Version
+
+log = logging.getLogger(__name__)
+
+
+# TypedDict schemas for TOML data validation and type hints
+
+
+class PretendMetadataDict(TypedDict, total=False):
+ """Schema for ScmVersion metadata fields that can be overridden via environment.
+
+ All fields are optional since partial overrides are allowed.
+ """
+
+ tag: str | _Version
+ distance: int
+ node: str | None
+ dirty: bool
+ preformatted: bool
+ branch: str | None
+ node_date: date | None
+ time: datetime
+
+
+class ConfigOverridesDict(TypedDict, total=False):
+ """Schema for Configuration fields that can be overridden via environment.
+
+ All fields are optional since partial overrides are allowed.
+ """
+
+ # Configuration fields
+ root: _t.PathT
+ version_scheme: _t.VERSION_SCHEME
+ local_scheme: _t.VERSION_SCHEME
+ tag_regex: str | Pattern[str]
+ parentdir_prefix_version: str | None
+ fallback_version: str | None
+ fallback_root: _t.PathT
+ write_to: _t.PathT | None
+ write_to_template: str | None
+ version_file: _t.PathT | None
+ version_file_template: str | None
+ parse: Any # ParseFunction - avoid circular import
+ git_describe_command: _t.CMD_TYPE | None # deprecated but still supported
+ dist_name: str | None
+ version_cls: Any # type[_Version] - avoid circular import
+ normalize: bool # Used in from_data
+ search_parent_directories: bool
+ parent: _t.PathT | None
+ scm: dict[str, Any] # Nested SCM configuration
+
+
+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_list = get_close_matches(
+ expected_suffix, candidates, n=3, cutoff=threshold
+ )
+
+ return [
+ f"{prefix}{match}" for match in close_matches_list if match != expected_suffix
+ ]
+
+
+def _read_pretended_metadata_for(
+ config: _config.Configuration,
+) -> PretendMetadataDict | 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}
+ """
+ from .overrides import EnvReader
+
+ log.debug("dist name: %s", config.dist_name)
+
+ reader = EnvReader(
+ tools_names=("SETUPTOOLS_SCM", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name=config.dist_name,
+ )
+
+ try:
+ # Use schema validation during TOML parsing
+ metadata_overrides = reader.read_toml(
+ "PRETEND_METADATA", schema=PretendMetadataDict
+ )
+ return metadata_overrides or None
+ except Exception as e:
+ log.error("Failed to parse pretend metadata: %s", e)
+ return None
+
+
+def _apply_metadata_overrides(
+ scm_version: ScmVersion | None,
+ config: _config.Configuration,
+) -> 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)
+
+ # Get type hints from PretendMetadataDict for validation
+ field_types = get_type_hints(PretendMetadataDict)
+
+ # Apply each override individually using dataclasses.replace
+ result = scm_version
+
+ for field, value in metadata_overrides.items():
+ # Validate field types using the TypedDict annotations
+ if field in field_types:
+ expected_type = field_types[field]
+ # Handle Optional/Union types (e.g., str | None)
+ if hasattr(expected_type, "__args__"):
+ # Union type - check if value is instance of any of the types
+ valid = any(
+ isinstance(value, t) if t is not type(None) else value is None
+ for t in expected_type.__args__
+ )
+ if not valid:
+ type_names = " | ".join(
+ t.__name__ if t is not type(None) else "None"
+ for t in expected_type.__args__
+ )
+ raise TypeError(
+ f"Field '{field}' must be {type_names}, "
+ f"got {type(value).__name__}: {value!r}"
+ )
+ else:
+ # Simple type
+ if not isinstance(value, expected_type):
+ raise TypeError(
+ f"Field '{field}' must be {expected_type.__name__}, "
+ f"got {type(value).__name__}: {value!r}"
+ )
+
+ result = dataclasses.replace(result, **{field: value}) # type: ignore[arg-type]
+
+ # 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(
+ config: _config.Configuration,
+) -> ScmVersion | None:
+ """read a a overridden version from the environment
+
+ tries ``SETUPTOOLS_SCM_PRETEND_VERSION``
+ and ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_$UPPERCASE_DIST_NAME``
+ """
+ from .overrides import EnvReader
+
+ log.debug("dist name: %s", config.dist_name)
+
+ reader = EnvReader(
+ tools_names=("SETUPTOOLS_SCM", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name=config.dist_name,
+ )
+ pretended = reader.read("PRETEND_VERSION")
+
+ if pretended:
+ return meta(tag=pretended, preformatted=True, config=config)
+ else:
+ return None
+
+
+def read_toml_overrides(dist_name: str | None) -> ConfigOverridesDict:
+ """Read TOML overrides from environment.
+
+ Validates that only known Configuration fields are provided.
+ """
+ from .overrides import EnvReader
+
+ reader = EnvReader(
+ tools_names=("SETUPTOOLS_SCM", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name=dist_name,
+ )
+ return reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/vcs-versioning/src/vcs_versioning/_pyproject_reading.py
similarity index 61%
rename from src/setuptools_scm/_integration/pyproject_reading.py
rename to vcs-versioning/src/vcs_versioning/_pyproject_reading.py
index 75d86f62..3fbabd20 100644
--- a/src/setuptools_scm/_integration/pyproject_reading.py
+++ b/vcs-versioning/src/vcs_versioning/_pyproject_reading.py
@@ -1,29 +1,36 @@
+"""Core pyproject.toml reading functionality"""
+
from __future__ import annotations
+import logging
+import os
+import sys
import warnings
-
+from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
-from typing import Sequence
+from typing import TypeAlias
+
+if sys.version_info >= (3, 11):
+ from typing import Self
+else:
+ from typing_extensions import Self
-from .. import _log
-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
+from ._requirement_cls import extract_package_name
+from ._toml import TOML_RESULT, InvalidTomlError, read_toml_content
-log = _log.log.getChild("pyproject_reading")
+log = logging.getLogger(__name__)
_ROOT = "root"
DEFAULT_PYPROJECT_PATH = Path("pyproject.toml")
-DEFAULT_TOOL_NAME = "setuptools_scm"
@dataclass
class PyProjectData:
+ """Core pyproject.toml data structure"""
+
path: Path
tool_name: str
project: TOML_RESULT
@@ -32,11 +39,13 @@ class PyProjectData:
section_present: bool
project_present: bool
build_requires: list[str]
+ definition: TOML_RESULT
@classmethod
def for_testing(
cls,
*,
+ tool_name: str,
is_required: bool = False,
section_present: bool = False,
project_present: bool = False,
@@ -44,7 +53,7 @@ def for_testing(
has_dynamic_version: bool = True,
build_requires: list[str] | None = None,
local_scheme: str | None = None,
- ) -> PyProjectData:
+ ) -> Self:
"""Create a PyProjectData instance for testing purposes."""
project: TOML_RESULT
if project_name is not None:
@@ -66,19 +75,18 @@ def for_testing(
section = {}
return cls(
path=DEFAULT_PYPROJECT_PATH,
- tool_name=DEFAULT_TOOL_NAME,
+ tool_name=tool_name,
project=project,
section=section,
is_required=is_required,
section_present=section_present,
project_present=project_present,
build_requires=build_requires,
+ definition={},
)
@classmethod
- def empty(
- cls, path: Path = DEFAULT_PYPROJECT_PATH, tool_name: str = DEFAULT_TOOL_NAME
- ) -> PyProjectData:
+ def empty(cls, tool_name: str, path: Path = DEFAULT_PYPROJECT_PATH) -> Self:
return cls(
path=path,
tool_name=tool_name,
@@ -88,8 +96,52 @@ def empty(
section_present=False,
project_present=False,
build_requires=[],
+ definition={},
)
+ @classmethod
+ def from_file(
+ cls,
+ path: str | os.PathLike[str] = "pyproject.toml",
+ *,
+ _tool_names: list[str] | None = None,
+ ) -> Self:
+ """Load PyProjectData from pyproject.toml.
+
+ Public API: reads tool.vcs-versioning section.
+ Internal use: pass _tool_names for multi-tool support (e.g., setuptools_scm transition).
+
+ Args:
+ path: Path to pyproject.toml file
+ _tool_names: Internal parameter for multi-tool support.
+ If None, uses ["vcs-versioning"] (public API behavior).
+
+ Returns:
+ PyProjectData instance loaded from file
+
+ Raises:
+ FileNotFoundError: If pyproject.toml not found
+ InvalidTomlError: If pyproject.toml has invalid TOML syntax
+
+ Example:
+ >>> # Public API usage
+ >>> pyproject = PyProjectData.from_file("pyproject.toml")
+ >>>
+ >>> # Internal usage (setuptools_scm transition)
+ >>> pyproject = PyProjectData.from_file(
+ ... "pyproject.toml",
+ ... _tool_names=["setuptools_scm", "vcs-versioning"]
+ ... )
+ """
+ if _tool_names is None:
+ # Public API path - only vcs-versioning
+ _tool_names = ["vcs-versioning"]
+
+ result = read_pyproject(Path(path), tool_names=_tool_names)
+ # Type narrowing for mypy: read_pyproject returns PyProjectData,
+ # but subclasses (like setuptools_scm's extended version) need Self
+ return result # type: ignore[return-value]
+
@property
def project_name(self) -> str | None:
return self.project.get("name")
@@ -103,37 +155,17 @@ def project_version(self) -> str | 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
+# Testing injection type for configuration reading
+GivenPyProjectResult: TypeAlias = (
+ PyProjectData | InvalidTomlError | FileNotFoundError | None
+)
def has_build_package(
requires: Sequence[str], canonical_build_package_name: str
) -> bool:
+ """Check if a package is in build requirements."""
for requirement in requires:
package_name = extract_package_name(requirement)
if package_name == canonical_build_package_name:
@@ -141,40 +173,12 @@ def has_build_package(
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 = DEFAULT_PYPROJECT_PATH,
- tool_name: str = DEFAULT_TOOL_NAME,
canonical_build_package_name: str = "setuptools-scm",
- _given_result: _t.GivenPyProjectResult = None,
+ _given_result: GivenPyProjectResult = None,
_given_definition: TOML_RESULT | None = None,
+ tool_names: list[str] | None = None,
) -> PyProjectData:
"""Read and parse pyproject configuration.
@@ -182,7 +186,6 @@ def read_pyproject(
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
@@ -191,12 +194,14 @@ def read_pyproject(
: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.
+ :param tool_names: List of tool section names to try in order.
+ If None, defaults to ["vcs-versioning", "setuptools_scm"]
"""
if _given_result is not None:
if isinstance(_given_result, PyProjectData):
return _given_result
- if isinstance(_given_result, (InvalidTomlError, FileNotFoundError)):
+ if isinstance(_given_result, InvalidTomlError | FileNotFoundError):
raise _given_result
if _given_definition is not None:
@@ -208,43 +213,46 @@ def read_pyproject(
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
+
+ # Determine which tool names to try
+ if tool_names is None:
+ # Default: try vcs-versioning first, then setuptools_scm for backward compat
+ tool_names = ["vcs-versioning", "setuptools_scm"]
+
+ # Try each tool name in order
+ section = {}
+ section_present = False
+ actual_tool_name = tool_names[0] if tool_names else "vcs-versioning"
+
+ for name in tool_names:
+ if name in tool_section:
+ section = tool_section[name]
+ section_present = True
+ actual_tool_name = name
+ break
if not section_present:
log.warning(
- "toml section missing %r does not contain a tool.%s section",
+ "toml section missing %r does not contain any of the tool sections: %s",
path,
- tool_name,
+ tool_names,
)
project = defn.get("project", {})
project_present = "project" in defn
+
pyproject_data = PyProjectData(
path,
- tool_name,
+ actual_tool_name,
project,
section,
is_required,
section_present,
project_present,
requires,
+ defn,
)
- 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
@@ -261,7 +269,8 @@ def get_args_for_pyproject(
warnings.warn(
f"{pyproject.path}: at [tool.{pyproject.tool_name}]\n"
f"ignoring value relative_to={relative!r}"
- " as its always relative to the config file"
+ " as its always relative to the config file",
+ stacklevel=2,
)
if "dist_name" in section:
if dist_name is None:
@@ -279,7 +288,8 @@ def get_args_for_pyproject(
if section[_ROOT] != kwargs[_ROOT]:
warnings.warn(
f"root {section[_ROOT]} is overridden"
- f" by the cli arg {kwargs[_ROOT]}"
+ f" by the cli arg {kwargs[_ROOT]}",
+ stacklevel=2,
)
section.pop(_ROOT, None)
return {"dist_name": dist_name, **section, **kwargs}
diff --git a/src/setuptools_scm/_requirement_cls.py b/vcs-versioning/src/vcs_versioning/_requirement_cls.py
similarity index 56%
rename from src/setuptools_scm/_requirement_cls.py
rename to vcs-versioning/src/vcs_versioning/_requirement_cls.py
index 9bb88462..43d1424d 100644
--- a/src/setuptools_scm/_requirement_cls.py
+++ b/vcs-versioning/src/vcs_versioning/_requirement_cls.py
@@ -1,21 +1,13 @@
from __future__ import annotations
-__all__ = ["Requirement", "extract_package_name"]
+import logging
-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,
- )
+__all__ = ["Requirement", "extract_package_name"]
-from . import _log
+from packaging.requirements import Requirement
+from packaging.utils import canonicalize_name
-log = _log.log.getChild("requirement_cls")
+log = logging.getLogger(__name__)
def extract_package_name(requirement_string: str) -> str:
diff --git a/src/setuptools_scm/_run_cmd.py b/vcs-versioning/src/vcs_versioning/_run_cmd.py
similarity index 87%
rename from src/setuptools_scm/_run_cmd.py
rename to vcs-versioning/src/vcs_versioning/_run_cmd.py
index 2dff6369..cbcd2856 100644
--- a/src/setuptools_scm/_run_cmd.py
+++ b/vcs-versioning/src/vcs_versioning/_run_cmd.py
@@ -1,48 +1,38 @@
from __future__ import annotations
+import logging
import os
import shlex
import subprocess
import textwrap
import warnings
+from collections.abc import Callable, Mapping, Sequence
+from typing import TypeVar, overload
-from typing import TYPE_CHECKING
-from typing import Callable
-from typing import Final
-from typing import Mapping
-from typing import Sequence
-from typing import TypeVar
-from typing import overload
-
-from . import _log
from . import _types as _t
-if TYPE_CHECKING:
- BaseCompletedProcess = subprocess.CompletedProcess[str]
-else:
- BaseCompletedProcess = subprocess.CompletedProcess
-
-# pick 40 seconds
-# unfortunately github CI for windows sometimes needs
-# up to 30 seconds to start a command
-
def _get_timeout(env: Mapping[str, str]) -> int:
- return int(env.get("SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT") or 40)
+ """Get subprocess timeout from override context or environment.
+
+ This function is kept for backward compatibility but now uses the
+ global override system.
+ """
+ from .overrides import get_subprocess_timeout
+ return get_subprocess_timeout()
-BROKEN_TIMEOUT: Final[int] = _get_timeout(os.environ)
-log = _log.log.getChild("run_cmd")
+log = logging.getLogger(__name__)
PARSE_RESULT = TypeVar("PARSE_RESULT")
T = TypeVar("T")
-class CompletedProcess(BaseCompletedProcess):
+class CompletedProcess(subprocess.CompletedProcess[str]):
@classmethod
def from_raw(
- cls, input: BaseCompletedProcess, strip: bool = True
+ cls, input: subprocess.CompletedProcess[str], strip: bool = True
) -> CompletedProcess:
return cls(
args=input.args,
@@ -153,7 +143,7 @@ def run(
cmd_4_trace = " ".join(map(_unsafe_quote_for_display, cmd))
log.debug("at %s\n $ %s ", cwd, cmd_4_trace)
if timeout is None:
- timeout = BROKEN_TIMEOUT
+ timeout = _get_timeout(os.environ)
res = subprocess.run(
cmd,
capture_output=True,
@@ -208,7 +198,7 @@ def has_command(
else:
res = not p.returncode
if not res and warn:
- warnings.warn(f"{name!r} was not found", category=RuntimeWarning)
+ warnings.warn(f"{name!r} was not found", category=RuntimeWarning, stacklevel=2)
return res
diff --git a/vcs-versioning/src/vcs_versioning/_scm_version.py b/vcs-versioning/src/vcs_versioning/_scm_version.py
new file mode 100644
index 00000000..0d32b497
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_scm_version.py
@@ -0,0 +1,385 @@
+"""Core ScmVersion data structure and parsing utilities.
+
+This module contains the ScmVersion class which represents a parsed version
+from source control metadata, along with utilities for creating and parsing
+ScmVersion objects.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+import logging
+import warnings
+from collections.abc import Callable
+from datetime import date, datetime
+from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypedDict
+
+from . import _config
+from . import _version_cls as _v
+from ._node_utils import _format_node_for_output
+from ._version_cls import _Version
+
+if TYPE_CHECKING:
+ import sys
+
+ if sys.version_info >= (3, 11):
+ from typing import Unpack
+ else:
+ from typing_extensions import Unpack
+
+_P = ParamSpec("_P")
+
+log = logging.getLogger(__name__)
+
+
+class _TagDict(TypedDict):
+ version: str
+ prefix: str
+ suffix: str
+
+
+class VersionExpectations(TypedDict, total=False):
+ """Expected properties for ScmVersion matching."""
+
+ tag: str | _Version
+ distance: int
+ dirty: bool
+ node_prefix: str # Prefix of the node/commit hash
+ branch: str | None
+ exact: bool
+ preformatted: bool
+ node_date: date | None
+ time: datetime | None
+
+
+@dataclasses.dataclass
+class mismatches:
+ """Represents mismatches between expected and actual ScmVersion properties."""
+
+ expected: dict[str, Any]
+ actual: dict[str, Any]
+
+ def __bool__(self) -> bool:
+ """mismatches is falsy to allow `if not version.matches(...)`."""
+ return False
+
+ def __str__(self) -> str:
+ """Format mismatches for error reporting."""
+ lines = []
+ for key, exp_val in self.expected.items():
+ if key == "node_prefix":
+ # Special handling for node prefix matching
+ actual_node = self.actual.get("node")
+ if not actual_node or not actual_node.startswith(exp_val):
+ lines.append(
+ f" node: expected prefix '{exp_val}', got '{actual_node}'"
+ )
+ else:
+ act_val = self.actual.get(key)
+ if str(exp_val) != str(act_val):
+ lines.append(f" {key}: expected {exp_val!r}, got {act_val!r}")
+ return "\n".join(lines)
+
+ def __repr__(self) -> str:
+ return f"mismatches(expected={self.expected!r}, actual={self.actual!r})"
+
+
+def _parse_version_tag(
+ tag: str | object, config: _config.Configuration
+) -> _TagDict | None:
+ match = config.tag_regex.match(str(tag))
+
+ if match:
+ key: str | int = 1 if len(match.groups()) == 1 else "version"
+ full = match.group(0)
+ log.debug("%r %r %s", tag, config.tag_regex, match)
+ log.debug(
+ "key %s data %s, %s, %r", key, match.groupdict(), match.groups(), full
+ )
+
+ 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)
+
+ return None
+
+
+def callable_or_entrypoint(group: str, callable_or_name: str | Any) -> Any:
+ log.debug("ep %r %r", group, callable_or_name)
+
+ if callable(callable_or_name):
+ return callable_or_name
+
+ from ._entrypoints import _get_ep
+
+ return _get_ep(group, callable_or_name)
+
+
+def tag_to_version(
+ tag: _Version | str, config: _config.Configuration
+) -> _Version | None:
+ """
+ take a tag that might be prefixed with a keyword and return only the version part
+ """
+ log.debug("tag %s", tag)
+
+ tag_dict = _parse_version_tag(tag, config)
+ if tag_dict is None or not tag_dict.get("version", None):
+ warnings.warn(f"tag {tag!r} no version found", stacklevel=2)
+ return None
+
+ version_str = tag_dict["version"]
+ log.debug("version pre parse %s", version_str)
+
+ # Try to create version from base version first
+ try:
+ version: _Version = 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}",
+ stacklevel=2,
+ )
+ # Fall back to trying without any suffix
+ version = config.version_cls(version_str)
+ log.debug("version=%r", version)
+ return 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: _Version = 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}", stacklevel=2
+ )
+ # Return the base version without suffix
+ return version
+
+ return version
+
+
+def _source_epoch_or_utc_now() -> datetime:
+ """Get datetime from SOURCE_DATE_EPOCH or current UTC time.
+
+ Uses the active GlobalOverrides context if available, otherwise returns
+ current UTC time.
+ """
+ from .overrides import source_epoch_or_utc_now
+
+ return source_epoch_or_utc_now()
+
+
+@dataclasses.dataclass
+class ScmVersion:
+ """represents a parsed version from scm"""
+
+ tag: _v.Version | _v.NonNormalizedVersion
+ """the related tag or preformatted version"""
+ config: _config.Configuration
+ """the configuration used to parse the version"""
+ distance: int = 0
+ """the number of commits since the tag"""
+ node: str | None = None
+ """the shortened node id"""
+ dirty: bool = False
+ """whether the working copy had uncommitted changes"""
+ preformatted: bool = False
+ """whether the version string was preformatted"""
+ branch: str | None = None
+ """the branch name if any"""
+ node_date: date | None = None
+ """the date of the commit if available"""
+ time: datetime = dataclasses.field(default_factory=_source_epoch_or_utc_now)
+ """the current time or source epoch time
+ only set for unit-testing version schemes
+ for real usage it must be `now(utc)` or `SOURCE_EPOCH`
+ """
+
+ @property
+ 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""
+ )
+
+ def format_with(self, fmt: str, **kw: object) -> str:
+ """format a given format string with attributes of this object"""
+ return fmt.format(
+ time=self.time,
+ tag=self.tag,
+ distance=self.distance,
+ node=_format_node_for_output(self.node),
+ dirty=self.dirty,
+ branch=self.branch,
+ node_date=self.node_date,
+ **kw,
+ )
+
+ def format_choice(self, clean_format: str, dirty_format: str, **kw: object) -> str:
+ """given `clean_format` and `dirty_format`
+
+ choose one based on `self.dirty` and format it using `self.format_with`"""
+
+ return self.format_with(dirty_format if self.dirty else clean_format, **kw)
+
+ def format_next_version(
+ self,
+ guess_next: Callable[Concatenate[ScmVersion, _P], str],
+ fmt: str = "{guessed}.dev{distance}",
+ *k: _P.args,
+ **kw: _P.kwargs,
+ ) -> str:
+ guessed = guess_next(self, *k, **kw)
+ return self.format_with(fmt, guessed=guessed)
+
+ def matches(self, **expectations: Unpack[VersionExpectations]) -> bool | mismatches:
+ """Check if this ScmVersion matches the given expectations.
+
+ Returns True if all specified properties match, or a mismatches
+ object (which is falsy) containing details of what didn't match.
+
+ Args:
+ **expectations: Properties to check, using VersionExpectations TypedDict
+ """
+ # Map expectation keys to ScmVersion attributes
+ attr_map: dict[str, Callable[[], Any]] = {
+ "tag": lambda: str(self.tag),
+ "node_prefix": lambda: self.node,
+ "distance": lambda: self.distance,
+ "dirty": lambda: self.dirty,
+ "branch": lambda: self.branch,
+ "exact": lambda: self.exact,
+ "preformatted": lambda: self.preformatted,
+ "node_date": lambda: self.node_date,
+ "time": lambda: self.time,
+ }
+
+ # Build actual values dict
+ actual: dict[str, Any] = {
+ key: attr_map[key]() for key in expectations if key in attr_map
+ }
+
+ # Process expectations
+ expected = {
+ "tag" if k == "tag" else k: str(v) if k == "tag" else v
+ for k, v in expectations.items()
+ }
+
+ # Check for mismatches
+ def has_mismatch() -> bool:
+ for key, exp_val in expected.items():
+ if key == "node_prefix":
+ act_val = actual.get("node_prefix")
+ if not act_val or not act_val.startswith(exp_val):
+ return True
+ else:
+ if str(exp_val) != str(actual.get(key)):
+ return True
+ return False
+
+ if has_mismatch():
+ # Rename node_prefix back to node for actual values in mismatch reporting
+ if "node_prefix" in actual:
+ actual["node"] = actual.pop("node_prefix")
+ return mismatches(expected=expected, actual=actual)
+ return True
+
+
+def _parse_tag(
+ tag: _Version | str, preformatted: bool, config: _config.Configuration
+) -> _Version:
+ if preformatted:
+ # 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
+ return version
+ else:
+ return tag
+
+
+class _ScmVersionKwargs(TypedDict, total=False):
+ """TypedDict for ScmVersion constructor keyword arguments."""
+
+ distance: int
+ node: str | None
+ dirty: bool
+ preformatted: bool
+ branch: str | None
+ node_date: date | None
+ time: datetime
+
+
+def meta(
+ tag: str | _Version,
+ *,
+ distance: int = 0,
+ dirty: bool = False,
+ node: str | None = None,
+ preformatted: bool = False,
+ branch: str | None = None,
+ config: _config.Configuration,
+ node_date: date | None = None,
+ time: datetime | None = None,
+) -> ScmVersion:
+ parsed_version: _Version
+ # 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}"
+
+ # Pass time explicitly to avoid triggering default_factory if provided
+ kwargs: _ScmVersionKwargs = {
+ "distance": distance,
+ "node": node,
+ "dirty": dirty,
+ "preformatted": preformatted,
+ "branch": branch,
+ "node_date": node_date,
+ }
+ if time is not None:
+ kwargs["time"] = time
+
+ scm_version = ScmVersion(parsed_version, config=config, **kwargs)
+ return scm_version
diff --git a/vcs-versioning/src/vcs_versioning/_test_utils.py b/vcs-versioning/src/vcs_versioning/_test_utils.py
new file mode 100644
index 00000000..d9c93a23
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_test_utils.py
@@ -0,0 +1,256 @@
+from __future__ import annotations
+
+import itertools
+from collections.abc import Callable
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+import pytest
+
+from vcs_versioning._run_cmd import has_command
+
+if TYPE_CHECKING:
+ import sys
+
+ from vcs_versioning._config import Configuration
+ from vcs_versioning._scm_version import ScmVersion, VersionExpectations
+
+ if sys.version_info >= (3, 11):
+ from typing import Unpack
+ else:
+ from typing_extensions import Unpack
+
+
+class WorkDir:
+ """a simple model for a"""
+
+ commit_command: str
+ signed_commit_command: str
+ add_command: str
+ tag_command: str
+ parse: Callable[[Path, Configuration], ScmVersion | None] | None = None
+
+ def __repr__(self) -> str:
+ return f""
+
+ def __init__(self, cwd: Path) -> None:
+ self.cwd = cwd
+ self.__counter = itertools.count()
+
+ 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 vcs_versioning._run_cmd import run
+
+ return run(cmd, cwd=self.cwd, timeout=timeout).stdout
+
+ def write(self, name: str, content: str | bytes) -> Path:
+ path = self.cwd / name
+ if isinstance(content, bytes):
+ path.write_bytes(content)
+ else:
+ path.write_text(content, encoding="utf-8")
+ return path
+
+ def _reason(self, given_reason: str | None) -> str:
+ if given_reason is None:
+ return f"number-{next(self.__counter)}"
+ else:
+ return given_reason
+
+ def add_and_commit(
+ self, reason: str | None = None, signed: bool = False, **kwargs: object
+ ) -> None:
+ self(self.add_command)
+ self.commit(reason=reason, signed=signed, **kwargs)
+
+ def commit(self, reason: str | None = None, signed: bool = False) -> None:
+ reason = self._reason(reason)
+ self(
+ self.commit_command if not signed else self.signed_commit_command,
+ reason=reason,
+ )
+
+ def commit_testfile(self, reason: str | None = None, signed: bool = False) -> None:
+ reason = self._reason(reason)
+ self.write("test.txt", f"test {reason}")
+ self(self.add_command)
+ self.commit(reason=reason, signed=signed)
+
+ def get_version(self, **kw: Any) -> str:
+ __tracebackhide__ = True
+ from vcs_versioning._get_version_impl import get_version
+
+ version = get_version(root=self.cwd, fallback_root=self.cwd, **kw)
+ print(self.cwd.name, version, sep=": ")
+ return version
+
+ def create_basic_setup_py(
+ self, name: str = "test-package", use_scm_version: str = "True"
+ ) -> None:
+ """Create a basic setup.py file with version configuration.
+
+ Note: This is for setuptools_scm compatibility testing.
+ """
+ self.write(
+ "setup.py",
+ f"""__import__('setuptools').setup(
+ name="{name}",
+ use_scm_version={use_scm_version},
+)""",
+ )
+
+ def create_basic_pyproject_toml(
+ self,
+ name: str = "test-package",
+ dynamic_version: bool = True,
+ tool_name: str = "vcs-versioning",
+ ) -> None:
+ """Create a basic pyproject.toml file with version configuration.
+
+ Args:
+ name: Project name
+ dynamic_version: Whether to add dynamic=['version']
+ tool_name: Tool section name (e.g., 'vcs-versioning' or 'setuptools_scm')
+ """
+ dynamic_section = 'dynamic = ["version"]' if dynamic_version else ""
+ self.write(
+ "pyproject.toml",
+ f"""[build-system]
+requires = ["setuptools>=64", "setuptools_scm>=8"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "{name}"
+{dynamic_section}
+
+[tool.{tool_name}]
+""",
+ )
+
+ def create_basic_setup_cfg(self, name: str = "test-package") -> None:
+ """Create a basic setup.cfg file with metadata."""
+ self.write(
+ "setup.cfg",
+ f"""[metadata]
+name = {name}
+""",
+ )
+
+ def create_test_file(
+ self, filename: str = "test.txt", content: str = "test content"
+ ) -> None:
+ """Create a test file and commit it to the repository."""
+ # Create parent directories if they don't exist
+ path = self.cwd / filename
+ path.parent.mkdir(parents=True, exist_ok=True)
+ self.write(filename, content)
+ self.add_and_commit()
+
+ def create_tag(self, tag: str = "1.0.0") -> None:
+ """Create a tag using the configured tag_command."""
+ if hasattr(self, "tag_command"):
+ self(self.tag_command, tag=tag)
+ else:
+ raise RuntimeError("No tag_command configured")
+
+ def configure_git_commands(self) -> None:
+ """Configure git commands without initializing the repository."""
+ from vcs_versioning._backends._git import parse as git_parse
+
+ self.add_command = "git add ."
+ self.commit_command = "git commit -m test-{reason}"
+ self.tag_command = "git tag {tag}"
+ self.parse = git_parse
+
+ def configure_hg_commands(self) -> None:
+ """Configure mercurial commands without initializing the repository."""
+ from vcs_versioning._backends._hg import parse as hg_parse
+
+ self.add_command = "hg add ."
+ self.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"'
+ self.tag_command = "hg tag {tag}"
+ self.parse = hg_parse
+
+ def setup_git(
+ self, monkeypatch: pytest.MonkeyPatch | None = None, *, init: bool = True
+ ) -> WorkDir:
+ """Set up git SCM for this WorkDir.
+
+ Args:
+ monkeypatch: Optional pytest MonkeyPatch to clear HOME environment
+ init: Whether to initialize the git repository (default: True)
+
+ Returns:
+ Self for method chaining
+
+ Raises:
+ pytest.skip: If git executable is not found
+ """
+ if not has_command("git", warn=False):
+ pytest.skip("git executable not found")
+
+ self.configure_git_commands()
+
+ if init:
+ if monkeypatch:
+ monkeypatch.delenv("HOME", raising=False)
+ self("git init")
+ self("git config user.email test@example.com")
+ self('git config user.name "a test"')
+
+ return self
+
+ def setup_hg(self, *, init: bool = True) -> WorkDir:
+ """Set up mercurial SCM for this WorkDir.
+
+ Args:
+ init: Whether to initialize the mercurial repository (default: True)
+
+ Returns:
+ Self for method chaining
+
+ Raises:
+ pytest.skip: If hg executable is not found
+ """
+ if not has_command("hg", warn=False):
+ pytest.skip("hg executable not found")
+
+ self.configure_hg_commands()
+
+ if init:
+ self("hg init")
+
+ return self
+
+ def expect_parse(
+ self,
+ **expectations: Unpack[VersionExpectations],
+ ) -> None:
+ """Parse version from this working directory and assert it matches expected properties.
+
+ Uses the same signature as ScmVersion.matches() via TypedDict Unpack.
+ """
+ __tracebackhide__ = True
+ from vcs_versioning._config import Configuration
+
+ if self.parse is None:
+ raise RuntimeError(
+ "No SCM configured - call setup_git() or setup_hg() first"
+ )
+
+ config = Configuration(root=self.cwd)
+ scm_version = self.parse(self.cwd, config)
+
+ if scm_version is None:
+ raise AssertionError("Failed to parse version")
+
+ # Call matches with all expectations
+ result = scm_version.matches(**expectations)
+
+ # If result is mismatches (falsy), raise assertion with details
+ if not result:
+ raise AssertionError(
+ f"Version mismatch:\n{result}\nActual version: {scm_version!r}"
+ )
diff --git a/vcs-versioning/src/vcs_versioning/_toml.py b/vcs-versioning/src/vcs_versioning/_toml.py
new file mode 100644
index 00000000..7b03c792
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_toml.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+import logging
+import sys
+from collections.abc import Callable
+from pathlib import Path
+from typing import Any, TypeAlias, TypedDict, TypeVar, cast, get_type_hints
+
+if sys.version_info >= (3, 11):
+ from tomllib import loads as load_toml
+else:
+ from tomli import loads as load_toml
+
+
+log = logging.getLogger(__name__)
+
+TOML_RESULT: TypeAlias = dict[str, Any]
+TOML_LOADER: TypeAlias = Callable[[str], TOML_RESULT]
+
+# TypeVar for generic TypedDict support - the schema defines the return type
+TSchema = TypeVar("TSchema", bound=TypedDict) # type: ignore[valid-type]
+
+
+class InvalidTomlError(ValueError):
+ """Raised when TOML data cannot be parsed."""
+
+
+class InvalidTomlSchemaError(ValueError):
+ """Raised when TOML data does not conform to the expected schema."""
+
+
+def read_toml_content(path: Path, default: TOML_RESULT | None = None) -> TOML_RESULT:
+ try:
+ data = path.read_text(encoding="utf-8")
+ except FileNotFoundError:
+ if default is None:
+ raise
+ else:
+ log.debug("%s missing, presuming default %r", path, default)
+ return default
+ else:
+ 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):
+ cheat: dict[str, Any]
+
+
+def _validate_against_schema(
+ data: dict[str, Any],
+ schema: type[TypedDict] | None, # type: ignore[valid-type]
+) -> dict[str, Any]:
+ """Validate parsed TOML data against a TypedDict schema.
+
+ Args:
+ data: Parsed TOML data to validate
+ schema: TypedDict class defining valid fields, or None to skip validation
+
+ Returns:
+ The validated data with invalid fields removed
+
+ Raises:
+ InvalidTomlSchemaError: If there are invalid fields (after logging warnings)
+ """
+ if schema is None:
+ return data
+
+ # Extract valid field names from the TypedDict
+ try:
+ valid_fields = frozenset(get_type_hints(schema).keys())
+ except NameError as e:
+ # If type hints can't be resolved, log warning and skip validation
+ log.warning("Could not resolve type hints for schema validation: %s", e)
+ return data
+
+ # If the schema has no fields (empty TypedDict), skip validation
+ if not valid_fields:
+ return data
+
+ invalid_fields = set(data.keys()) - valid_fields
+ if invalid_fields:
+ log.warning(
+ "Invalid fields in TOML data: %s. Valid fields are: %s",
+ sorted(invalid_fields),
+ sorted(valid_fields),
+ )
+ # Remove invalid fields
+ validated_data = {k: v for k, v in data.items() if k not in invalid_fields}
+ return validated_data
+
+ return data
+
+
+def load_toml_or_inline_map(data: str | None, *, schema: type[TSchema]) -> TSchema:
+ """Load toml data - with a special hack if only a inline map is given.
+
+ Args:
+ data: TOML string to parse, or None for empty dict
+ schema: TypedDict class for schema validation.
+ Invalid fields will be logged as warnings and removed.
+
+ Returns:
+ Parsed TOML data as a dictionary conforming to the schema type
+
+ Raises:
+ InvalidTomlError: If the TOML content is malformed
+ """
+ if not data:
+ return {} # type: ignore[return-value]
+ try:
+ if data[0] == "{":
+ data = "cheat=" + data
+ loaded: _CheatTomlData = cast(_CheatTomlData, load_toml(data))
+ result = loaded["cheat"]
+ else:
+ result = load_toml(data)
+
+ return _validate_against_schema(result, schema) # type: ignore[return-value]
+ except Exception as e: # tomllib/tomli raise different decode errors
+ # Don't re-wrap our own validation errors
+ if isinstance(e, InvalidTomlSchemaError):
+ raise
+ raise InvalidTomlError("Invalid TOML content") from e
diff --git a/vcs-versioning/src/vcs_versioning/_types.py b/vcs-versioning/src/vcs_versioning/_types.py
new file mode 100644
index 00000000..87769927
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_types.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from collections.abc import Callable, Sequence
+from typing import TYPE_CHECKING, TypeAlias
+
+if TYPE_CHECKING:
+ from ._scm_version import ScmVersion
+
+# Re-export from _compat for backward compatibility
+from ._compat import PathT as PathT # noqa: PLC0414
+
+__all__ = [
+ "PathT",
+ "CMD_TYPE",
+ "VERSION_SCHEME",
+ "VERSION_SCHEMES",
+ "SCMVERSION",
+ "GIT_PRE_PARSE",
+]
+
+CMD_TYPE: TypeAlias = Sequence[PathT] | str
+
+VERSION_SCHEME: TypeAlias = str | Callable[["ScmVersion"], str]
+VERSION_SCHEMES: TypeAlias = list[str] | tuple[str, ...] | VERSION_SCHEME
+SCMVERSION: TypeAlias = "ScmVersion"
+
+# Git pre-parse function types
+GIT_PRE_PARSE: TypeAlias = str | None
diff --git a/src/setuptools_scm/_version_cls.py b/vcs-versioning/src/vcs_versioning/_version_cls.py
similarity index 88%
rename from src/setuptools_scm/_version_cls.py
rename to vcs-versioning/src/vcs_versioning/_version_cls.py
index e0fe387b..ff362e71 100644
--- a/src/setuptools_scm/_version_cls.py
+++ b/vcs-versioning/src/vcs_versioning/_version_cls.py
@@ -1,22 +1,21 @@
from __future__ import annotations
-from typing import Type
-from typing import Union
-from typing import cast
+import logging
+from typing import TypeAlias, cast
try:
from packaging.version import InvalidVersion
from packaging.version import Version as Version
except ImportError:
- from setuptools.extern.packaging.version import ( # type: ignore[import-not-found, no-redef]
+ from setuptools.extern.packaging.version import ( # type: ignore[import-not-found,no-redef]
InvalidVersion,
)
from setuptools.extern.packaging.version import ( # type: ignore[no-redef]
Version as Version,
)
-from . import _log
-log = _log.log.getChild("version_cls")
+
+log = logging.getLogger(__name__)
class NonNormalizedVersion(Version):
@@ -68,7 +67,7 @@ def _version_as_tuple(version_str: str) -> tuple[int | str, ...]:
return version_fields
-_VersionT = Union[Version, NonNormalizedVersion]
+_Version: TypeAlias = Version | NonNormalizedVersion
def import_name(name: str) -> object:
@@ -80,8 +79,8 @@ def import_name(name: str) -> object:
def _validate_version_cls(
- version_cls: type[_VersionT] | str | None, normalize: bool
-) -> type[_VersionT]:
+ version_cls: type[_Version] | str | None, normalize: bool
+) -> type[_Version]:
if not normalize:
if version_cls is not None:
raise ValueError(
@@ -94,7 +93,7 @@ def _validate_version_cls(
return Version
elif isinstance(version_cls, str):
try:
- return cast(Type[_VersionT], import_name(version_cls))
+ return cast(type[_Version], import_name(version_cls))
except Exception:
raise ValueError(f"Unable to import version_cls='{version_cls}'") from None
else:
diff --git a/vcs-versioning/src/vcs_versioning/_version_inference.py b/vcs-versioning/src/vcs_versioning/_version_inference.py
new file mode 100644
index 00000000..db4068a0
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_version_inference.py
@@ -0,0 +1,52 @@
+"""Core version inference functionality for build tool integrations."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+ from ._pyproject_reading import PyProjectData
+
+
+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.
+
+ This is a pure helper that avoids requiring build-tool specific
+ distribution objects, making it easier to test and reuse across
+ different build systems.
+
+ 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.
+
+ Raises:
+ SystemExit: If version cannot be determined (via _version_missing)
+ """
+ from ._config import Configuration
+ from ._get_version_impl import _get_version, _version_missing
+
+ config = 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
+
+
+__all__ = ["infer_version_string"]
diff --git a/vcs-versioning/src/vcs_versioning/_version_schemes/__init__.py b/vcs-versioning/src/vcs_versioning/_version_schemes/__init__.py
new file mode 100644
index 00000000..ccf73d8c
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_version_schemes/__init__.py
@@ -0,0 +1,123 @@
+"""Version schemes package for setuptools-scm.
+
+This package contains all version and local schemes that determine how
+version numbers are calculated and formatted from SCM metadata.
+"""
+
+from __future__ import annotations
+
+import logging
+
+from .. import _entrypoints
+from .._scm_version import ScmVersion, callable_or_entrypoint, meta, tag_to_version
+from ._common import (
+ SEMVER_LEN,
+ SEMVER_MINOR,
+ SEMVER_PATCH,
+ combine_version_with_local_parts,
+)
+from ._standard import (
+ calver_by_date,
+ date_ver_match,
+ get_local_dirty_tag,
+ get_local_node_and_date,
+ get_local_node_and_timestamp,
+ get_no_local_node,
+ guess_next_date_ver,
+ guess_next_dev_version,
+ guess_next_simple_semver,
+ guess_next_version,
+ no_guess_dev_version,
+ only_version,
+ postrelease_version,
+ release_branch_semver,
+ release_branch_semver_version,
+ simplified_semver_version,
+)
+from ._towncrier import version_from_fragments
+
+log = logging.getLogger(__name__)
+
+__all__ = [
+ # Constants
+ "SEMVER_LEN",
+ "SEMVER_MINOR",
+ "SEMVER_PATCH",
+ # Core types and utilities
+ "ScmVersion",
+ "meta",
+ "tag_to_version",
+ "callable_or_entrypoint",
+ "format_version",
+ # Version schemes
+ "guess_next_version",
+ "guess_next_dev_version",
+ "guess_next_simple_semver",
+ "simplified_semver_version",
+ "release_branch_semver_version",
+ "release_branch_semver", # deprecated
+ "only_version",
+ "no_guess_dev_version",
+ "calver_by_date",
+ "date_ver_match",
+ "guess_next_date_ver",
+ "postrelease_version",
+ # Local schemes
+ "get_local_node_and_date",
+ "get_local_node_and_timestamp",
+ "get_local_dirty_tag",
+ "get_no_local_node",
+ # Towncrier
+ "version_from_fragments",
+ # Utilities
+ "combine_version_with_local_parts",
+]
+
+
+def format_version(version: ScmVersion) -> str:
+ """Format a ScmVersion into a final version string.
+
+ This orchestrates calling the version scheme and local scheme,
+ then combining them with any local data from the original tag.
+
+ Args:
+ version: The ScmVersion to format
+
+ Returns:
+ A fully formatted version string
+ """
+ log.debug("scm version %s", version)
+ log.debug("config %s", version.config)
+ if version.preformatted:
+ 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_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)
+
+ # 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/vcs-versioning/src/vcs_versioning/_version_schemes/_common.py b/vcs-versioning/src/vcs_versioning/_version_schemes/_common.py
new file mode 100644
index 00000000..b544b5fb
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_version_schemes/_common.py
@@ -0,0 +1,62 @@
+"""Common utilities shared across version schemes."""
+
+from __future__ import annotations
+
+# Semantic versioning constants
+SEMVER_MINOR = 2
+SEMVER_PATCH = 3
+SEMVER_LEN = 3
+
+
+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
diff --git a/vcs-versioning/src/vcs_versioning/_version_schemes/_standard.py b/vcs-versioning/src/vcs_versioning/_version_schemes/_standard.py
new file mode 100644
index 00000000..b9656ce9
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_version_schemes/_standard.py
@@ -0,0 +1,237 @@
+"""Standard version and local schemes for setuptools-scm.
+
+This module contains the built-in version schemes and local schemes that determine
+how version numbers are calculated and formatted.
+"""
+
+from __future__ import annotations
+
+import logging
+import re
+import warnings
+from datetime import date, datetime, timedelta, timezone
+from re import Match
+from typing import TYPE_CHECKING
+
+from .. import _modify_version
+from .._scm_version import ScmVersion, _parse_version_tag
+from .._version_cls import Version as PkgVersion
+from ._common import SEMVER_LEN, SEMVER_MINOR, SEMVER_PATCH
+
+if TYPE_CHECKING:
+ pass
+
+log = logging.getLogger(__name__)
+
+
+# Version Schemes
+# ----------------
+
+
+def guess_next_version(tag_version: ScmVersion) -> str:
+ version = _modify_version.strip_local(str(tag_version.tag))
+ return _modify_version._bump_dev(version) or _modify_version._bump_regex(version)
+
+
+def guess_next_dev_version(version: ScmVersion) -> str:
+ if version.exact:
+ return version.format_with("{tag}")
+ else:
+ return version.format_next_version(guess_next_version)
+
+
+def guess_next_simple_semver(
+ version: ScmVersion, retain: int, increment: bool = True
+) -> str:
+ parts = list(version.tag.release[:retain])
+ while len(parts) < retain:
+ parts.append(0)
+ if increment:
+ parts[-1] += 1
+ while len(parts) < SEMVER_LEN:
+ parts.append(0)
+ return ".".join(str(i) for i in parts)
+
+
+def simplified_semver_version(version: ScmVersion) -> str:
+ if version.exact:
+ return guess_next_simple_semver(version, retain=SEMVER_LEN, increment=False)
+ elif version.branch is not None and "feature" in version.branch:
+ return version.format_next_version(
+ guess_next_simple_semver, retain=SEMVER_MINOR
+ )
+ else:
+ return version.format_next_version(
+ guess_next_simple_semver, retain=SEMVER_PATCH
+ )
+
+
+def release_branch_semver_version(version: ScmVersion) -> str:
+ if version.exact:
+ return version.format_with("{tag}")
+ if version.branch is not None:
+ # Does the branch name (stripped of namespace) parse as a version?
+ branch_ver_data = _parse_version_tag(
+ version.branch.split("/")[-1], version.config
+ )
+ if branch_ver_data is not None:
+ branch_ver = branch_ver_data["version"]
+ if branch_ver[0] == "v":
+ # Allow branches that start with 'v', similar to Version.
+ branch_ver = branch_ver[1:]
+ # Does the branch version up to the minor part match the tag? If not it
+ # might be like, an issue number or something and not a version number, so
+ # we only want to use it if it matches.
+ tag_ver_up_to_minor = str(version.tag).split(".")[:SEMVER_MINOR]
+ branch_ver_up_to_minor = branch_ver.split(".")[:SEMVER_MINOR]
+ if branch_ver_up_to_minor == tag_ver_up_to_minor:
+ # We're in a release/maintenance branch, next is a patch/rc/beta bump:
+ return version.format_next_version(guess_next_version)
+ # We're in a development branch, next is a minor bump:
+ return version.format_next_version(guess_next_simple_semver, retain=SEMVER_MINOR)
+
+
+def release_branch_semver(version: ScmVersion) -> str:
+ warnings.warn(
+ "release_branch_semver is deprecated and will be removed in the future. "
+ "Use release_branch_semver_version instead",
+ category=DeprecationWarning,
+ stacklevel=2,
+ )
+ return release_branch_semver_version(version)
+
+
+def only_version(version: ScmVersion) -> str:
+ return version.format_with("{tag}")
+
+
+def no_guess_dev_version(version: ScmVersion) -> str:
+ if version.exact:
+ return version.format_with("{tag}")
+ else:
+ return version.format_next_version(_modify_version._dont_guess_next_version)
+
+
+_DATE_REGEX = re.compile(
+ r"""
+ ^(?P
+ (?P[vV]?)
+ (?P\d{2}|\d{4})(?:\.\d{1,2}){2})
+ (?:\.(?P\d*))?$
+ """,
+ re.VERBOSE,
+)
+
+
+def date_ver_match(ver: str) -> Match[str] | None:
+ return _DATE_REGEX.match(ver)
+
+
+def guess_next_date_ver(
+ version: ScmVersion,
+ node_date: date | None = None,
+ date_fmt: str | None = None,
+ version_cls: type | None = None,
+) -> str:
+ """
+ same-day -> patch +1
+ other-day -> today
+
+ distance is always added as .devX
+ """
+ match = date_ver_match(str(version.tag))
+ if match is None:
+ warnings.warn(
+ f"{version} does not correspond to a valid versioning date, "
+ "assuming legacy version",
+ stacklevel=2,
+ )
+ if date_fmt is None:
+ date_fmt = "%y.%m.%d"
+ else:
+ # deduct date format if not provided
+ if date_fmt is None:
+ date_fmt = "%Y.%m.%d" if len(match.group("year")) == 4 else "%y.%m.%d"
+ if prefix := match.group("prefix"):
+ if not date_fmt.startswith(prefix):
+ date_fmt = prefix + date_fmt
+
+ today = version.time.date()
+ head_date = node_date or today
+ # compute patch
+ if match is None:
+ # For legacy non-date tags, always use patch=0 (treat as "other day")
+ # Use yesterday to ensure tag_date != head_date
+ tag_date = head_date - timedelta(days=1)
+ else:
+ tag_date = (
+ datetime.strptime(match.group("date"), date_fmt)
+ .replace(tzinfo=timezone.utc)
+ .date()
+ )
+ if tag_date == head_date:
+ 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 (only for actual date tags, not legacy)
+ warnings.warn(
+ f"your previous tag ({tag_date}) is ahead your node date ({head_date})",
+ stacklevel=2,
+ )
+ patch = 0
+ next_version = "{node_date:{date_fmt}}.{patch}".format(
+ node_date=head_date, date_fmt=date_fmt, patch=patch
+ )
+ # rely on the Version object to ensure consistency (e.g. remove leading 0s)
+ if version_cls is None:
+ version_cls = PkgVersion
+ next_version = str(version_cls(next_version))
+ return next_version
+
+
+def calver_by_date(version: ScmVersion) -> str:
+ if version.exact and not version.dirty:
+ return version.format_with("{tag}")
+ # TODO: move the release-X check to a new scheme
+ if version.branch is not None and version.branch.startswith("release-"):
+ branch_ver = _parse_version_tag(version.branch.split("-")[-1], version.config)
+ if branch_ver is not None:
+ ver = branch_ver["version"]
+ match = date_ver_match(ver)
+ if match:
+ return ver
+ return version.format_next_version(
+ guess_next_date_ver,
+ node_date=version.node_date,
+ version_cls=version.config.version_cls,
+ )
+
+
+def postrelease_version(version: ScmVersion) -> str:
+ if version.exact:
+ return version.format_with("{tag}")
+ else:
+ return version.format_with("{tag}.post{distance}")
+
+
+# Local Schemes
+# -------------
+
+
+def get_local_node_and_date(version: ScmVersion) -> str:
+ return _modify_version._format_local_with_time(version, time_format="%Y%m%d")
+
+
+def get_local_node_and_timestamp(version: ScmVersion) -> str:
+ return _modify_version._format_local_with_time(version, time_format="%Y%m%d%H%M%S")
+
+
+def get_local_dirty_tag(version: ScmVersion) -> str:
+ return version.format_choice("", "+dirty")
+
+
+def get_no_local_node(version: ScmVersion) -> str:
+ return ""
diff --git a/vcs-versioning/src/vcs_versioning/_version_schemes/_towncrier.py b/vcs-versioning/src/vcs_versioning/_version_schemes/_towncrier.py
new file mode 100644
index 00000000..4ec9aca6
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_version_schemes/_towncrier.py
@@ -0,0 +1,212 @@
+"""Version scheme based on towncrier changelog fragments.
+
+This version scheme analyzes changelog fragments in the changelog.d/ directory
+to determine the appropriate version bump:
+- Major bump: if 'removal' fragments are present
+- Minor bump: if 'feature' or 'deprecation' fragments are present
+- Patch bump: if only 'bugfix', 'doc', or 'misc' fragments are present
+
+Falls back to guess-next-dev if no fragments are found.
+"""
+
+from __future__ import annotations
+
+import logging
+from pathlib import Path
+
+from .._scm_version import ScmVersion
+from ._common import SEMVER_MINOR, SEMVER_PATCH
+from ._standard import guess_next_dev_version, guess_next_simple_semver
+
+log = logging.getLogger(__name__)
+
+# Fragment types that indicate different version bumps
+MAJOR_FRAGMENT_TYPES = {"removal"}
+MINOR_FRAGMENT_TYPES = {"feature", "deprecation"}
+PATCH_FRAGMENT_TYPES = {"bugfix", "doc", "misc"}
+
+ALL_FRAGMENT_TYPES = MAJOR_FRAGMENT_TYPES | MINOR_FRAGMENT_TYPES | PATCH_FRAGMENT_TYPES
+
+
+def _find_fragments(
+ root: Path, changelog_dir: str = "changelog.d"
+) -> dict[str, list[str]]:
+ """Find and categorize changelog fragments.
+
+ Args:
+ root: Root directory to search from
+ changelog_dir: Name of the changelog directory
+
+ Returns:
+ Dictionary mapping fragment types to lists of fragment filenames
+ """
+ fragments: dict[str, list[str]] = {ftype: [] for ftype in ALL_FRAGMENT_TYPES}
+
+ changelog_path = root / changelog_dir
+ if not changelog_path.exists():
+ log.debug("No changelog directory found at %s", changelog_path)
+ return fragments
+
+ for entry in changelog_path.iterdir():
+ if not entry.is_file():
+ continue
+
+ # Skip template, README, and .gitkeep files
+ if entry.name in ("template.md", "README.md", ".gitkeep"):
+ continue
+
+ # Fragment naming: {number}.{type}.md
+ parts = entry.name.split(".")
+ if len(parts) >= 2:
+ fragment_type = parts[1]
+ if fragment_type in ALL_FRAGMENT_TYPES:
+ fragments[fragment_type].append(entry.name)
+ log.debug("Found %s fragment: %s", fragment_type, entry.name)
+
+ return fragments
+
+
+def _determine_bump_type(fragments: dict[str, list[str]]) -> str | None:
+ """Determine version bump type from fragments.
+
+ Returns:
+ 'major', 'minor', 'patch', or None if no fragments found
+ """
+ # Check for any fragments at all
+ total_fragments = sum(len(files) for files in fragments.values())
+ if total_fragments == 0:
+ return None
+
+ # Major bump if any removal fragments
+ if any(fragments[ftype] for ftype in MAJOR_FRAGMENT_TYPES):
+ return "major"
+
+ # Minor bump if any feature/deprecation fragments
+ if any(fragments[ftype] for ftype in MINOR_FRAGMENT_TYPES):
+ return "minor"
+
+ # Patch bump for other fragments
+ if any(fragments[ftype] for ftype in PATCH_FRAGMENT_TYPES):
+ return "patch"
+
+ return None
+
+
+def _get_changelog_root(version: ScmVersion) -> Path:
+ """Get the root directory where changelog.d/ should be located.
+
+ For monorepo support, prefers relative_to (config file location).
+ Falls back to absolute_root (VCS root).
+ """
+ import os
+
+ if version.config.relative_to:
+ # relative_to is typically the pyproject.toml file path
+ # changelog.d/ should be in the same directory
+ if os.path.isfile(version.config.relative_to):
+ return Path(os.path.dirname(version.config.relative_to))
+ else:
+ return Path(version.config.relative_to)
+ else:
+ # When no relative_to is set, use absolute_root (the VCS root)
+ return Path(version.config.absolute_root)
+
+
+def _guess_next_major(version: ScmVersion) -> str:
+ """Guess next major version (X+1.0.0) from current tag."""
+ from .. import _modify_version
+
+ tag_version = _modify_version.strip_local(str(version.tag))
+ parts = tag_version.split(".")
+ if len(parts) >= 1:
+ major = int(parts[0].lstrip("v")) # Handle 'v' prefix
+ return f"{major + 1}.0.0"
+ # Fallback to bump_dev
+ bumped = _modify_version._bump_dev(tag_version)
+ return bumped if bumped is not None else f"{tag_version}.dev0"
+
+
+def version_from_fragments(version: ScmVersion) -> str:
+ """Version scheme that determines version from towncrier fragments.
+
+ This is the main entry point registered as a setuptools_scm version scheme.
+
+ Args:
+ version: ScmVersion object from VCS
+
+ Returns:
+ Formatted version string
+ """
+ # If we're exactly on a tag, return it
+ if version.exact:
+ return version.format_with("{tag}")
+
+ root = _get_changelog_root(version)
+ log.debug("Analyzing fragments in %s", root)
+
+ # Find and analyze fragments
+ fragments = _find_fragments(root)
+ bump_type = _determine_bump_type(fragments)
+
+ if bump_type is None:
+ log.debug("No fragments found, falling back to guess-next-dev")
+ return guess_next_dev_version(version)
+
+ log.info("Determined version bump type from fragments: %s", bump_type)
+
+ # Determine the next version based on bump type
+ if bump_type == "major":
+ return version.format_next_version(_guess_next_major)
+
+ elif bump_type == "minor":
+ return version.format_next_version(
+ guess_next_simple_semver, retain=SEMVER_MINOR
+ )
+
+ else: # patch
+ return version.format_next_version(
+ guess_next_simple_semver, retain=SEMVER_PATCH
+ )
+
+
+def get_release_version(version: ScmVersion) -> str | None:
+ """Get clean release version from towncrier fragments (no .devN suffix).
+
+ Unlike version_from_fragments(), this returns only the clean version
+ string (e.g., "10.0.0") without .devN suffix. Used by release tooling.
+
+ Args:
+ version: ScmVersion object from VCS
+
+ Returns:
+ Clean version string, or None if no fragments found
+ """
+ # If we're exactly on a tag, return it
+ if version.exact:
+ return version.format_with("{tag}")
+
+ root = _get_changelog_root(version)
+ log.debug("Analyzing fragments for release version in %s", root)
+
+ fragments = _find_fragments(root)
+ bump_type = _determine_bump_type(fragments)
+
+ if bump_type is None:
+ log.debug("No fragments found, cannot determine release version")
+ return None
+
+ log.info("Determined release version bump type from fragments: %s", bump_type)
+
+ # KEY DIFFERENCE: Use fmt="{guessed}" for clean version (no .devN)
+ if bump_type == "major":
+ return version.format_next_version(_guess_next_major, fmt="{guessed}")
+
+ elif bump_type == "minor":
+ return version.format_next_version(
+ guess_next_simple_semver, fmt="{guessed}", retain=SEMVER_MINOR
+ )
+
+ else: # patch
+ return version.format_next_version(
+ guess_next_simple_semver, fmt="{guessed}", retain=SEMVER_PATCH
+ )
diff --git a/vcs-versioning/src/vcs_versioning/overrides.py b/vcs-versioning/src/vcs_versioning/overrides.py
new file mode 100644
index 00000000..9b1e55e5
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/overrides.py
@@ -0,0 +1,719 @@
+"""
+Environment variable overrides API for VCS versioning.
+
+This module provides tools for managing environment variable overrides
+in a structured way, with support for custom tool prefixes and fallback
+to VCS_VERSIONING_* variables.
+
+Example usage:
+ >>> from vcs_versioning.overrides import GlobalOverrides
+ >>>
+ >>> # Apply overrides for the entire execution scope
+ >>> with GlobalOverrides.from_env("HATCH_VCS"):
+ >>> version = get_version(...)
+
+See the integrators documentation for more details.
+"""
+
+from __future__ import annotations
+
+import logging
+import os
+import warnings
+from collections.abc import Mapping, MutableMapping
+from contextlib import ContextDecorator
+from contextvars import ContextVar
+from dataclasses import dataclass
+from datetime import datetime
+from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, overload
+
+from packaging.utils import canonicalize_name
+
+from ._overrides import (
+ _find_close_env_var_matches,
+ _search_env_vars_with_prefix,
+)
+from ._toml import load_toml_or_inline_map
+
+# TypeVar for generic TypedDict support
+TSchema = TypeVar("TSchema", bound=TypedDict) # type: ignore[valid-type]
+
+if TYPE_CHECKING:
+ from pytest import MonkeyPatch
+
+log = logging.getLogger(__name__)
+
+
+class EnvReader:
+ """Helper class to read environment variables with tool prefix fallback.
+
+ This class provides a structured way to read environment variables by trying
+ multiple tool prefixes in order, with support for distribution-specific variants.
+
+ Attributes:
+ tools_names: Tuple of tool prefixes to try in order (e.g., ("HATCH_VCS", "VCS_VERSIONING"))
+ env: Environment mapping to read from
+ dist_name: Optional distribution name for dist-specific env vars
+
+ Example:
+ >>> reader = EnvReader(
+ ... tools_names=("HATCH_VCS", "VCS_VERSIONING"),
+ ... env=os.environ,
+ ... dist_name="my-package"
+ ... )
+ >>> debug_val = reader.read("DEBUG") # tries HATCH_VCS_DEBUG, then VCS_VERSIONING_DEBUG
+ >>> pretend = reader.read("PRETEND_VERSION") # tries dist-specific first, then generic
+ """
+
+ tools_names: tuple[str, ...]
+ env: Mapping[str, str]
+ dist_name: str | None
+
+ def __init__(
+ self,
+ tools_names: tuple[str, ...],
+ env: Mapping[str, str],
+ dist_name: str | None = None,
+ ):
+ """Initialize the EnvReader.
+
+ Args:
+ tools_names: Tuple of tool prefixes to try in order (e.g., ("HATCH_VCS", "VCS_VERSIONING"))
+ env: Environment mapping to read from
+ dist_name: Optional distribution name for dist-specific variables
+ """
+ if not tools_names:
+ raise TypeError("tools_names must be a non-empty tuple")
+ self.tools_names = tools_names
+ self.env = env
+ self.dist_name = dist_name
+
+ @overload
+ def read(self, name: str, *, split: str) -> list[str]: ...
+
+ @overload
+ def read(self, name: str, *, split: str, default: list[str]) -> list[str]: ...
+
+ @overload
+ def read(self, name: str, *, default: str) -> str: ...
+
+ @overload
+ def read(self, name: str) -> str | None: ...
+
+ def read(
+ self, name: str, *, split: str | None = None, default: Any = None
+ ) -> str | list[str] | None:
+ """Read a named environment variable, trying each tool in tools_names order.
+
+ If dist_name is provided, tries distribution-specific variants first
+ (e.g., TOOL_NAME_FOR_DIST), then falls back to generic variants (e.g., TOOL_NAME).
+
+ Also provides helpful diagnostics when similar environment variables are found
+ but don't match exactly (e.g., typos or incorrect normalizations in distribution names).
+
+ Args:
+ name: The environment variable name component (e.g., "DEBUG", "PRETEND_VERSION")
+ split: Optional separator to split the value by (e.g., os.pathsep for path lists)
+ default: Default value to return if not found (defaults to None)
+
+ Returns:
+ - If split is provided and value found: list[str] of split values
+ - If split is provided and not found: default value
+ - If split is None and value found: str value
+ - If split is None and not found: default value
+ """
+ # If dist_name is provided, try dist-specific variants first
+ found_value: str | None = None
+ if self.dist_name is not None:
+ canonical_dist_name = canonicalize_name(self.dist_name)
+ env_var_dist_name = canonical_dist_name.replace("-", "_").upper()
+
+ # Try each tool's dist-specific variant
+ for tool in self.tools_names:
+ expected_env_var = f"{tool}_{name}_FOR_{env_var_dist_name}"
+ val = self.env.get(expected_env_var)
+ if val is not None:
+ found_value = val
+ break
+
+ # Try generic versions for each tool
+ if found_value is None:
+ for tool in self.tools_names:
+ val = self.env.get(f"{tool}_{name}")
+ if val is not None:
+ found_value = val
+ break
+
+ # Not found - if dist_name is provided, check for common mistakes
+ if found_value is None and self.dist_name is not None:
+ canonical_dist_name = canonicalize_name(self.dist_name)
+ env_var_dist_name = canonical_dist_name.replace("-", "_").upper()
+
+ # Try each tool prefix for fuzzy matching
+ for tool in self.tools_names:
+ expected_env_var = f"{tool}_{name}_FOR_{env_var_dist_name}"
+ prefix = f"{tool}_{name}_FOR_"
+
+ # Search for alternative normalizations
+ matches = _search_env_vars_with_prefix(prefix, self.dist_name, self.env)
+ if matches:
+ env_var_name, value = matches[0]
+ log.warning(
+ "Found environment variable '%s' for dist name '%s', "
+ "but expected '%s'. Consider using the standard normalized name.",
+ env_var_name,
+ self.dist_name,
+ expected_env_var,
+ )
+ if len(matches) > 1:
+ other_vars = [var for var, _ in matches[1:]]
+ log.warning(
+ "Multiple alternative environment variables found: %s. Using '%s'.",
+ other_vars,
+ env_var_name,
+ )
+ found_value = value
+ break
+
+ # Search for close matches (potential typos)
+ close_matches = _find_close_env_var_matches(
+ prefix, env_var_dist_name, self.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,
+ self.dist_name,
+ canonical_dist_name,
+ close_matches,
+ )
+
+ # Process the found value or return default
+ if found_value is not None:
+ if split is not None:
+ # Split the value by the provided separator, filtering out empty strings
+ return [part for part in found_value.split(split) if part]
+ return found_value
+ # Return default, honoring the type based on split parameter
+ if split is not None:
+ # When split is provided, default should be a list
+ return default if default is not None else []
+ # For non-split case, default can be None or str
+ return default # type: ignore[no-any-return]
+
+ def read_toml(self, name: str, *, schema: type[TSchema]) -> TSchema:
+ """Read and parse a TOML-formatted environment variable.
+
+ This method is useful for reading structured configuration like:
+ - Config overrides (e.g., TOOL_OVERRIDES_FOR_DIST)
+ - ScmVersion metadata (e.g., TOOL_PRETEND_METADATA_FOR_DIST)
+
+ Supports both full TOML documents and inline TOML maps (starting with '{').
+
+ Args:
+ name: The environment variable name component (e.g., "OVERRIDES", "PRETEND_METADATA")
+ schema: TypedDict class for schema validation.
+ Invalid fields will be logged as warnings and removed.
+
+ Returns:
+ Parsed TOML data conforming to the schema type, or an empty dict if not found.
+ Raises InvalidTomlError if the TOML content is malformed.
+
+ Example:
+ >>> from typing import TypedDict
+ >>> class MySchema(TypedDict, total=False):
+ ... local_scheme: str
+ >>> reader = EnvReader(tools_names=("TOOL",), env={
+ ... "TOOL_OVERRIDES": '{"local_scheme": "no-local-version"}',
+ ... })
+ >>> result: MySchema = reader.read_toml("OVERRIDES", schema=MySchema)
+ >>> result["local_scheme"]
+ 'no-local-version'
+ """
+ data = self.read(name)
+ return load_toml_or_inline_map(data, schema=schema)
+
+
+@dataclass(frozen=True)
+class GlobalOverrides:
+ """Global environment variable overrides for VCS versioning.
+
+ Use as a context manager to apply overrides for the execution scope.
+ Logging is automatically configured when entering the context.
+
+ Attributes:
+ debug: Debug logging level (int from logging module) or False to disable
+ subprocess_timeout: Timeout for subprocess commands in seconds
+ hg_command: Command to use for Mercurial operations
+ source_date_epoch: Unix timestamp for reproducible builds (None if not set)
+ ignore_vcs_roots: List of VCS root paths to ignore for file finding
+ tool: Tool prefix used to read these overrides
+ dist_name: Optional distribution name for dist-specific env var lookups
+ additional_loggers: List of logger instances to configure alongside vcs_versioning
+
+ Usage:
+ with GlobalOverrides.from_env("HATCH_VCS", dist_name="my-package") as overrides:
+ # All modules now have access to these overrides
+ # Logging is automatically configured based on HATCH_VCS_DEBUG
+
+ # Read custom environment variables
+ custom_val = overrides.env_reader.read("MY_CUSTOM_VAR")
+
+ version = get_version(...)
+ """
+
+ debug: int | Literal[False]
+ subprocess_timeout: int
+ hg_command: str
+ source_date_epoch: int | None
+ ignore_vcs_roots: list[str]
+ tool: str
+ env_reader: EnvReader
+ dist_name: str | None = None
+ additional_loggers: tuple[logging.Logger, ...] = ()
+
+ def __post_init__(self) -> None:
+ """Validate that env_reader configuration matches GlobalOverrides settings."""
+ # Verify that the env_reader's dist_name matches
+ if self.env_reader.dist_name != self.dist_name:
+ raise ValueError(
+ f"EnvReader dist_name mismatch: "
+ f"GlobalOverrides has {self.dist_name!r}, "
+ f"but EnvReader has {self.env_reader.dist_name!r}"
+ )
+
+ # Verify that the env_reader has the correct tool prefix
+ expected_tools = (self.tool, "VCS_VERSIONING")
+ if self.env_reader.tools_names != expected_tools:
+ raise ValueError(
+ f"EnvReader tools_names mismatch: "
+ f"expected {expected_tools}, "
+ f"but got {self.env_reader.tools_names}"
+ )
+
+ @classmethod
+ def from_env(
+ cls,
+ tool: str,
+ env: Mapping[str, str] = os.environ,
+ dist_name: str | None = None,
+ additional_loggers: logging.Logger | list[logging.Logger] | tuple[()] = (),
+ ) -> GlobalOverrides:
+ """Read all global overrides from environment variables.
+
+ Checks both tool-specific prefix and VCS_VERSIONING prefix as fallback.
+
+ Args:
+ tool: Tool prefix (e.g., "HATCH_VCS", "SETUPTOOLS_SCM")
+ env: Environment dict to read from (defaults to os.environ)
+ dist_name: Optional distribution name for dist-specific env var lookups
+ additional_loggers: Logger instance(s) to configure alongside vcs_versioning.
+ Can be a single logger, a list of loggers, or empty tuple.
+
+ Returns:
+ GlobalOverrides instance ready to use as context manager
+ """
+
+ # Create EnvReader for reading environment variables with fallback
+ reader = EnvReader(
+ tools_names=(tool, "VCS_VERSIONING"), env=env, dist_name=dist_name
+ )
+
+ # Convert additional_loggers to a tuple of logger instances
+ logger_tuple: tuple[logging.Logger, ...]
+ if isinstance(additional_loggers, logging.Logger):
+ logger_tuple = (additional_loggers,)
+ elif isinstance(additional_loggers, list):
+ logger_tuple = tuple(additional_loggers)
+ else:
+ logger_tuple = ()
+
+ # Read debug flag - support multiple formats
+ debug_val = reader.read("DEBUG")
+ if debug_val is None:
+ debug: int | Literal[False] = False
+ else:
+ # Try to parse as integer log level
+ try:
+ parsed_int = int(debug_val)
+ # If it's a small integer (0, 1), treat as boolean flag
+ # Otherwise treat as explicit log level (10, 20, 30, etc.)
+ if parsed_int in (0, 1):
+ debug = logging.DEBUG if parsed_int else False
+ else:
+ debug = parsed_int
+ except ValueError:
+ # Not an integer - check if it's a level name (DEBUG, INFO, WARNING, etc.)
+ level_name = debug_val.upper()
+ level_value = getattr(logging, level_name, None)
+ if isinstance(level_value, int):
+ # Valid level name found
+ debug = level_value
+ else:
+ # Unknown value - treat as boolean flag (any non-empty value means DEBUG)
+ debug = logging.DEBUG
+
+ # Read subprocess timeout
+ timeout_val = reader.read("SUBPROCESS_TIMEOUT")
+ subprocess_timeout = 40 # default
+ if timeout_val is not None:
+ try:
+ subprocess_timeout = int(timeout_val)
+ except ValueError:
+ log.warning(
+ "Invalid SUBPROCESS_TIMEOUT value '%s', using default %d",
+ timeout_val,
+ subprocess_timeout,
+ )
+
+ # Read hg command
+ hg_command = reader.read("HG_COMMAND") or "hg"
+
+ # Read SOURCE_DATE_EPOCH (standard env var, no prefix)
+ source_date_epoch_val = env.get("SOURCE_DATE_EPOCH")
+ source_date_epoch: int | None = None
+ if source_date_epoch_val is not None:
+ try:
+ source_date_epoch = int(source_date_epoch_val)
+ except ValueError:
+ log.warning(
+ "Invalid SOURCE_DATE_EPOCH value '%s', ignoring",
+ source_date_epoch_val,
+ )
+
+ # Read ignore_vcs_roots - paths separated by os.pathsep
+ ignore_vcs_roots_raw = reader.read(
+ "IGNORE_VCS_ROOTS", split=os.pathsep, default=[]
+ )
+ ignore_vcs_roots = [os.path.normcase(p) for p in ignore_vcs_roots_raw]
+
+ return cls(
+ debug=debug,
+ subprocess_timeout=subprocess_timeout,
+ hg_command=hg_command,
+ source_date_epoch=source_date_epoch,
+ ignore_vcs_roots=ignore_vcs_roots,
+ tool=tool,
+ env_reader=reader,
+ dist_name=dist_name,
+ additional_loggers=logger_tuple,
+ )
+
+ def __enter__(self) -> GlobalOverrides:
+ """Enter context: set this as the active override and configure logging."""
+ token = _active_overrides.set(self)
+ # Store the token so we can restore in __exit__
+ object.__setattr__(self, "_token", token)
+
+ # Automatically configure logging using the log_level property
+ from ._log import _configure_loggers
+
+ _configure_loggers(
+ log_level=self.log_level(), additional_loggers=list(self.additional_loggers)
+ )
+
+ return self
+
+ def __exit__(self, *exc_info: Any) -> None:
+ """Exit context: restore previous override state."""
+ token = getattr(self, "_token", None)
+ if token is not None:
+ _active_overrides.reset(token)
+ object.__delattr__(self, "_token")
+
+ def log_level(self) -> int:
+ """Get the appropriate logging level from the debug setting.
+
+ Returns:
+ logging level constant (DEBUG, WARNING, etc.)
+ """
+ if self.debug is False:
+ return logging.WARNING
+ return self.debug
+
+ def source_epoch_or_utc_now(self) -> datetime:
+ """Get datetime from SOURCE_DATE_EPOCH or current UTC time.
+
+ Returns:
+ datetime object in UTC timezone
+ """
+ from datetime import datetime, timezone
+
+ if self.source_date_epoch is not None:
+ return datetime.fromtimestamp(self.source_date_epoch, timezone.utc)
+ else:
+ return datetime.now(timezone.utc)
+
+ @classmethod
+ def from_active(cls, **changes: Any) -> GlobalOverrides:
+ """Create a new GlobalOverrides instance based on the currently active one.
+
+ Uses dataclasses.replace() to create a modified copy of the active overrides.
+ If no overrides are currently active, raises a RuntimeError.
+
+ Args:
+ **changes: Fields to update in the new instance
+
+ Returns:
+ New GlobalOverrides instance with the specified changes
+
+ Raises:
+ RuntimeError: If no GlobalOverrides context is currently active
+
+ Example:
+ >>> with GlobalOverrides.from_env("TEST"):
+ ... # Create a modified version with different debug level
+ ... with GlobalOverrides.from_active(debug=logging.INFO):
+ ... # This context has INFO level instead
+ ... pass
+ """
+ from dataclasses import replace
+
+ active = _active_overrides.get()
+ if active is None:
+ raise RuntimeError(
+ "Cannot call from_active() without an active GlobalOverrides context. "
+ "Use from_env() to create the initial context."
+ )
+
+ # If dist_name or tool is being changed, create a new EnvReader with the updated settings
+ new_dist_name = changes.get("dist_name", active.dist_name)
+ new_tool = changes.get("tool", active.tool)
+
+ if ("dist_name" in changes and changes["dist_name"] != active.dist_name) or (
+ "tool" in changes and changes["tool"] != active.tool
+ ):
+ changes["env_reader"] = EnvReader(
+ tools_names=(new_tool, "VCS_VERSIONING"),
+ env=active.env_reader.env,
+ dist_name=new_dist_name,
+ )
+
+ return replace(active, **changes)
+
+ def export(self, target: MutableMapping[str, str] | MonkeyPatch) -> None:
+ """Export overrides to environment variables.
+
+ Can export to either a dict-like environment or a pytest monkeypatch fixture.
+ This is useful for tests that need to propagate overrides to subprocesses.
+
+ Args:
+ target: Either a MutableMapping (e.g., dict, os.environ) or a pytest
+ MonkeyPatch instance (or any object with a setenv method)
+
+ Example:
+ >>> # Export to environment dict
+ >>> overrides = GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "1"})
+ >>> env = {}
+ >>> overrides.export(env)
+ >>> print(env["TEST_DEBUG"])
+ 1
+
+ >>> # Export via pytest monkeypatch
+ >>> def test_something(monkeypatch):
+ ... overrides = GlobalOverrides.from_env("TEST")
+ ... overrides.export(monkeypatch)
+ ... # Environment is now set
+ """
+
+ # Helper to set variable based on target type
+ def set_var(key: str, value: str) -> None:
+ if isinstance(target, MutableMapping):
+ target[key] = value
+ else:
+ target.setenv(key, value)
+
+ # Export SOURCE_DATE_EPOCH
+ if self.source_date_epoch is not None:
+ set_var("SOURCE_DATE_EPOCH", str(self.source_date_epoch))
+
+ # Export tool-prefixed variables
+ prefix = self.tool
+
+ # Export debug
+ if self.debug is False:
+ set_var(f"{prefix}_DEBUG", "0")
+ else:
+ set_var(f"{prefix}_DEBUG", str(self.debug))
+
+ # Export subprocess timeout
+ set_var(f"{prefix}_SUBPROCESS_TIMEOUT", str(self.subprocess_timeout))
+
+ # Export hg command
+ set_var(f"{prefix}_HG_COMMAND", self.hg_command)
+
+
+# Thread-local storage for active global overrides
+_active_overrides: ContextVar[GlobalOverrides | None] = ContextVar(
+ "vcs_versioning_overrides", default=None
+)
+
+# Flag to track if we've already warned about auto-creating context
+_auto_create_warning_issued = False
+
+
+# Accessor functions for getting current override values
+
+
+def get_active_overrides() -> GlobalOverrides:
+ """Get the currently active GlobalOverrides instance.
+
+ If no context is active, creates one from the current environment
+ using SETUPTOOLS_SCM prefix for legacy compatibility.
+
+ Note: The auto-created instance reads from os.environ at call time,
+ so it will pick up environment changes (e.g., from pytest monkeypatch).
+
+ Returns:
+ GlobalOverrides instance
+ """
+ global _auto_create_warning_issued
+
+ overrides = _active_overrides.get()
+ if overrides is None:
+ # Auto-create context from environment for backwards compatibility
+ # Note: We create a fresh instance each time to pick up env changes
+ if not _auto_create_warning_issued:
+ warnings.warn(
+ "No GlobalOverrides context is active. "
+ "Auto-creating one with SETUPTOOLS_SCM prefix for backwards compatibility. "
+ "Consider using 'with GlobalOverrides.from_env(\"YOUR_TOOL\"):' explicitly.",
+ UserWarning,
+ stacklevel=2,
+ )
+ _auto_create_warning_issued = True
+ overrides = GlobalOverrides.from_env(
+ "SETUPTOOLS_SCM",
+ env=os.environ,
+ additional_loggers=logging.getLogger("setuptools_scm"),
+ )
+ return overrides
+
+
+def get_debug_level() -> int | Literal[False]:
+ """Get current debug level from active override context.
+
+ Returns:
+ logging level constant (DEBUG, INFO, WARNING, etc.) or False
+ """
+ return get_active_overrides().debug
+
+
+def get_subprocess_timeout() -> int:
+ """Get current subprocess timeout from active override context.
+
+ Returns:
+ Subprocess timeout in seconds
+ """
+ return get_active_overrides().subprocess_timeout
+
+
+def get_hg_command() -> str:
+ """Get current Mercurial command from active override context.
+
+ Returns:
+ Mercurial command string
+ """
+ return get_active_overrides().hg_command
+
+
+def get_source_date_epoch() -> int | None:
+ """Get SOURCE_DATE_EPOCH from active override context.
+
+ Returns:
+ Unix timestamp or None
+ """
+ return get_active_overrides().source_date_epoch
+
+
+def source_epoch_or_utc_now() -> datetime:
+ """Get datetime from SOURCE_DATE_EPOCH or current UTC time.
+
+ Uses the active GlobalOverrides context. If no SOURCE_DATE_EPOCH is set,
+ returns the current UTC time.
+
+ Returns:
+ datetime object in UTC timezone
+ """
+ return get_active_overrides().source_epoch_or_utc_now()
+
+
+class ensure_context(ContextDecorator):
+ """Context manager/decorator that ensures a GlobalOverrides context is active.
+
+ If no context is active, creates one using from_env() with the specified tool.
+ Can be used as a decorator or context manager.
+
+ Example as decorator:
+ @ensure_context("SETUPTOOLS_SCM", additional_loggers=logging.getLogger("setuptools_scm"))
+ def my_entry_point():
+ # Will automatically have context
+ pass
+
+ Example as context manager:
+ with ensure_context("SETUPTOOLS_SCM", additional_loggers=logging.getLogger("setuptools_scm")):
+ # Will have context here
+ pass
+ """
+
+ def __init__(
+ self,
+ tool: str,
+ *,
+ env: Mapping[str, str] | None = None,
+ dist_name: str | None = None,
+ additional_loggers: logging.Logger | list[logging.Logger] | tuple[()] = (),
+ ):
+ """Initialize the context ensurer.
+
+ Args:
+ tool: Tool name (e.g., "SETUPTOOLS_SCM", "vcs-versioning")
+ env: Environment variables to read from (defaults to os.environ)
+ dist_name: Optional distribution name
+ additional_loggers: Logger instance(s) to configure
+ """
+ self.tool = tool
+ self.env = env if env is not None else os.environ
+ self.dist_name = dist_name
+ self.additional_loggers = additional_loggers
+ self._context: GlobalOverrides | None = None
+ self._created_context = False
+
+ def __enter__(self) -> GlobalOverrides:
+ """Enter context: create GlobalOverrides if none is active."""
+ # Check if there's already an active context
+ existing = _active_overrides.get()
+
+ if existing is not None:
+ # Already have a context, just return it
+ self._created_context = False
+ return existing
+
+ # No context active, create one
+ self._created_context = True
+ self._context = GlobalOverrides.from_env(
+ self.tool,
+ env=self.env,
+ dist_name=self.dist_name,
+ additional_loggers=self.additional_loggers,
+ )
+ return self._context.__enter__()
+
+ def __exit__(self, *exc_info: Any) -> None:
+ """Exit context: only exit if we created the context."""
+ if self._created_context and self._context is not None:
+ self._context.__exit__(*exc_info)
+
+
+__all__ = [
+ "EnvReader",
+ "GlobalOverrides",
+ "ensure_context",
+ "get_active_overrides",
+ "get_debug_level",
+ "get_hg_command",
+ "get_source_date_epoch",
+ "get_subprocess_timeout",
+ "source_epoch_or_utc_now",
+]
diff --git a/vcs-versioning/src/vcs_versioning/test_api.py b/vcs-versioning/src/vcs_versioning/test_api.py
new file mode 100644
index 00000000..0b772f4f
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/test_api.py
@@ -0,0 +1,148 @@
+"""
+Pytest plugin and test API for vcs_versioning.
+
+This module can be used as a pytest plugin by adding to conftest.py:
+ pytest_plugins = ["vcs_versioning.test_api"]
+"""
+
+from __future__ import annotations
+
+import contextlib
+import os
+import shutil
+import sys
+from collections.abc import Iterator
+from datetime import datetime, timezone
+from pathlib import Path
+from types import TracebackType
+
+import pytest
+
+from ._run_cmd import run
+
+if sys.version_info >= (3, 11):
+ from typing import Self
+else:
+ from typing_extensions import Self
+
+# Re-export WorkDir from _test_utils module
+from ._test_utils import WorkDir
+
+__all__ = [
+ "TEST_SOURCE_DATE",
+ "TEST_SOURCE_DATE_EPOCH",
+ "TEST_SOURCE_DATE_FORMATTED",
+ "TEST_SOURCE_DATE_TIMESTAMP",
+ "DebugMode",
+ "WorkDir",
+ "debug_mode",
+ "hg_exe",
+ "repositories_hg_git",
+ "wd",
+]
+
+# Test time constants: 2009-02-13T23:31:30+00:00
+TEST_SOURCE_DATE = datetime(2009, 2, 13, 23, 31, 30, tzinfo=timezone.utc)
+TEST_SOURCE_DATE_EPOCH = int(TEST_SOURCE_DATE.timestamp())
+TEST_SOURCE_DATE_FORMATTED = "20090213" # As used in node-and-date local scheme
+TEST_SOURCE_DATE_TIMESTAMP = (
+ "20090213233130" # As used in node-and-timestamp local scheme
+)
+
+
+def pytest_configure(config: pytest.Config) -> None:
+ """Configure pytest for vcs_versioning tests."""
+ # 2009-02-13T23:31:30+00:00
+ os.environ["SOURCE_DATE_EPOCH"] = str(TEST_SOURCE_DATE_EPOCH)
+ os.environ["VCS_VERSIONING_DEBUG"] = "1"
+
+
+@pytest.fixture(scope="session", autouse=True)
+def _global_overrides_context() -> Iterator[None]:
+ """Automatically apply GlobalOverrides context for all tests.
+
+ This ensures that SOURCE_DATE_EPOCH and debug settings from pytest_configure
+ are properly picked up by the override system.
+ """
+ from .overrides import GlobalOverrides
+
+ # Use SETUPTOOLS_SCM prefix for backwards compatibility.
+ # EnvReader will also check VCS_VERSIONING as a fallback.
+ with GlobalOverrides.from_env("SETUPTOOLS_SCM"):
+ yield
+
+
+class DebugMode(contextlib.AbstractContextManager): # type: ignore[type-arg]
+ """Context manager to enable debug logging for tests."""
+
+ from . import _log as __module
+
+ def __init__(self) -> None:
+ self.__stack = contextlib.ExitStack()
+
+ def __enter__(self) -> Self:
+ self.enable()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self.disable()
+
+ def enable(self) -> None:
+ self.__stack.enter_context(self.__module.defer_to_pytest())
+
+ def disable(self) -> None:
+ self.__stack.close()
+
+
+@pytest.fixture(autouse=True)
+def debug_mode() -> Iterator[DebugMode]:
+ """Fixture to enable debug mode for all tests."""
+ with DebugMode() as debug_mode:
+ yield debug_mode
+
+
+@pytest.fixture
+def wd(tmp_path: Path) -> WorkDir:
+ """Base WorkDir fixture that returns an unconfigured working directory.
+
+ Individual test modules should override this fixture to set up specific SCM configurations.
+ """
+ target_wd = tmp_path.resolve() / "wd"
+ target_wd.mkdir()
+ return WorkDir(target_wd)
+
+
+@pytest.fixture(scope="session")
+def hg_exe() -> str:
+ """Fixture to get the hg executable path, skipping if not found."""
+ 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]:
+ """Fixture to create paired git and hg repositories for hg-git tests."""
+ tmp_path = tmp_path.resolve()
+ path_git = tmp_path / "repo_git"
+ path_git.mkdir()
+
+ wd = WorkDir(path_git).setup_git()
+
+ path_hg = tmp_path / "repo_hg"
+ run(["hg", "clone", path_git, path_hg, "--config", "extensions.hggit="], tmp_path)
+ assert path_hg.exists()
+
+ with open(path_hg / ".hg/hgrc", "a") as file:
+ file.write("[extensions]\nhggit =\n")
+
+ wd_hg = WorkDir(path_hg)
+ wd_hg.configure_hg_commands()
+
+ return wd_hg, wd
diff --git a/vcs-versioning/testing_vcs/__init__.py b/vcs-versioning/testing_vcs/__init__.py
new file mode 100644
index 00000000..66e3cb07
--- /dev/null
+++ b/vcs-versioning/testing_vcs/__init__.py
@@ -0,0 +1 @@
+"""Tests for vcs-versioning."""
diff --git a/vcs-versioning/testing_vcs/conftest.py b/vcs-versioning/testing_vcs/conftest.py
new file mode 100644
index 00000000..bc8b6a12
--- /dev/null
+++ b/vcs-versioning/testing_vcs/conftest.py
@@ -0,0 +1,10 @@
+"""Pytest configuration for vcs-versioning tests.
+
+Uses vcs_versioning.test_api as a pytest plugin.
+"""
+
+from __future__ import annotations
+
+# Use our own test_api module as a pytest plugin
+# Moved to pyproject.toml addopts to avoid non-top-level conftest issues
+# pytest_plugins = ["vcs_versioning.test_api"]
diff --git a/testing/test_better_root_errors.py b/vcs-versioning/testing_vcs/test_better_root_errors.py
similarity index 84%
rename from testing/test_better_root_errors.py
rename to vcs-versioning/testing_vcs/test_better_root_errors.py
index 0ba964cc..b8b809e3 100644
--- a/testing/test_better_root_errors.py
+++ b/vcs-versioning/testing_vcs/test_better_root_errors.py
@@ -9,39 +9,21 @@
from __future__ import annotations
import pytest
+from vcs_versioning import Configuration
+from vcs_versioning._get_version_impl import (
+ _find_scm_in_parents,
+ _version_missing,
+ get_version,
+)
+from vcs_versioning.test_api import WorkDir
-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")
+# No longer need to import setup functions - using WorkDir methods directly
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)
+ wd.setup_git()
# Create a subdirectory structure
subdir = wd.cwd / "subproject" / "nested"
@@ -57,7 +39,7 @@ def test_find_scm_in_parents_finds_git(wd: WorkDir) -> None:
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)
+ wd.setup_hg()
# Create a subdirectory structure
subdir = wd.cwd / "subproject" / "nested"
@@ -85,7 +67,7 @@ def test_find_scm_in_parents_returns_none(wd: WorkDir) -> 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)
+ wd.setup_git()
# Create a subdirectory structure
subdir = wd.cwd / "subproject" / "nested"
@@ -130,7 +112,7 @@ def test_version_missing_no_scm_found(wd: WorkDir) -> None:
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)
+ wd.setup_git()
# Create a subdirectory structure
subdir = wd.cwd / "subproject" / "nested"
@@ -161,7 +143,7 @@ def test_search_parent_directories_works_as_suggested(
) -> None:
"""Test that the suggested search_parent_directories=True solution actually works."""
# Set up git repo
- setup_git_repo(wd)
+ wd.setup_git()
wd.commit_testfile() # Make sure there's a commit for version detection
# Create a subdirectory
@@ -182,7 +164,7 @@ def test_integration_better_error_from_nested_directory(
) -> None:
"""Integration test: get_version from nested directory should give helpful error."""
# Set up git repo
- setup_git_repo(wd)
+ wd.setup_git()
# Create a subdirectory
subdir = wd.cwd / "subproject"
diff --git a/testing/test_compat.py b/vcs-versioning/testing_vcs/test_compat.py
similarity index 95%
rename from testing/test_compat.py
rename to vcs-versioning/testing_vcs/test_compat.py
index 3cd52771..85c8b4ba 100644
--- a/testing/test_compat.py
+++ b/vcs-versioning/testing_vcs/test_compat.py
@@ -3,9 +3,7 @@
from __future__ import annotations
import pytest
-
-from setuptools_scm._compat import normalize_path_for_assertion
-from setuptools_scm._compat import strip_path_suffix
+from vcs_versioning._compat import normalize_path_for_assertion, strip_path_suffix
def test_normalize_path_for_assertion() -> None:
diff --git a/vcs-versioning/testing_vcs/test_config.py b/vcs-versioning/testing_vcs/test_config.py
new file mode 100644
index 00000000..464c7c3f
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_config.py
@@ -0,0 +1,56 @@
+"""Tests for core Configuration functionality."""
+
+from __future__ import annotations
+
+import re
+
+import pytest
+from vcs_versioning import Configuration
+
+
+@pytest.mark.parametrize(
+ ("tag", "expected_version"),
+ [
+ ("apache-arrow-0.9.0", "0.9.0"),
+ ("arrow-0.9.0", "0.9.0"),
+ ("arrow-0.9.0-rc", "0.9.0-rc"),
+ ("arrow-1", "1"),
+ ("arrow-1+", "1"),
+ ("arrow-1+foo", "1"),
+ ("arrow-1.1+foo", "1.1"),
+ ("v1.1", "v1.1"),
+ ("V1.1", "V1.1"),
+ ],
+)
+def test_tag_regex(tag: str, expected_version: str) -> None:
+ config = Configuration()
+ match = config.tag_regex.match(tag)
+ assert match
+ version = match.group("version")
+ assert version == expected_version
+
+
+def test_config_regex_init() -> None:
+ tag_regex = re.compile(r"v(\d+)")
+ conf = Configuration(tag_regex=tag_regex)
+ assert conf.tag_regex is tag_regex
+
+
+@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/vcs-versioning/testing_vcs/test_expect_parse.py b/vcs-versioning/testing_vcs/test_expect_parse.py
new file mode 100644
index 00000000..b95acb90
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_expect_parse.py
@@ -0,0 +1,173 @@
+"""Test the expect_parse and matches functionality."""
+
+from __future__ import annotations
+
+from datetime import date, datetime, timezone
+from pathlib import Path
+
+import pytest
+from vcs_versioning import Configuration
+from vcs_versioning._scm_version import ScmVersion, meta, mismatches
+from vcs_versioning.test_api import TEST_SOURCE_DATE, WorkDir
+
+
+def test_scm_version_matches_basic() -> None:
+ """Test the ScmVersion.matches method with various combinations."""
+ c = Configuration()
+
+ # Create test version with all properties set
+ version = meta(
+ "1.2.3",
+ distance=5,
+ dirty=True,
+ node="abc123def456",
+ branch="main",
+ config=c,
+ )
+
+ # Test individual matches
+ assert version.matches(tag="1.2.3")
+ assert version.matches(distance=5)
+ assert version.matches(dirty=True)
+ assert version.matches(branch="main")
+ assert version.matches(exact=False)
+
+ # Test combined matches
+ assert version.matches(tag="1.2.3", distance=5, dirty=True, branch="main")
+
+ # Test node prefix matching
+ assert version.matches(node_prefix="a")
+ assert version.matches(node_prefix="abc")
+ assert version.matches(node_prefix="abc123")
+ assert version.matches(node_prefix="abc123def456")
+
+ # Test mismatches are falsy
+ assert not version.matches(tag="1.2.4")
+ assert not version.matches(node_prefix="xyz")
+ assert not version.matches(distance=10)
+ assert not version.matches(dirty=False)
+
+
+def test_scm_version_matches_exact() -> None:
+ """Test exact matching."""
+ c = Configuration()
+
+ # Exact version
+ exact_version = meta("2.0.0", distance=0, dirty=False, config=c)
+ assert exact_version.matches(exact=True)
+
+ # Non-exact due to distance
+ with_distance = meta("2.0.0", distance=1, dirty=False, config=c)
+ assert not with_distance.matches(exact=True)
+
+ # Non-exact due to dirty
+ dirty_version = meta("2.0.0", distance=0, dirty=True, config=c)
+ assert not dirty_version.matches(exact=True)
+
+
+def test_expect_parse_calls_matches(tmp_path: Path) -> None:
+ """Test that expect_parse correctly parses and calls matches."""
+ wd = WorkDir(tmp_path)
+
+ # Create a mock parse function that returns a predefined ScmVersion
+ c = Configuration()
+ mock_version = meta(
+ "3.0.0",
+ distance=2,
+ dirty=False,
+ node="fedcba987654",
+ branch="develop",
+ config=c,
+ )
+
+ def mock_parse(root: Path, config: Configuration) -> ScmVersion | None:
+ return mock_version
+
+ wd.parse = mock_parse
+
+ # Test successful match
+ wd.expect_parse(tag="3.0.0", distance=2, dirty=False)
+ wd.expect_parse(node_prefix="fed")
+ wd.expect_parse(branch="develop")
+
+ # Test that mismatches raise AssertionError
+ with pytest.raises(AssertionError, match="Version mismatch"):
+ wd.expect_parse(tag="3.0.1")
+
+ with pytest.raises(AssertionError, match="Version mismatch"):
+ wd.expect_parse(dirty=True)
+
+ with pytest.raises(AssertionError, match="Version mismatch"):
+ wd.expect_parse(node_prefix="abc")
+
+
+def test_expect_parse_without_parse_function(tmp_path: Path) -> None:
+ """Test that expect_parse raises error when parse is not configured."""
+ wd = WorkDir(tmp_path)
+
+ with pytest.raises(RuntimeError, match="No SCM configured"):
+ wd.expect_parse(tag="1.0.0")
+
+
+def test_expect_parse_with_none_result(tmp_path: Path) -> None:
+ """Test that expect_parse handles None result from parse."""
+ wd = WorkDir(tmp_path)
+
+ def mock_parse_none(root: Path, config: Configuration) -> ScmVersion | None:
+ return None
+
+ wd.parse = mock_parse_none
+
+ with pytest.raises(AssertionError, match="Failed to parse version"):
+ wd.expect_parse(tag="1.0.0")
+
+
+def test_missmatches_string_formatting() -> None:
+ """Test mismatches string representation for good error messages."""
+ mismatch_obj = mismatches(
+ expected={"tag": "1.0.0", "distance": 0, "dirty": False},
+ actual={"tag": "2.0.0", "distance": 5, "dirty": True},
+ )
+
+ # Test that mismatches is falsy
+ assert not mismatch_obj
+
+ # Test string representation
+ str_repr = str(mismatch_obj)
+ assert "tag: expected '1.0.0', got '2.0.0'" in str_repr
+ assert "distance: expected 0, got 5" in str_repr
+ assert "dirty: expected False, got True" in str_repr
+
+
+def test_missmatches_node_prefix_formatting() -> None:
+ """Test mismatches formatting for node prefix mismatches."""
+ mismatch_obj = mismatches(
+ expected={"node_prefix": "abc"},
+ actual={"node": "def123456"},
+ )
+
+ str_repr = str(mismatch_obj)
+ assert "node: expected prefix 'abc', got 'def123456'" in str_repr
+
+
+def test_scm_version_matches_datetime() -> None:
+ """Test that ScmVersion.matches works with datetime fields."""
+ c = Configuration()
+
+ # Create version with specific datetime
+ version = meta(
+ "1.0.0",
+ distance=0,
+ dirty=False,
+ node_date=date(2023, 6, 15),
+ time=TEST_SOURCE_DATE,
+ config=c,
+ )
+
+ # Test date matching
+ assert version.matches(node_date=date(2023, 6, 15))
+ assert not version.matches(node_date=date(2023, 6, 16))
+
+ # Test time matching
+ assert version.matches(time=TEST_SOURCE_DATE)
+ assert not version.matches(time=datetime(2023, 1, 1, tzinfo=timezone.utc))
diff --git a/testing/test_file_finder.py b/vcs-versioning/testing_vcs/test_file_finders.py
similarity index 89%
rename from testing/test_file_finder.py
rename to vcs-versioning/testing_vcs/test_file_finders.py
index 7d31e7d1..7b51e76c 100644
--- a/testing/test_file_finder.py
+++ b/vcs-versioning/testing_vcs/test_file_finders.py
@@ -2,14 +2,11 @@
import os
import sys
-
-from typing import Iterable
+from collections.abc import Iterable
import pytest
-
-from setuptools_scm._file_finders import find_files
-
-from .wd_wrapper import WorkDir
+from vcs_versioning._file_finders import find_files
+from vcs_versioning.test_api import WorkDir
@pytest.fixture(params=["git", "hg"])
@@ -18,21 +15,9 @@ def inwd(
) -> WorkDir:
param: str = request.param # type: ignore[attr-defined]
if param == "git":
- try:
- wd("git init")
- except OSError:
- pytest.skip("git executable not found")
- 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}"
+ wd.setup_git(monkeypatch)
elif param == "hg":
- try:
- wd("hg init")
- except OSError:
- pytest.skip("hg executable not found")
- wd.add_command = "hg add ."
- wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"'
+ wd.setup_hg()
(wd.cwd / "file1").touch()
adir = wd.cwd / "adir"
adir.mkdir()
@@ -249,10 +234,7 @@ def test_archive(
@pytest.fixture
def hg_wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> WorkDir:
- try:
- wd("hg init")
- except OSError:
- pytest.skip("hg executable not found")
+ wd.setup_hg()
(wd.cwd / "file").touch()
wd("hg add file")
monkeypatch.chdir(wd.cwd)
@@ -270,8 +252,8 @@ def test_hg_command_from_env(
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
+ from vcs_versioning.overrides import GlobalOverrides
+
+ monkeypatch.setenv("PATH", str(hg_wd.cwd / "not-existing"))
+ with GlobalOverrides.from_active(hg_command=hg_exe):
assert set(find_files()) == {"file"}
diff --git a/testing/test_git.py b/vcs-versioning/testing_vcs/test_git.py
similarity index 88%
rename from testing/test_git.py
rename to vcs-versioning/testing_vcs/test_git.py
index 31cac7a3..6e50b7f3 100644
--- a/testing/test_git.py
+++ b/vcs-versioning/testing_vcs/test_git.py
@@ -5,56 +5,42 @@
import shutil
import subprocess
import sys
-
-from datetime import date
-from datetime import datetime
-from datetime import timezone
+from collections.abc import Generator
+from datetime import date, datetime, timezone
from os.path import join as opj
from pathlib import Path
from textwrap import dedent
-from typing import Generator
-from unittest.mock import Mock
-from unittest.mock import patch
+from unittest.mock import Mock, patch
import pytest
-import setuptools_scm._file_finders
-
-from setuptools_scm import Configuration
-from setuptools_scm import NonNormalizedVersion
-from setuptools_scm import git
-from setuptools_scm._file_finders.git import git_find_files
-from setuptools_scm._run_cmd import CommandNotFoundError
-from setuptools_scm._run_cmd import CompletedProcess
-from setuptools_scm._run_cmd import has_command
-from setuptools_scm._run_cmd import run
-from setuptools_scm.git import archival_to_version
-from setuptools_scm.version import format_version
-
-from .conftest import DebugMode
-from .wd_wrapper import WorkDir
-
-pytestmark = pytest.mark.skipif(
- not has_command("git", warn=False), reason="git executable not found"
+# File finder imports (now in vcs_versioning)
+import vcs_versioning._file_finders # noqa: F401
+from vcs_versioning import Configuration
+from vcs_versioning._backends import _git
+from vcs_versioning._file_finders._git import git_find_files
+from vcs_versioning._run_cmd import (
+ CommandNotFoundError,
+ CompletedProcess,
+ has_command,
+ run,
)
+from vcs_versioning._version_cls import NonNormalizedVersion
+from vcs_versioning._version_schemes import format_version
+from vcs_versioning.test_api import DebugMode, WorkDir
+# Use vcs_versioning git backend directly
+git = _git
+archival_to_version = _git.archival_to_version
-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
+# Note: Git availability is now checked in WorkDir.setup_git() method
-@pytest.fixture(name="wd")
+@pytest.fixture
def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> WorkDir:
+ """Set up git for git-specific tests."""
debug_mode.disable()
- setup_git_wd(wd, monkeypatch)
+ wd.setup_git(monkeypatch)
debug_mode.enable()
return wd
@@ -69,12 +55,12 @@ def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> W
def test_parse_describe_output(
given: str, tag: str, number: int, node: str, dirty: bool
) -> None:
- parsed = git._git_parse_describe(given)
+ parsed = _git._git_parse_describe(given)
assert parsed == (tag, number, node, dirty)
def test_root_relative_to(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
+ monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG", raising=False)
p = wd.cwd.joinpath("sub/package")
p.mkdir(parents=True)
p.joinpath("setup.py").write_text(
@@ -91,7 +77,7 @@ def test_root_relative_to(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
def test_root_search_parent_directories(
wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
- monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
+ monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG", raising=False)
p = wd.cwd.joinpath("sub/package")
p.mkdir(parents=True)
p.joinpath("setup.py").write_text(
@@ -107,7 +93,7 @@ def test_root_search_parent_directories(
def test_git_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("PATH", str(wd.cwd / "not-existing"))
- wd.write("pyproject.toml", "[tool.setuptools_scm]")
+ wd.write("pyproject.toml", "[tool.vcs-versioning]")
with pytest.raises(CommandNotFoundError, match=r"git"):
git.parse(wd.cwd, Configuration(), git.DEFAULT_DESCRIBE)
@@ -167,34 +153,34 @@ def test_not_owner(wd: WorkDir) -> None:
def test_version_from_git(wd: WorkDir) -> None:
- assert wd.get_version() == "0.1.dev0+d20090213"
+ # No commits yet - fallback version
+ wd.expect_parse(tag="0.0", distance=0, dirty=True)
parsed = git.parse(str(wd.cwd), Configuration(), git.DEFAULT_DESCRIBE)
assert parsed is not None
assert parsed.branch in ("master", "main")
wd.commit_testfile()
- assert wd.get_version().startswith("0.1.dev1+g")
- assert not wd.get_version().endswith("1-")
+ wd.expect_parse(tag="0.0", distance=1, dirty=False, node_prefix="g")
wd("git tag v0.1")
- assert wd.get_version() == "0.1"
+ wd.expect_parse(tag="0.1", distance=0, dirty=False, exact=True)
wd.write("test.txt", "test2")
- assert wd.get_version().startswith("0.2.dev0+g")
+ wd.expect_parse(tag="0.1", distance=0, dirty=True)
wd.commit_testfile()
- assert wd.get_version().startswith("0.2.dev1+g")
+ wd.expect_parse(tag="0.1", distance=1, dirty=False, node_prefix="g")
wd("git tag version-0.2")
- assert wd.get_version().startswith("0.2")
+ wd.expect_parse(tag="0.2", distance=0, dirty=False, exact=True)
wd.commit_testfile()
wd("git tag version-0.2.post210+gbe48adfpost3+g0cc25f2")
with pytest.warns(
UserWarning, match="tag '.*' will be stripped of its suffix '.*'"
):
- assert wd.get_version().startswith("0.2")
+ wd.expect_parse(tag="0.2.post210", distance=0, dirty=False)
wd.commit_testfile()
wd("git tag 17.33.0-rc")
@@ -274,7 +260,6 @@ def test_git_version_unnormalized_setuptools(
assert wd.cwd.joinpath("VERSION.txt").read_text(encoding="utf-8") == "17.33.0-rc1"
-@pytest.mark.issue(179)
def test_unicode_version_scheme(wd: WorkDir) -> None:
scheme = b"guess-next-dev".decode("ascii")
assert wd.get_version(version_scheme=scheme)
@@ -285,10 +270,12 @@ def test_unicode_version_scheme(wd: WorkDir) -> None:
def test_git_worktree(wd: WorkDir) -> None:
wd.write("test.txt", "test2")
# untracked files dont change the state
- assert wd.get_version() == "0.1.dev0+d20090213"
+ # No commits yet, so it's dirty and tag is 0.0
+ wd.expect_parse(tag="0.0", distance=0, dirty=True)
wd("git add test.txt")
- assert wd.get_version().startswith("0.1.dev0+d")
+ # Still no commits, but now we have staged changes
+ wd.expect_parse(tag="0.0", distance=0, dirty=True)
@pytest.mark.issue(86)
@@ -296,18 +283,23 @@ def test_git_worktree(wd: WorkDir) -> None:
def test_git_dirty_notag(
today: bool, wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
- if today:
- monkeypatch.delenv("SOURCE_DATE_EPOCH", raising=False)
+ from vcs_versioning.overrides import GlobalOverrides
+
wd.commit_testfile()
wd.write("test.txt", "test2")
wd("git add test.txt")
- version = wd.get_version()
if today:
- # the date on the tag is in UTC
- tag = datetime.now(timezone.utc).date().strftime(".d%Y%m%d")
+ # Use from_active() to create overrides without SOURCE_DATE_EPOCH
+ with GlobalOverrides.from_active(source_date_epoch=None):
+ version = wd.get_version()
+ # the date on the tag is in UTC
+ tag = datetime.now(timezone.utc).date().strftime(".d%Y%m%d")
else:
+ # Use the existing context with SOURCE_DATE_EPOCH set
+ version = wd.get_version()
tag = ".d20090213"
+
assert version.startswith("0.1.dev1+g")
assert version.endswith(tag)
@@ -362,7 +354,7 @@ def test_find_files_stop_at_root_git(wd: WorkDir) -> None:
project = wd.cwd / "project"
project.mkdir()
project.joinpath("setup.cfg").touch()
- assert setuptools_scm._file_finders.find_files(str(project)) == []
+ assert vcs_versioning._file_finders.find_files(str(project)) == []
@pytest.mark.issue(128)
@@ -374,7 +366,8 @@ def test_parse_no_worktree(tmp_path: Path) -> None:
def test_alphanumeric_tags_match(wd: WorkDir) -> None:
wd.commit_testfile()
wd("git tag newstyle-development-started")
- assert wd.get_version().startswith("0.1.dev1+g")
+ # Non-version tag should not be recognized, so we're still at distance 1 from 0.0
+ wd.expect_parse(tag="0.0", distance=1, dirty=False, node_prefix="g")
def test_git_archive_export_ignore(
@@ -391,7 +384,7 @@ def test_git_archive_export_ignore(
wd("git add test1.txt test2.txt")
wd.commit()
monkeypatch.chdir(wd.cwd)
- assert setuptools_scm._file_finders.find_files(".") == [opj(".", "test1.txt")]
+ assert vcs_versioning._file_finders.find_files(".") == [opj(".", "test1.txt")]
@pytest.mark.issue(228)
@@ -401,7 +394,7 @@ def test_git_archive_subdirectory(wd: WorkDir, monkeypatch: pytest.MonkeyPatch)
wd("git add foobar")
wd.commit()
monkeypatch.chdir(wd.cwd)
- assert setuptools_scm._file_finders.find_files(".") == [
+ assert vcs_versioning._file_finders.find_files(".") == [
opj(".", "foobar", "test1.txt")
]
@@ -415,7 +408,7 @@ def test_git_archive_run_from_subdirectory(
wd("git add foobar")
wd.commit()
monkeypatch.chdir(wd.cwd / "foobar")
- assert setuptools_scm._file_finders.find_files(".") == [opj(".", "test1.txt")]
+ assert vcs_versioning._file_finders.find_files(".") == [opj(".", "test1.txt")]
@pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/728")
@@ -458,13 +451,16 @@ def test_non_dotted_version(wd: WorkDir) -> None:
wd.commit_testfile()
wd("git tag apache-arrow-1")
wd.commit_testfile()
- assert wd.get_version().startswith("2")
+ # Tag is recognized as version 1, so next commit is distance 1 from it
+ wd.expect_parse(tag="1", distance=1, dirty=False, node_prefix="g")
def test_non_dotted_version_with_updated_regex(wd: WorkDir) -> None:
wd.commit_testfile()
wd("git tag apache-arrow-1")
wd.commit_testfile()
+ # With custom regex, tag is still recognized as version 1
+ # This test is really about the regex configuration, so keep using get_version
assert wd.get_version(tag_regex=r"^apache-arrow-([\.0-9]+)$").startswith("2")
@@ -474,7 +470,8 @@ def test_non_dotted_tag_no_version_match(wd: WorkDir) -> None:
wd.commit_testfile()
wd("git tag apache-arrow")
wd.commit_testfile()
- assert wd.get_version().startswith("0.11.2.dev2")
+ # "apache-arrow" tag without version is not recognized, so we're at distance 2 from 0.11.1
+ wd.expect_parse(tag="0.11.1", distance=2, dirty=False, node_prefix="g")
@pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/381")
@@ -513,7 +510,7 @@ def test_git_getdate_badgit(
git_wd = git.GitWorkdir(wd.cwd)
fake_date_result = CompletedProcess(args=[], stdout="%cI", stderr="", returncode=0)
with patch.object(
- git,
+ _git,
"run_git",
Mock(return_value=fake_date_result),
):
@@ -529,7 +526,7 @@ def test_git_getdate_git_2_45_0_plus(
args=[], stdout="2024-04-30T22:33:10Z", stderr="", returncode=0
)
with patch.object(
- git,
+ _git,
"run_git",
Mock(return_value=fake_date_result),
):
@@ -685,7 +682,7 @@ def test_fail_on_missing_submodules_with_uninitialized_submodules(
# 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))
+ test_wd = WorkDir(test_repo).setup_git()
# Create a fake .gitmodules file (this simulates what happens after cloning without --recurse-submodules)
gitmodules_content = """[submodule "external"]
@@ -712,8 +709,7 @@ def test_git_pre_parse_config_integration(wd: WorkDir) -> None:
assert result is not None
# Test with explicit configuration
- from setuptools_scm._config import GitConfiguration
- from setuptools_scm._config import ScmConfiguration
+ from vcs_versioning._config import GitConfiguration, ScmConfiguration
config_with_pre_parse = Configuration(
scm=ScmConfiguration(
@@ -819,8 +815,7 @@ def test_git_describe_command_init_argument_deprecation() -> None:
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
+ from vcs_versioning._config import GitConfiguration, ScmConfiguration
# Both old init arg and new configuration specified - should raise ValueError
with pytest.warns(DeprecationWarning, match=r"git_describe_command.*deprecated"):
@@ -839,9 +834,7 @@ 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"')
+ wd.setup_git()
# Test with fallback_version set - should use the fallback instead of "0.0"
config = Configuration(fallback_version="1.2.3")
diff --git a/testing/test_hg_git.py b/vcs-versioning/testing_vcs/test_hg_git.py
similarity index 56%
rename from testing/test_hg_git.py
rename to vcs-versioning/testing_vcs/test_hg_git.py
index 1c9101a6..57864ed5 100644
--- a/testing/test_hg_git.py
+++ b/vcs-versioning/testing_vcs/test_hg_git.py
@@ -1,13 +1,10 @@
from __future__ import annotations
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
+from vcs_versioning import Configuration
+from vcs_versioning._backends._hg import parse
+from vcs_versioning._run_cmd import CommandNotFoundError, has_command, run
+from vcs_versioning.test_api import WorkDir
@pytest.fixture(scope="module", autouse=True)
@@ -29,42 +26,52 @@ def _check_hg_git() -> None:
def test_base(repositories_hg_git: tuple[WorkDir, WorkDir]) -> None:
wd, wd_git = repositories_hg_git
- assert wd_git.get_version() == "0.1.dev0+d20090213"
- assert wd.get_version() == "0.1.dev0+d20090213"
+ # Both should parse the same initial state
+ wd_git.expect_parse(tag="0.0", distance=0, dirty=True)
+ wd.expect_parse(tag="0.0", distance=0, dirty=True)
+
+ # Also verify they produce the same formatted output
+ assert wd_git.get_version() == wd.get_version()
wd_git.commit_testfile()
version_git = wd_git.get_version()
wd("hg pull -u")
-
version = wd.get_version()
+ # Both should parse the same after commit
+ wd_git.expect_parse(tag="0.0", distance=1, dirty=False, node_prefix="g")
+ wd.expect_parse(tag="0.0", distance=1, dirty=False, node_prefix="g")
+
+ # Check formatted output is similar (starts with same prefix)
assert version_git.startswith("0.1.dev1+g")
assert version.startswith("0.1.dev1+g")
- assert not version_git.endswith("1-")
- assert not version.endswith("1-")
-
wd_git("git tag v0.1")
wd("hg pull -u")
- assert wd_git.get_version() == "0.1"
- assert wd.get_version() == "0.1"
+
+ # Both should recognize the tag
+ wd_git.expect_parse(tag="0.1", distance=0, dirty=False, exact=True)
+ wd.expect_parse(tag="0.1", distance=0, dirty=False, exact=True)
wd_git.write("test.txt", "test2")
wd.write("test.txt", "test2")
- assert wd_git.get_version().startswith("0.2.dev0+g")
- assert wd.get_version().startswith("0.2.dev0+g")
+ # Both should be dirty
+ wd_git.expect_parse(tag="0.1", distance=0, dirty=True)
+ wd.expect_parse(tag="0.1", distance=0, dirty=True)
wd_git.commit_testfile()
wd("hg pull")
wd("hg up -C")
- assert wd_git.get_version().startswith("0.2.dev1+g")
- assert wd.get_version().startswith("0.2.dev1+g")
+ # Both should be at distance 1 from 0.1
+ wd_git.expect_parse(tag="0.1", distance=1, dirty=False, node_prefix="g")
+ wd.expect_parse(tag="0.1", distance=1, dirty=False, node_prefix="g")
wd_git("git tag version-0.2")
wd("hg pull -u")
- assert wd_git.get_version().startswith("0.2")
- assert wd.get_version().startswith("0.2")
+ # Both should recognize the new tag
+ wd_git.expect_parse(tag="0.2", distance=0, dirty=False, exact=True)
+ wd.expect_parse(tag="0.2", distance=0, dirty=False, exact=True)
wd_git.commit_testfile()
wd_git("git tag version-0.2.post210+gbe48adfpost3+g0cc25f2")
@@ -72,12 +79,12 @@ def test_base(repositories_hg_git: tuple[WorkDir, WorkDir]) -> None:
with pytest.warns(
UserWarning, match="tag '.*' will be stripped of its suffix '.*'"
):
- assert wd_git.get_version().startswith("0.2")
+ wd_git.expect_parse(tag="0.2.post210", distance=0, dirty=False)
with pytest.warns(
UserWarning, match="tag '.*' will be stripped of its suffix '.*'"
):
- assert wd.get_version().startswith("0.2")
+ wd.expect_parse(tag="0.2.post210", distance=0, dirty=False)
wd_git.commit_testfile()
wd_git("git tag 17.33.0-rc")
@@ -105,10 +112,11 @@ def test_hg_command_from_env(
request: pytest.FixtureRequest,
hg_exe: str,
) -> None:
+ from vcs_versioning.overrides import GlobalOverrides
+
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]")
+ wd.write("pyproject.toml", "[tool.vcs-versioning]")
+
+ monkeypatch.setenv("PATH", str(wd.cwd / "not-existing"))
+ with GlobalOverrides.from_active(hg_command=hg_exe):
assert wd.get_version().startswith("0.1.dev0+")
diff --git a/vcs-versioning/testing_vcs/test_integrator_helpers.py b/vcs-versioning/testing_vcs/test_integrator_helpers.py
new file mode 100644
index 00000000..b7da315c
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_integrator_helpers.py
@@ -0,0 +1,552 @@
+"""Tests for integrator helper API."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+from vcs_versioning import PyProjectData, build_configuration_from_pyproject
+from vcs_versioning._integrator_helpers import (
+ build_configuration_from_pyproject_internal,
+)
+
+
+class TestPyProjectDataFromFile:
+ """Test PyProjectData.from_file() public API."""
+
+ def test_from_file_reads_vcs_versioning(self, tmp_path: Path) -> None:
+ """Public API reads vcs-versioning section by default."""
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(
+ """
+[tool.vcs-versioning]
+version_scheme = "guess-next-dev"
+local_scheme = "no-local-version"
+"""
+ )
+
+ data = PyProjectData.from_file(pyproject)
+
+ assert data.tool_name == "vcs-versioning"
+ assert data.section_present is True
+ assert data.section["version_scheme"] == "guess-next-dev"
+ assert data.section["local_scheme"] == "no-local-version"
+
+ def test_from_file_ignores_setuptools_scm_by_default(self, tmp_path: Path) -> None:
+ """Public API ignores setuptools_scm section without internal parameter."""
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(
+ """
+[tool.setuptools_scm]
+version_scheme = "guess-next-dev"
+"""
+ )
+
+ # Public API doesn't read setuptools_scm section
+ data = PyProjectData.from_file(pyproject)
+ assert data.section_present is False
+
+ def test_from_file_internal_multi_tool_support(self, tmp_path: Path) -> None:
+ """Internal _tool_names parameter supports multiple tools."""
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(
+ """
+[tool.setuptools_scm]
+version_scheme = "guess-next-dev"
+"""
+ )
+
+ # Internal API can use _tool_names
+ data = PyProjectData.from_file(
+ pyproject,
+ _tool_names=["setuptools_scm", "vcs-versioning"],
+ )
+
+ assert data.tool_name == "setuptools_scm"
+ assert data.section_present is True
+ assert data.section["version_scheme"] == "guess-next-dev"
+
+ def test_from_file_internal_tries_in_order(self, tmp_path: Path) -> None:
+ """Internal API tries tool names in order."""
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(
+ """
+[tool.vcs-versioning]
+local_scheme = "no-local-version"
+
+[tool.setuptools_scm]
+local_scheme = "node-and-date"
+"""
+ )
+
+ # First tool name wins
+ data = PyProjectData.from_file(
+ pyproject,
+ _tool_names=["setuptools_scm", "vcs-versioning"],
+ )
+ assert data.tool_name == "setuptools_scm"
+ assert data.section["local_scheme"] == "node-and-date"
+
+ # Order matters
+ data2 = PyProjectData.from_file(
+ pyproject,
+ _tool_names=["vcs-versioning", "setuptools_scm"],
+ )
+ assert data2.tool_name == "vcs-versioning"
+ assert data2.section["local_scheme"] == "no-local-version"
+
+
+class TestManualPyProjectComposition:
+ """Test manual PyProjectData composition by integrators."""
+
+ def test_manual_composition_basic(self) -> None:
+ """Integrators can manually compose PyProjectData."""
+ pyproject = PyProjectData(
+ path=Path("pyproject.toml"),
+ tool_name="vcs-versioning",
+ project={"name": "my-pkg"},
+ section={"local_scheme": "no-local-version"},
+ is_required=True,
+ section_present=True,
+ project_present=True,
+ build_requires=["vcs-versioning"],
+ definition={},
+ )
+
+ assert pyproject.tool_name == "vcs-versioning"
+ assert pyproject.project_name == "my-pkg"
+ assert pyproject.section["local_scheme"] == "no-local-version"
+
+ def test_manual_composition_with_config_builder(self) -> None:
+ """Manual composition works with config builder."""
+ pyproject = PyProjectData(
+ path=Path("pyproject.toml"),
+ tool_name="vcs-versioning",
+ project={"name": "test-pkg"},
+ section={"version_scheme": "guess-next-dev"},
+ is_required=False,
+ section_present=True,
+ project_present=True,
+ build_requires=[],
+ definition={},
+ )
+
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name="test-pkg",
+ )
+
+ assert config.dist_name == "test-pkg"
+ assert config.version_scheme == "guess-next-dev"
+
+
+class TestBuildConfigurationFromPyProject:
+ """Test build_configuration_from_pyproject() function."""
+
+ def test_build_configuration_basic(self, tmp_path: Path) -> None:
+ """Basic configuration building from pyproject data."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-package"
+
+[tool.vcs-versioning]
+version_scheme = "guess-next-dev"
+local_scheme = "no-local-version"
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ )
+
+ assert config.dist_name == "test-package"
+ assert config.version_scheme == "guess-next-dev"
+ assert config.local_scheme == "no-local-version"
+
+ def test_build_configuration_with_dist_name_override(self, tmp_path: Path) -> None:
+ """dist_name argument overrides project name."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "wrong-name"
+
+[tool.vcs-versioning]
+version_scheme = "guess-next-dev"
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name="correct-name",
+ )
+
+ assert config.dist_name == "correct-name"
+
+ def test_build_configuration_with_integrator_overrides(
+ self, tmp_path: Path
+ ) -> None:
+ """Integrator overrides override config file."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+version_scheme = "guess-next-dev"
+local_scheme = "node-and-date"
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ # Integrator overrides
+ local_scheme="no-local-version",
+ version_scheme="release-branch-semver",
+ )
+
+ # Integrator overrides win over config file
+ assert config.local_scheme == "no-local-version"
+ assert config.version_scheme == "release-branch-semver"
+
+ def test_build_configuration_with_env_overrides(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Env overrides win over integrator overrides."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+local_scheme = "node-and-date"
+"""
+ )
+
+ # Set environment TOML override
+ monkeypatch.setenv(
+ "VCS_VERSIONING_OVERRIDES",
+ '{local_scheme = "no-local-version"}',
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ # Integrator tries to override, but env wins
+ local_scheme="dirty-tag",
+ )
+
+ # Env override wins
+ assert config.local_scheme == "no-local-version"
+
+
+class TestOverridePriorityOrder:
+ """Test complete priority order: env > integrator > config > defaults."""
+
+ def test_priority_defaults_only(self, tmp_path: Path) -> None:
+ """When nothing is set, use defaults."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(pyproject_data=pyproject)
+
+ # Default values
+ from vcs_versioning import DEFAULT_LOCAL_SCHEME, DEFAULT_VERSION_SCHEME
+
+ assert config.local_scheme == DEFAULT_LOCAL_SCHEME
+ assert config.version_scheme == DEFAULT_VERSION_SCHEME
+
+ def test_priority_config_over_defaults(self, tmp_path: Path) -> None:
+ """Config file overrides defaults."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+local_scheme = "node-and-date"
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(pyproject_data=pyproject)
+
+ assert config.local_scheme == "node-and-date"
+
+ def test_priority_integrator_over_config(self, tmp_path: Path) -> None:
+ """Integrator overrides override config file."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+local_scheme = "node-and-date"
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ local_scheme="no-local-version",
+ )
+
+ assert config.local_scheme == "no-local-version"
+
+ def test_priority_env_over_integrator(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Environment overrides win over integrator overrides."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+local_scheme = "node-and-date"
+"""
+ )
+
+ monkeypatch.setenv(
+ "VCS_VERSIONING_OVERRIDES",
+ '{local_scheme = "dirty-tag"}',
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ local_scheme="no-local-version",
+ )
+
+ # Env wins over everything
+ assert config.local_scheme == "dirty-tag"
+
+ def test_priority_complete_chain(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Test complete priority chain with all levels."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+local_scheme = "node-and-date"
+version_scheme = "guess-next-dev"
+"""
+ )
+
+ # Env only overrides local_scheme
+ monkeypatch.setenv(
+ "VCS_VERSIONING_OVERRIDES",
+ '{local_scheme = "dirty-tag"}',
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ # Integrator overrides both
+ local_scheme="no-local-version",
+ version_scheme="release-branch-semver",
+ )
+
+ # local_scheme: env wins (dirty-tag)
+ # version_scheme: integrator wins (no env override)
+ assert config.local_scheme == "dirty-tag"
+ assert config.version_scheme == "release-branch-semver"
+
+
+class TestInternalAPIMultiTool:
+ """Test internal API for setuptools_scm transition."""
+
+ def test_internal_build_configuration_multi_tool(self, tmp_path: Path) -> None:
+ """Internal API supports multiple tool names."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.setuptools_scm]
+local_scheme = "no-local-version"
+"""
+ )
+
+ # Internal API can load setuptools_scm section
+ pyproject = PyProjectData.from_file(
+ pyproject_file,
+ _tool_names=["setuptools_scm", "vcs-versioning"],
+ )
+
+ # Internal helper can build configuration from it
+ config = build_configuration_from_pyproject_internal(
+ pyproject_data=pyproject,
+ dist_name="test-pkg",
+ )
+
+ assert config.local_scheme == "no-local-version"
+ assert config.dist_name == "test-pkg"
+
+ def test_internal_prefers_first_tool_name(self, tmp_path: Path) -> None:
+ """Internal API uses first available tool name."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[tool.setuptools_scm]
+local_scheme = "setuptools-value"
+
+[tool.vcs-versioning]
+local_scheme = "vcs-value"
+"""
+ )
+
+ # setuptools_scm first
+ pyproject1 = PyProjectData.from_file(
+ pyproject_file,
+ _tool_names=["setuptools_scm", "vcs-versioning"],
+ )
+ config1 = build_configuration_from_pyproject_internal(pyproject_data=pyproject1)
+ assert config1.local_scheme == "setuptools-value"
+
+ # vcs-versioning first
+ pyproject2 = PyProjectData.from_file(
+ pyproject_file,
+ _tool_names=["vcs-versioning", "setuptools_scm"],
+ )
+ config2 = build_configuration_from_pyproject_internal(pyproject_data=pyproject2)
+ assert config2.local_scheme == "vcs-value"
+
+
+class TestDistNameResolution:
+ """Test dist_name resolution in different scenarios."""
+
+ def test_dist_name_from_argument(self, tmp_path: Path) -> None:
+ """Explicit dist_name argument has highest priority."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "project-name"
+
+[tool.vcs-versioning]
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name="argument-name",
+ )
+
+ # Argument wins
+ assert config.dist_name == "argument-name"
+
+ def test_dist_name_from_config(self, tmp_path: Path) -> None:
+ """dist_name from config if no argument."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "project-name"
+
+[tool.vcs-versioning]
+dist_name = "config-name"
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(pyproject_data=pyproject)
+
+ assert config.dist_name == "config-name"
+
+ def test_dist_name_from_project(self, tmp_path: Path) -> None:
+ """dist_name from project.name if not in config."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "project-name"
+
+[tool.vcs-versioning]
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(pyproject_data=pyproject)
+
+ assert config.dist_name == "project-name"
+
+ def test_dist_name_none_when_missing(self, tmp_path: Path) -> None:
+ """dist_name is None when not specified anywhere."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[tool.vcs-versioning]
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(pyproject_data=pyproject)
+
+ assert config.dist_name is None
+
+
+class TestEmptyPyProjectData:
+ """Test with empty or minimal PyProjectData."""
+
+ def test_empty_pyproject_section(self, tmp_path: Path) -> None:
+ """Empty vcs-versioning section uses defaults."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[tool.vcs-versioning]
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(pyproject_data=pyproject)
+
+ # Should use defaults
+ from vcs_versioning import DEFAULT_LOCAL_SCHEME, DEFAULT_VERSION_SCHEME
+
+ assert config.local_scheme == DEFAULT_LOCAL_SCHEME
+ assert config.version_scheme == DEFAULT_VERSION_SCHEME
+
+ def test_section_not_present(self, tmp_path: Path) -> None:
+ """Missing section still creates configuration."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+"""
+ )
+
+ # Note: This will log a warning but should not fail
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name="test-pkg",
+ )
+
+ # Should still create config with defaults
+ assert config.dist_name == "test-pkg"
diff --git a/vcs-versioning/testing_vcs/test_internal_log_level.py b/vcs-versioning/testing_vcs/test_internal_log_level.py
new file mode 100644
index 00000000..f14eb273
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_internal_log_level.py
@@ -0,0 +1,47 @@
+from __future__ import annotations
+
+import logging
+
+from vcs_versioning.overrides import GlobalOverrides
+
+
+def test_log_levels_when_set() -> None:
+ # Empty string or "1" should map to DEBUG (10)
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": ""}) as overrides:
+ assert overrides.log_level() == logging.DEBUG
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "1"}) as overrides:
+ assert overrides.log_level() == logging.DEBUG
+
+ # Level names should be recognized
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "INFO"}) as overrides:
+ assert overrides.log_level() == logging.INFO
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "info"}) as overrides:
+ assert overrides.log_level() == logging.INFO
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "WARNING"}) as overrides:
+ assert overrides.log_level() == logging.WARNING
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "ERROR"}) as overrides:
+ assert overrides.log_level() == logging.ERROR
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "CRITICAL"}) as overrides:
+ assert overrides.log_level() == logging.CRITICAL
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "DEBUG"}) as overrides:
+ assert overrides.log_level() == logging.DEBUG
+
+ # Unknown string should default to DEBUG
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "yes"}) as overrides:
+ assert overrides.log_level() == logging.DEBUG
+
+ # Explicit log level (>=2) should be used as-is
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "10"}) as overrides:
+ assert overrides.log_level() == logging.DEBUG
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "20"}) as overrides:
+ assert overrides.log_level() == logging.INFO
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "30"}) as overrides:
+ assert overrides.log_level() == logging.WARNING
diff --git a/testing/test_mercurial.py b/vcs-versioning/testing_vcs/test_mercurial.py
similarity index 77%
rename from testing/test_mercurial.py
rename to vcs-versioning/testing_vcs/test_mercurial.py
index 3e15aae8..082e18eb 100644
--- a/testing/test_mercurial.py
+++ b/vcs-versioning/testing_vcs/test_mercurial.py
@@ -1,32 +1,23 @@
from __future__ import annotations
import os
-
from pathlib import Path
import pytest
+import vcs_versioning._file_finders # noqa: F401
+from vcs_versioning import Configuration
+from vcs_versioning._backends._hg import archival_to_version, parse
+from vcs_versioning._run_cmd import CommandNotFoundError
+from vcs_versioning._version_schemes import format_version
+from vcs_versioning.test_api import WorkDir
-import setuptools_scm._file_finders
-
-from setuptools_scm import Configuration
-from setuptools_scm._run_cmd import CommandNotFoundError
-from setuptools_scm._run_cmd import has_command
-from setuptools_scm.hg import archival_to_version
-from setuptools_scm.hg import parse
-from setuptools_scm.version import format_version
-from testing.wd_wrapper import WorkDir
-
-pytestmark = pytest.mark.skipif(
- not has_command("hg", warn=False), reason="hg executable not found"
-)
+# Note: Mercurial availability is now checked in WorkDir.setup_hg() method
@pytest.fixture
def wd(wd: WorkDir) -> WorkDir:
- wd("hg init")
- wd.add_command = "hg add ."
- wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"'
- return wd
+ """Set up mercurial for hg-specific tests."""
+ return wd.setup_hg()
archival_mapping = {
@@ -60,7 +51,7 @@ def test_archival_to_version(expected: str, data: dict[str, str]) -> None:
def test_hg_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("PATH", str(wd.cwd / "not-existing"))
config = Configuration()
- wd.write("pyproject.toml", "[tool.setuptools_scm]")
+ wd.write("pyproject.toml", "[tool.vcs-versioning]")
with pytest.raises(CommandNotFoundError, match=r"hg"):
parse(wd.cwd, config=config)
@@ -73,24 +64,28 @@ def test_hg_command_from_env(
request: pytest.FixtureRequest,
hg_exe: str,
) -> None:
- wd.write("pyproject.toml", "[tool.setuptools_scm]")
+ from vcs_versioning.overrides import GlobalOverrides
+
+ wd.write("pyproject.toml", "[tool.vcs-versioning]")
# 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+")
+ with GlobalOverrides.from_active(hg_command=hg_exe):
+ 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]")
+ from vcs_versioning.overrides import GlobalOverrides
+
+ config = Configuration()
+ wd.write("pyproject.toml", "[tool.vcs-versioning]")
+
+ # Use from_active() to create overrides with invalid hg command
+ with GlobalOverrides.from_active(hg_command=str(wd.cwd / "not-existing")):
with pytest.raises(CommandNotFoundError, match=r"test.*hg.*not-existing"):
parse(wd.cwd, config=config)
@@ -105,40 +100,41 @@ def test_find_files_stop_at_root_hg(
project.mkdir()
project.joinpath("setup.cfg").touch()
# setup.cfg has not been committed
- assert setuptools_scm._file_finders.find_files(str(project)) == []
+ assert vcs_versioning._file_finders.find_files(str(project)) == []
# issue 251
wd.add_and_commit()
monkeypatch.chdir(project)
- assert setuptools_scm._file_finders.find_files() == ["setup.cfg"]
+ assert vcs_versioning._file_finders.find_files() == ["setup.cfg"]
# XXX: better tests for tag prefixes
def test_version_from_hg_id(wd: WorkDir) -> None:
- assert wd.get_version() == "0.0"
+ # Initial state with no commits
+ wd.expect_parse(tag="0.0", distance=0, dirty=False, exact=True)
wd.commit_testfile()
- assert wd.get_version().startswith("0.1.dev1+")
+ wd.expect_parse(tag="0.0", distance=1, dirty=False, node_prefix="h")
# tagging commit is considered the tag
wd('hg tag v0.1 -u test -d "0 0"')
- assert wd.get_version() == "0.1"
+ wd.expect_parse(tag="0.1", distance=0, dirty=False, exact=True)
wd.commit_testfile()
- assert wd.get_version().startswith("0.2.dev2")
+ wd.expect_parse(tag="0.1", distance=2, dirty=False)
wd("hg up v0.1")
- assert wd.get_version() == "0.1"
+ wd.expect_parse(tag="0.1", distance=0, dirty=False, exact=True)
# commit originating from the tagged revision
# that is not an actual tag
wd.commit_testfile()
- assert wd.get_version().startswith("0.2.dev1+")
+ wd.expect_parse(tag="0.1", distance=1, dirty=False)
# several tags
wd("hg up")
wd('hg tag v0.2 -u test -d "0 0"')
wd('hg tag v0.3 -u test -d "0 0" -r v0.2')
- assert wd.get_version() == "0.3"
+ wd.expect_parse(tag="0.3", distance=0, dirty=False, exact=True)
def test_version_from_archival(wd: WorkDir) -> None:
diff --git a/vcs-versioning/testing_vcs/test_overrides_api.py b/vcs-versioning/testing_vcs/test_overrides_api.py
new file mode 100644
index 00000000..06822e13
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_overrides_api.py
@@ -0,0 +1,709 @@
+"""Tests for GlobalOverrides API methods."""
+
+from __future__ import annotations
+
+import logging
+
+import pytest
+from vcs_versioning.overrides import GlobalOverrides
+
+
+def test_from_active_modifies_field() -> None:
+ """Test that from_active() creates a modified copy."""
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "1"}):
+ # Original has DEBUG level
+ assert GlobalOverrides.from_active().debug == logging.DEBUG
+
+ # Create modified version with INFO level
+ with GlobalOverrides.from_active(debug=logging.INFO):
+ from vcs_versioning.overrides import get_active_overrides
+
+ active = get_active_overrides()
+ assert active.debug == logging.INFO
+
+
+def test_from_active_preserves_other_fields() -> None:
+ """Test that from_active() preserves fields not explicitly changed."""
+ env = {
+ "TEST_DEBUG": "20", # INFO
+ "TEST_SUBPROCESS_TIMEOUT": "100",
+ "TEST_HG_COMMAND": "custom_hg",
+ "SOURCE_DATE_EPOCH": "1234567890",
+ }
+
+ with GlobalOverrides.from_env("TEST", env=env):
+ # Modify only debug level
+ with GlobalOverrides.from_active(debug=logging.WARNING):
+ from vcs_versioning.overrides import get_active_overrides
+
+ active = get_active_overrides()
+ assert active.debug == logging.WARNING
+ # Other fields preserved
+ assert active.subprocess_timeout == 100
+ assert active.hg_command == "custom_hg"
+ assert active.source_date_epoch == 1234567890
+ assert active.tool == "TEST"
+
+
+def test_from_active_without_context_raises() -> None:
+ """Test that from_active() raises when no context is active."""
+ from vcs_versioning import overrides
+
+ # Temporarily clear any active context
+ token = overrides._active_overrides.set(None)
+ try:
+ with pytest.raises(
+ RuntimeError,
+ match="Cannot call from_active\\(\\) without an active GlobalOverrides context",
+ ):
+ GlobalOverrides.from_active(debug=logging.INFO)
+ finally:
+ overrides._active_overrides.reset(token)
+
+
+def test_export_to_dict() -> None:
+ """Test exporting overrides to a dictionary."""
+ env_source = {
+ "TEST_DEBUG": "INFO",
+ "TEST_SUBPROCESS_TIMEOUT": "99",
+ "TEST_HG_COMMAND": "/usr/bin/hg",
+ "SOURCE_DATE_EPOCH": "1672531200",
+ }
+
+ overrides = GlobalOverrides.from_env("TEST", env=env_source)
+
+ target_env: dict[str, str] = {}
+ overrides.export(target_env)
+
+ assert target_env["TEST_DEBUG"] == "20" # INFO level
+ assert target_env["TEST_SUBPROCESS_TIMEOUT"] == "99"
+ assert target_env["TEST_HG_COMMAND"] == "/usr/bin/hg"
+ assert target_env["SOURCE_DATE_EPOCH"] == "1672531200"
+
+
+def test_export_to_monkeypatch(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Test exporting overrides via monkeypatch."""
+ import os
+
+ overrides = GlobalOverrides.from_env(
+ "TEST",
+ env={
+ "TEST_DEBUG": "DEBUG",
+ "TEST_SUBPROCESS_TIMEOUT": "77",
+ "SOURCE_DATE_EPOCH": "1000000000",
+ },
+ )
+
+ overrides.export(monkeypatch)
+
+ # Check that environment was set
+ assert os.environ["TEST_DEBUG"] == "10" # DEBUG level
+ assert os.environ["TEST_SUBPROCESS_TIMEOUT"] == "77"
+ assert os.environ["SOURCE_DATE_EPOCH"] == "1000000000"
+
+
+def test_export_debug_false() -> None:
+ """Test that debug=False exports as '0'."""
+ overrides = GlobalOverrides.from_env("TEST", env={})
+
+ target_env: dict[str, str] = {}
+ overrides.export(target_env)
+
+ assert target_env["TEST_DEBUG"] == "0"
+
+
+def test_from_active_and_export_together(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Test using from_active() and export() together."""
+ import os
+
+ # Start with one context
+ with GlobalOverrides.from_env("TOOL", env={"TOOL_DEBUG": "1"}):
+ # Create a modified version
+ modified = GlobalOverrides.from_active(
+ debug=logging.WARNING, subprocess_timeout=200
+ )
+
+ # Export it
+ modified.export(monkeypatch)
+
+ # Verify it was exported correctly
+ assert os.environ["TOOL_DEBUG"] == "30" # WARNING
+ assert os.environ["TOOL_SUBPROCESS_TIMEOUT"] == "200"
+
+
+def test_nested_from_active_contexts() -> None:
+ """Test nested contexts using from_active()."""
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "DEBUG"}):
+ from vcs_versioning.overrides import get_active_overrides
+
+ # Original: DEBUG level
+ assert get_active_overrides().debug == logging.DEBUG
+
+ with GlobalOverrides.from_active(debug=logging.INFO):
+ # Modified: INFO level
+ assert get_active_overrides().debug == logging.INFO
+
+ with GlobalOverrides.from_active(debug=logging.WARNING):
+ # Further modified: WARNING level
+ assert get_active_overrides().debug == logging.WARNING
+
+ # Back to INFO
+ assert get_active_overrides().debug == logging.INFO
+
+ # Back to DEBUG
+ assert get_active_overrides().debug == logging.DEBUG
+
+
+def test_export_without_source_date_epoch() -> None:
+ """Test that export() handles None source_date_epoch correctly."""
+ overrides = GlobalOverrides.from_env("TEST", env={})
+
+ target_env: dict[str, str] = {}
+ overrides.export(target_env)
+
+ # SOURCE_DATE_EPOCH should not be in the exported env
+ assert "SOURCE_DATE_EPOCH" not in target_env
+ assert "TEST_DEBUG" in target_env
+ assert "TEST_SUBPROCESS_TIMEOUT" in target_env
+ assert "TEST_HG_COMMAND" in target_env
+
+
+def test_from_active_multiple_fields() -> None:
+ """Test changing multiple fields at once with from_active()."""
+ env = {
+ "TEST_DEBUG": "DEBUG",
+ "TEST_SUBPROCESS_TIMEOUT": "50",
+ "TEST_HG_COMMAND": "hg",
+ "SOURCE_DATE_EPOCH": "1000000000",
+ }
+
+ with GlobalOverrides.from_env("TEST", env=env):
+ # Change multiple fields
+ with GlobalOverrides.from_active(
+ debug=logging.ERROR,
+ subprocess_timeout=999,
+ hg_command="/custom/hg",
+ source_date_epoch=2000000000,
+ ):
+ from vcs_versioning.overrides import get_active_overrides
+
+ active = get_active_overrides()
+ assert active.debug == logging.ERROR
+ assert active.subprocess_timeout == 999
+ assert active.hg_command == "/custom/hg"
+ assert active.source_date_epoch == 2000000000
+ # Tool should be preserved
+ assert active.tool == "TEST"
+
+
+def test_export_roundtrip() -> None:
+ """Test that export -> from_env produces equivalent overrides."""
+ original = GlobalOverrides.from_env(
+ "TEST",
+ env={
+ "TEST_DEBUG": "WARNING",
+ "TEST_SUBPROCESS_TIMEOUT": "123",
+ "TEST_HG_COMMAND": "/my/hg",
+ "SOURCE_DATE_EPOCH": "1234567890",
+ },
+ )
+
+ # Export to dict
+ exported_env: dict[str, str] = {}
+ original.export(exported_env)
+
+ # Create new overrides from exported env
+ recreated = GlobalOverrides.from_env("TEST", env=exported_env)
+
+ # Should be equivalent
+ assert recreated.debug == original.debug
+ assert recreated.subprocess_timeout == original.subprocess_timeout
+ assert recreated.hg_command == original.hg_command
+ assert recreated.source_date_epoch == original.source_date_epoch
+ assert recreated.tool == original.tool
+
+
+def test_from_active_preserves_tool() -> None:
+ """Test that from_active() preserves the tool prefix."""
+ with GlobalOverrides.from_env("CUSTOM_TOOL", env={"CUSTOM_TOOL_DEBUG": "1"}):
+ with GlobalOverrides.from_active(subprocess_timeout=999):
+ from vcs_versioning.overrides import get_active_overrides
+
+ active = get_active_overrides()
+ assert active.tool == "CUSTOM_TOOL"
+
+
+def test_export_with_different_debug_levels() -> None:
+ """Test that export() correctly formats different debug levels."""
+ test_cases = [
+ (False, "0"),
+ (logging.DEBUG, "10"),
+ (logging.INFO, "20"),
+ (logging.WARNING, "30"),
+ (logging.ERROR, "40"),
+ (logging.CRITICAL, "50"),
+ ]
+
+ for debug_val, expected_str in test_cases:
+ # Need an active context to use from_active()
+ with GlobalOverrides.from_env("TEST", env={}):
+ modified = GlobalOverrides.from_active(debug=debug_val)
+
+ target_env: dict[str, str] = {}
+ modified.export(target_env)
+
+ assert target_env["TEST_DEBUG"] == expected_str, (
+ f"Expected {expected_str} for debug={debug_val}, got {target_env['TEST_DEBUG']}"
+ )
+
+
+def test_from_active_with_source_date_epoch_none() -> None:
+ """Test that from_active() can clear source_date_epoch."""
+ with GlobalOverrides.from_env("TEST", env={"SOURCE_DATE_EPOCH": "1234567890"}):
+ from vcs_versioning.overrides import get_active_overrides
+
+ # Original has epoch set
+ assert get_active_overrides().source_date_epoch == 1234567890
+
+ # Clear it with from_active
+ with GlobalOverrides.from_active(source_date_epoch=None):
+ assert get_active_overrides().source_date_epoch is None
+
+
+def test_export_integration_with_subprocess_pattern() -> None:
+ """Test the common pattern of exporting for subprocess calls."""
+
+ # Simulate the pattern used in tests
+ with GlobalOverrides.from_env("TOOL", env={"TOOL_DEBUG": "INFO"}):
+ modified = GlobalOverrides.from_active(
+ subprocess_timeout=5, debug=logging.DEBUG
+ )
+
+ # Export to a clean environment
+ subprocess_env: dict[str, str] = {}
+ modified.export(subprocess_env)
+
+ # Verify subprocess would get the right values
+ assert subprocess_env["TOOL_DEBUG"] == "10" # DEBUG
+ assert subprocess_env["TOOL_SUBPROCESS_TIMEOUT"] == "5"
+
+ # Can be used with subprocess.run
+ # subprocess.run(["cmd"], env=subprocess_env)
+
+
+def test_env_reader_property() -> None:
+ """Test that GlobalOverrides provides a configured EnvReader."""
+ env = {
+ "TOOL_CUSTOM_VAR": "value1",
+ "VCS_VERSIONING_FALLBACK_VAR": "value2",
+ "TOOL_VAR_FOR_MY_PKG": "dist_specific",
+ }
+
+ # Without dist_name
+ with GlobalOverrides.from_env("TOOL", env=env) as overrides:
+ reader = overrides.env_reader
+ assert reader.read("CUSTOM_VAR") == "value1"
+ assert reader.read("FALLBACK_VAR") == "value2" # Uses VCS_VERSIONING fallback
+ assert reader.read("NONEXISTENT") is None
+
+ # With dist_name
+ with GlobalOverrides.from_env("TOOL", env=env, dist_name="my-pkg") as overrides:
+ reader = overrides.env_reader
+ assert reader.read("VAR") == "dist_specific" # Dist-specific takes precedence
+
+
+def test_env_reader_property_with_dist_name() -> None:
+ """Test EnvReader property with distribution-specific variables."""
+ env = {
+ "TOOL_CONFIG_FOR_MY_PACKAGE": '{local_scheme = "no-local"}',
+ "TOOL_CONFIG": '{version_scheme = "guess-next-dev"}',
+ }
+
+ from typing import TypedDict
+
+ class TestSchema(TypedDict, total=False):
+ local_scheme: str
+ version_scheme: str
+
+ with GlobalOverrides.from_env("TOOL", env=env, dist_name="my-package") as overrides:
+ # Should read dist-specific TOML
+ config = overrides.env_reader.read_toml("CONFIG", schema=TestSchema)
+ assert config == {"local_scheme": "no-local"}
+
+ # Without dist_name, gets generic
+ with GlobalOverrides.from_env("TOOL", env=env) as overrides:
+ config = overrides.env_reader.read_toml("CONFIG", schema=TestSchema)
+ assert config == {"version_scheme": "guess-next-dev"}
+
+
+class TestEnvReader:
+ """Tests for the EnvReader class."""
+
+ def test_requires_tools_names(self) -> None:
+ """Test that EnvReader requires tools_names to be provided."""
+ from vcs_versioning.overrides import EnvReader
+
+ with pytest.raises(TypeError, match="tools_names must be a non-empty tuple"):
+ EnvReader(tools_names=(), env={})
+
+ def test_empty_tools_names_raises(self) -> None:
+ """Test that empty tools_names raises an error."""
+ from vcs_versioning.overrides import EnvReader
+
+ with pytest.raises(TypeError, match="tools_names must be a non-empty tuple"):
+ EnvReader(tools_names=(), env={})
+
+ def test_read_generic_first_tool(self) -> None:
+ """Test reading generic env var from first tool."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_DEBUG": "1"}
+ reader = EnvReader(tools_names=("TOOL_A", "TOOL_B"), env=env)
+ assert reader.read("DEBUG") == "1"
+
+ def test_read_generic_fallback_to_second_tool(self) -> None:
+ """Test falling back to second tool when first not found."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_B_DEBUG": "2"}
+ reader = EnvReader(tools_names=("TOOL_A", "TOOL_B"), env=env)
+ assert reader.read("DEBUG") == "2"
+
+ def test_read_generic_first_tool_wins(self) -> None:
+ """Test that first tool takes precedence."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_DEBUG": "1", "TOOL_B_DEBUG": "2"}
+ reader = EnvReader(tools_names=("TOOL_A", "TOOL_B"), env=env)
+ assert reader.read("DEBUG") == "1"
+
+ def test_read_not_found(self) -> None:
+ """Test that None is returned when env var not found."""
+ from vcs_versioning.overrides import EnvReader
+
+ reader = EnvReader(tools_names=("TOOL_A", "TOOL_B"), env={})
+ assert reader.read("DEBUG") is None
+
+ def test_read_dist_specific_first_tool(self) -> None:
+ """Test reading dist-specific env var from first tool."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_PRETEND_VERSION_FOR_MY_PACKAGE": "1.0.0"}
+ reader = EnvReader(
+ tools_names=("TOOL_A", "TOOL_B"), env=env, dist_name="my-package"
+ )
+ assert reader.read("PRETEND_VERSION") == "1.0.0"
+
+ def test_read_dist_specific_fallback_to_second_tool(self) -> None:
+ """Test falling back to second tool for dist-specific."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_B_PRETEND_VERSION_FOR_MY_PACKAGE": "2.0.0"}
+ reader = EnvReader(
+ tools_names=("TOOL_A", "TOOL_B"), env=env, dist_name="my-package"
+ )
+ assert reader.read("PRETEND_VERSION") == "2.0.0"
+
+ def test_read_dist_specific_takes_precedence_over_generic(self) -> None:
+ """Test that dist-specific takes precedence over generic."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {
+ "TOOL_A_PRETEND_VERSION_FOR_MY_PACKAGE": "1.0.0",
+ "TOOL_A_PRETEND_VERSION": "2.0.0",
+ }
+ reader = EnvReader(
+ tools_names=("TOOL_A", "TOOL_B"), env=env, dist_name="my-package"
+ )
+ assert reader.read("PRETEND_VERSION") == "1.0.0"
+
+ def test_read_dist_specific_second_tool_over_generic_first_tool(self) -> None:
+ """Test that dist-specific from second tool beats generic from first tool."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {
+ "TOOL_B_PRETEND_VERSION_FOR_MY_PACKAGE": "2.0.0",
+ "TOOL_A_PRETEND_VERSION": "1.0.0",
+ }
+ reader = EnvReader(
+ tools_names=("TOOL_A", "TOOL_B"), env=env, dist_name="my-package"
+ )
+ # Dist-specific from TOOL_B should win
+ assert reader.read("PRETEND_VERSION") == "2.0.0"
+
+ def test_read_falls_back_to_generic_when_no_dist_specific(self) -> None:
+ """Test falling back to generic when dist-specific not found."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_B_PRETEND_VERSION": "2.0.0"}
+ reader = EnvReader(
+ tools_names=("TOOL_A", "TOOL_B"), env=env, dist_name="my-package"
+ )
+ assert reader.read("PRETEND_VERSION") == "2.0.0"
+
+ def test_read_normalizes_dist_name(self) -> None:
+ """Test that distribution names are normalized correctly."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_PRETEND_VERSION_FOR_MY_PACKAGE": "1.0.0"}
+ # Try various equivalent dist names
+ for dist_name in ["my-package", "My.Package", "my_package", "MY-PACKAGE"]:
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name=dist_name)
+ assert reader.read("PRETEND_VERSION") == "1.0.0"
+
+ def test_read_finds_alternative_normalization(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """Test that read warns about alternative normalizations."""
+ from vcs_versioning.overrides import EnvReader
+
+ # Use a non-standard normalization
+ env = {"TOOL_A_PRETEND_VERSION_FOR_MY-PACKAGE": "1.0.0"}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name="my-package")
+
+ with caplog.at_level(logging.WARNING):
+ result = reader.read("PRETEND_VERSION")
+
+ assert result == "1.0.0"
+ assert "Found environment variable" in caplog.text
+ assert "but expected" in caplog.text
+ assert "TOOL_A_PRETEND_VERSION_FOR_MY_PACKAGE" in caplog.text
+
+ def test_read_suggests_close_matches(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """Test that read suggests close matches for typos."""
+ from vcs_versioning.overrides import EnvReader
+
+ # Use a typo in dist name
+ env = {"TOOL_A_PRETEND_VERSION_FOR_MY_PACKGE": "1.0.0"}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name="my-package")
+
+ with caplog.at_level(logging.WARNING):
+ result = reader.read("PRETEND_VERSION")
+
+ assert result is None
+ assert "Did you mean" in caplog.text
+
+ def test_read_returns_exact_match_without_warning(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """Test that exact matches don't trigger diagnostics."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_PRETEND_VERSION_FOR_MY_PACKAGE": "1.0.0"}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name="my-package")
+
+ with caplog.at_level(logging.WARNING):
+ result = reader.read("PRETEND_VERSION")
+
+ assert result == "1.0.0"
+ # No warnings should be logged for exact matches
+ assert not caplog.records
+
+ def test_read_toml_inline_map(self) -> None:
+ """Test reading an inline TOML map."""
+ from vcs_versioning._overrides import ConfigOverridesDict
+ from vcs_versioning.overrides import EnvReader
+
+ env = {
+ "TOOL_A_OVERRIDES": '{local_scheme = "no-local-version", version_scheme = "release-branch-semver"}'
+ }
+ reader = EnvReader(tools_names=("TOOL_A",), env=env)
+
+ result = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+ assert result == {
+ "local_scheme": "no-local-version",
+ "version_scheme": "release-branch-semver",
+ }
+
+ def test_read_toml_full_document(self) -> None:
+ """Test reading a full TOML document."""
+ from vcs_versioning._overrides import PretendMetadataDict
+ from vcs_versioning.overrides import EnvReader
+
+ env = {
+ "TOOL_A_PRETEND_METADATA": 'tag = "v1.0.0"\ndistance = 4\nnode = "g123abc"'
+ }
+ reader = EnvReader(tools_names=("TOOL_A",), env=env)
+
+ result = reader.read_toml("PRETEND_METADATA", schema=PretendMetadataDict)
+ assert result == {"tag": "v1.0.0", "distance": 4, "node": "g123abc"}
+
+ def test_read_toml_not_found_returns_empty_dict(self) -> None:
+ """Test that read_toml returns empty dict when not found."""
+ from vcs_versioning._overrides import ConfigOverridesDict
+ from vcs_versioning.overrides import EnvReader
+
+ reader = EnvReader(tools_names=("TOOL_A",), env={})
+
+ result = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+ assert result == {}
+
+ def test_read_toml_empty_string_returns_empty_dict(self) -> None:
+ """Test that empty string returns empty dict."""
+ from vcs_versioning._overrides import ConfigOverridesDict
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_OVERRIDES": ""}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env)
+
+ result = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+ assert result == {}
+
+ def test_read_toml_with_tool_fallback(self) -> None:
+ """Test that read_toml respects tool fallback order."""
+ from typing import TypedDict
+
+ from vcs_versioning.overrides import EnvReader
+
+ class _TestSchema(TypedDict, total=False):
+ """Schema for this test without validation."""
+
+ debug: bool
+
+ env = {"TOOL_B_OVERRIDES": "{debug = true}"}
+ reader = EnvReader(tools_names=("TOOL_A", "TOOL_B"), env=env)
+
+ result = reader.read_toml("OVERRIDES", schema=_TestSchema)
+ assert result == {"debug": True}
+
+ def test_read_toml_with_dist_specific(self) -> None:
+ """Test reading dist-specific TOML data."""
+ from vcs_versioning._overrides import ConfigOverridesDict
+ from vcs_versioning.overrides import EnvReader
+
+ env = {
+ "TOOL_A_OVERRIDES_FOR_MY_PACKAGE": '{local_scheme = "no-local-version"}',
+ "TOOL_A_OVERRIDES": '{version_scheme = "guess-next-dev"}',
+ }
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name="my-package")
+
+ # Should get dist-specific version
+ result = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+ assert result == {"local_scheme": "no-local-version"}
+
+ def test_read_toml_dist_specific_fallback_to_generic(self) -> None:
+ """Test falling back to generic when dist-specific not found."""
+ from vcs_versioning._overrides import ConfigOverridesDict
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_OVERRIDES": '{version_scheme = "guess-next-dev"}'}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name="my-package")
+
+ result = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+ assert result == {"version_scheme": "guess-next-dev"}
+
+ def test_read_toml_invalid_raises(self) -> None:
+ """Test that invalid TOML raises InvalidTomlError."""
+ from vcs_versioning._overrides import ConfigOverridesDict
+ from vcs_versioning._toml import InvalidTomlError
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_OVERRIDES": "this is not valid toml {{{"}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env)
+
+ with pytest.raises(InvalidTomlError, match="Invalid TOML content"):
+ reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+
+ def test_read_toml_with_alternative_normalization(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """Test that read_toml works with diagnostic warnings."""
+ from typing import TypedDict
+
+ from vcs_versioning.overrides import EnvReader
+
+ class _TestSchema(TypedDict, total=False):
+ """Schema for this test without validation."""
+
+ key: str
+
+ # Use a non-standard normalization
+ env = {"TOOL_A_OVERRIDES_FOR_MY-PACKAGE": '{key = "value"}'}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name="my-package")
+
+ with caplog.at_level(logging.WARNING):
+ result = reader.read_toml("OVERRIDES", schema=_TestSchema)
+
+ assert result == {"key": "value"}
+ assert "Found environment variable" in caplog.text
+ assert "but expected" in caplog.text
+
+ def test_read_toml_complex_metadata(self) -> None:
+ """Test reading complex ScmVersion metadata."""
+ from vcs_versioning._overrides import PretendMetadataDict
+ from vcs_versioning.overrides import EnvReader
+
+ env = {
+ "TOOL_A_PRETEND_METADATA": '{tag = "v2.0.0", distance = 10, node = "gabcdef123", dirty = true, branch = "main"}'
+ }
+ reader = EnvReader(tools_names=("TOOL_A",), env=env)
+
+ result = reader.read_toml("PRETEND_METADATA", schema=PretendMetadataDict)
+ assert result["tag"] == "v2.0.0"
+ assert result["distance"] == 10
+ assert result["node"] == "gabcdef123"
+ assert result["dirty"] is True
+ assert result["branch"] == "main"
+
+ def test_read_toml_with_schema_validation(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """Test that schema validation filters invalid fields."""
+ from typing import TypedDict
+
+ from vcs_versioning.overrides import EnvReader
+
+ # Define a test schema
+ class TestSchema(TypedDict, total=False):
+ valid_field: str
+ another_valid: str
+
+ env = {
+ "TOOL_A_DATA": '{valid_field = "ok", invalid_field = "bad", another_valid = "also ok"}'
+ }
+ reader = EnvReader(tools_names=("TOOL_A",), env=env)
+
+ with caplog.at_level(logging.WARNING):
+ result = reader.read_toml("DATA", schema=TestSchema)
+
+ # Invalid field should be removed
+ assert result == {"valid_field": "ok", "another_valid": "also ok"}
+ assert "invalid_field" not in result
+
+ # Should have logged a warning about invalid fields
+ assert "Invalid fields in TOML data" in caplog.text
+ assert "invalid_field" in caplog.text
+
+
+def test_read_toml_overrides_with_schema(
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ """Test that read_toml_overrides validates against CONFIG_OVERRIDES_SCHEMA."""
+ import os
+ from unittest.mock import patch
+
+ from vcs_versioning._overrides import read_toml_overrides
+
+ # Mock the environment with valid and invalid fields
+ mock_env = {
+ "SETUPTOOLS_SCM_OVERRIDES": '{version_scheme = "guess-next-dev", local_scheme = "no-local-version", invalid_field = "bad"}'
+ }
+
+ with (
+ patch.dict(os.environ, mock_env, clear=True),
+ caplog.at_level(logging.WARNING),
+ ):
+ result = read_toml_overrides(dist_name=None)
+
+ # Valid fields should be present
+ assert result["version_scheme"] == "guess-next-dev"
+ assert result["local_scheme"] == "no-local-version"
+
+ # Invalid field should be removed
+ assert "invalid_field" not in result
+
+ # Should have logged a warning
+ assert "Invalid fields in TOML data" in caplog.text
+ assert "invalid_field" in caplog.text
diff --git a/testing/test_overrides.py b/vcs-versioning/testing_vcs/test_overrides_env_reader.py
similarity index 93%
rename from testing/test_overrides.py
rename to vcs-versioning/testing_vcs/test_overrides_env_reader.py
index afba5339..5ad042b4 100644
--- a/testing/test_overrides.py
+++ b/vcs-versioning/testing_vcs/test_overrides_env_reader.py
@@ -3,10 +3,28 @@
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
+from vcs_versioning._overrides import (
+ _find_close_env_var_matches,
+ _search_env_vars_with_prefix,
+)
+from vcs_versioning.overrides import EnvReader
+
+
+# Helper function that matches the old read_named_env signature for tests
+def read_named_env(
+ *,
+ name: str,
+ dist_name: str | None,
+ env: dict[str, str],
+ tool: str = "SETUPTOOLS_SCM",
+) -> str | None:
+ """Test helper that wraps EnvReader to match old read_named_env signature."""
+ reader = EnvReader(
+ tools_names=(tool, "VCS_VERSIONING"),
+ env=env,
+ dist_name=dist_name,
+ )
+ return reader.read(name)
class TestSearchEnvVarsWithPrefix:
diff --git a/vcs-versioning/testing_vcs/test_regressions.py b/vcs-versioning/testing_vcs/test_regressions.py
new file mode 100644
index 00000000..ae032543
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_regressions.py
@@ -0,0 +1,104 @@
+"""Core VCS regression tests."""
+
+from __future__ import annotations
+
+import sys
+from collections.abc import Sequence
+from dataclasses import replace
+from pathlib import Path
+
+import pytest
+from vcs_versioning import Configuration
+from vcs_versioning._backends._git import parse
+from vcs_versioning._run_cmd import run
+from vcs_versioning._scm_version import meta
+from vcs_versioning.test_api import WorkDir
+
+
+@pytest.mark.skipif(sys.platform != "win32", reason="this bug is only valid on windows")
+def test_case_mismatch_on_windows_git(tmp_path: Path) -> None:
+ """Case insensitive path checks on Windows"""
+ camel_case_path = tmp_path / "CapitalizedDir"
+ camel_case_path.mkdir()
+ run("git init", camel_case_path)
+ res = parse(str(camel_case_path).lower(), Configuration())
+ 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()
+ wd = WorkDir(repo_path).setup_git()
+
+ # Create a nested directory with specific casing
+ nested_dir = repo_path / "CasedDir"
+ nested_dir.mkdir()
+
+ # Create a pyproject.toml in the nested directory
+ wd.write(
+ "CasedDir/pyproject.toml",
+ """
+[build-system]
+requires = ["setuptools>=64", "setuptools-scm"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "test-project"
+dynamic = ["version"]
+
+[tool.setuptools_scm]
+""",
+ )
+
+ # Add and commit the file
+ wd.add_and_commit("Initial commit")
+
+ # 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_write_to_absolute_path_passes_when_subdir_of_root(tmp_path: Path) -> None:
+ c = Configuration(root=tmp_path, write_to=tmp_path / "VERSION.py")
+ v = meta("1.0", config=c)
+ from vcs_versioning._get_version_impl import write_version_files
+
+ with pytest.warns(DeprecationWarning, match=".*write_to=.* is a absolute.*"):
+ write_version_files(c, "1.0", v)
+ write_version_files(replace(c, write_to="VERSION.py"), "1.0", v)
+ subdir = tmp_path / "subdir"
+ subdir.mkdir()
+ with pytest.raises(
+ # todo: python version specific error list
+ ValueError,
+ match=r".*VERSION.py' .* .*subdir.*",
+ ):
+ write_version_files(replace(c, root=subdir), "1.0", v)
+
+
+@pytest.mark.parametrize(
+ ("input", "expected"),
+ [
+ ("1.0", (1, 0)),
+ ("1.0a2", (1, 0, "a2")),
+ ("1.0.b2dev1", (1, 0, "b2", "dev1")),
+ ("1.0.dev1", (1, 0, "dev1")),
+ ],
+)
+def test_version_as_tuple(input: str, expected: Sequence[int | str]) -> None:
+ from vcs_versioning._version_cls import _version_as_tuple
+
+ assert _version_as_tuple(input) == expected
diff --git a/testing/test_version.py b/vcs-versioning/testing_vcs/test_version.py
similarity index 86%
rename from testing/test_version.py
rename to vcs-versioning/testing_vcs/test_version.py
index e63c9493..a0a35bd6 100644
--- a/testing/test_version.py
+++ b/vcs-versioning/testing_vcs/test_version.py
@@ -1,28 +1,23 @@
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 datetime import date, datetime, timedelta, timezone
from typing import Any
import pytest
-
-from setuptools_scm import Configuration
-from setuptools_scm import NonNormalizedVersion
-from setuptools_scm.version import ScmVersion
-from setuptools_scm.version import calver_by_date
-from setuptools_scm.version import format_version
-from setuptools_scm.version import guess_next_date_ver
-from setuptools_scm.version import guess_next_version
-from setuptools_scm.version import meta
-from setuptools_scm.version import no_guess_dev_version
-from setuptools_scm.version import only_version
-from setuptools_scm.version import release_branch_semver_version
-from setuptools_scm.version import simplified_semver_version
+from vcs_versioning import Configuration, NonNormalizedVersion
+from vcs_versioning._scm_version import ScmVersion, meta
+from vcs_versioning._version_schemes import (
+ calver_by_date,
+ format_version,
+ guess_next_date_ver,
+ guess_next_version,
+ no_guess_dev_version,
+ only_version,
+ release_branch_semver_version,
+ simplified_semver_version,
+)
c = Configuration()
c_non_normalize = Configuration(version_cls=NonNormalizedVersion)
@@ -70,34 +65,6 @@ def test_next_semver(version: ScmVersion, expected_next: str) -> None:
assert computed == expected_next
-def test_next_semver_bad_tag() -> None:
- # 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"
- ):
- simplified_semver_version(version)
-
-
@pytest.mark.parametrize(
("version", "expected_next"),
[
@@ -241,7 +208,6 @@ def test_tag_regex1(tag: str, expected: str) -> None:
result = meta(tag, config=c)
else:
result = meta(tag, config=c)
- assert not isinstance(result.tag, str)
assert result.tag.public == expected
@@ -297,7 +263,7 @@ def test_custom_version_schemes() -> None:
config=replace(
c,
local_scheme="no-local-version",
- version_scheme="setuptools_scm.version:no_guess_dev_version",
+ version_scheme="vcs_versioning._version_schemes:no_guess_dev_version",
),
)
custom_computed = format_version(version)
@@ -478,6 +444,63 @@ def test_calver_guess_next_data(tag: str, node_date: date, expected: str) -> Non
assert next == expected
+def test_scm_version_matches() -> None:
+ """Test ScmVersion.matches() method."""
+ # Create a test version
+ version = meta(
+ "1.2.3",
+ distance=5,
+ dirty=True,
+ node="abc123def456",
+ branch="main",
+ config=c,
+ )
+
+ # Test exact matches
+ assert version.matches(tag="1.2.3")
+ assert version.matches(distance=5)
+ assert version.matches(dirty=True)
+ assert version.matches(branch="main")
+ assert version.matches(exact=False) # distance > 0
+
+ # Test multiple matches
+ assert version.matches(tag="1.2.3", distance=5, dirty=True)
+
+ # Test node prefix matching
+ assert version.matches(node_prefix="abc")
+ assert version.matches(node_prefix="abc123")
+ assert version.matches(node_prefix="abc123def456")
+
+ # Test mismatches - we only care that they're falsy
+ assert not version.matches(tag="1.2.4")
+ assert not version.matches(distance=3)
+ assert not version.matches(dirty=False)
+ assert not version.matches(node_prefix="xyz")
+
+ # Test multiple mismatches
+ assert not version.matches(tag="1.2.4", distance=3, dirty=False)
+
+
+def test_scm_version_matches_exact() -> None:
+ """Test ScmVersion.matches() with exact versions."""
+ # Exact version (tag with no distance and not dirty)
+ exact_version = meta("2.0.0", distance=0, dirty=False, config=c)
+ assert exact_version.matches(exact=True)
+ assert exact_version.matches(tag="2.0.0", exact=True)
+
+ # Non-exact version
+ non_exact = meta("2.0.0", distance=1, dirty=False, config=c)
+ assert not non_exact.matches(exact=True)
+
+
+def test_scm_version_matches_none_node() -> None:
+ """Test ScmVersion.matches() when node is None."""
+ version = meta("1.0.0", node=None, config=c)
+
+ # Should fail node_prefix match when node is None
+ assert not version.matches(node_prefix="abc")
+
+
def test_custom_version_cls() -> None:
"""Test that we can pass our own version class instead of pkg_resources"""
diff --git a/vcs-versioning/testing_vcs/test_version_scheme_towncrier.py b/vcs-versioning/testing_vcs/test_version_scheme_towncrier.py
new file mode 100644
index 00000000..f7987653
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_version_scheme_towncrier.py
@@ -0,0 +1,484 @@
+"""Tests for the towncrier-fragments version scheme."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+from vcs_versioning import _config
+from vcs_versioning._scm_version import ScmVersion
+from vcs_versioning._version_cls import Version
+from vcs_versioning._version_schemes._towncrier import (
+ _determine_bump_type,
+ _find_fragments,
+ version_from_fragments,
+)
+
+
+@pytest.fixture
+def changelog_dir(tmp_path: Path) -> Path:
+ """Create a temporary changelog.d directory."""
+ changelog_d = tmp_path / "changelog.d"
+ changelog_d.mkdir()
+ return changelog_d
+
+
+@pytest.fixture
+def config(tmp_path: Path) -> _config.Configuration:
+ """Create a minimal configuration object."""
+ return _config.Configuration(root=tmp_path)
+
+
+def test_find_fragments_empty(changelog_dir: Path) -> None:
+ """Test finding fragments in an empty directory."""
+ fragments = _find_fragments(changelog_dir.parent)
+ assert all(len(frags) == 0 for frags in fragments.values())
+
+
+def test_find_fragments_feature(changelog_dir: Path) -> None:
+ """Test finding feature fragments."""
+ (changelog_dir / "123.feature.md").write_text("Add new feature")
+ (changelog_dir / "456.feature.md").write_text("Another feature")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["feature"]) == 2
+ assert "123.feature.md" in fragments["feature"]
+ assert "456.feature.md" in fragments["feature"]
+
+
+def test_find_fragments_bugfix(changelog_dir: Path) -> None:
+ """Test finding bugfix fragments."""
+ (changelog_dir / "789.bugfix.md").write_text("Fix bug")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["bugfix"]) == 1
+ assert "789.bugfix.md" in fragments["bugfix"]
+
+
+def test_find_fragments_removal(changelog_dir: Path) -> None:
+ """Test finding removal fragments."""
+ (changelog_dir / "321.removal.md").write_text("Remove deprecated API")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["removal"]) == 1
+ assert "321.removal.md" in fragments["removal"]
+
+
+def test_find_fragments_deprecation(changelog_dir: Path) -> None:
+ """Test finding deprecation fragments."""
+ (changelog_dir / "654.deprecation.md").write_text("Deprecate old method")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["deprecation"]) == 1
+ assert "654.deprecation.md" in fragments["deprecation"]
+
+
+def test_find_fragments_doc(changelog_dir: Path) -> None:
+ """Test finding doc fragments."""
+ (changelog_dir / "111.doc.md").write_text("Update documentation")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["doc"]) == 1
+ assert "111.doc.md" in fragments["doc"]
+
+
+def test_find_fragments_misc(changelog_dir: Path) -> None:
+ """Test finding misc fragments."""
+ (changelog_dir / "222.misc.md").write_text("Refactor internal code")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["misc"]) == 1
+ assert "222.misc.md" in fragments["misc"]
+
+
+def test_find_fragments_ignores_template(changelog_dir: Path) -> None:
+ """Test that template files are ignored."""
+ (changelog_dir / "template.md").write_text("Template content")
+ (changelog_dir / "README.md").write_text("README content")
+ (changelog_dir / ".gitkeep").write_text("")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert all(len(frags) == 0 for frags in fragments.values())
+
+
+def test_find_fragments_mixed_types(changelog_dir: Path) -> None:
+ """Test finding multiple fragment types."""
+ (changelog_dir / "1.feature.md").write_text("Feature")
+ (changelog_dir / "2.bugfix.md").write_text("Bugfix")
+ (changelog_dir / "3.doc.md").write_text("Doc")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["feature"]) == 1
+ assert len(fragments["bugfix"]) == 1
+ assert len(fragments["doc"]) == 1
+
+
+def test_determine_bump_type_none() -> None:
+ """Test bump type with no fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": [],
+ "deprecation": [],
+ "bugfix": [],
+ "doc": [],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) is None
+
+
+def test_determine_bump_type_major() -> None:
+ """Test major bump with removal fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": ["1.removal.md"],
+ "feature": [],
+ "deprecation": [],
+ "bugfix": [],
+ "doc": [],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "major"
+
+
+def test_determine_bump_type_major_with_others() -> None:
+ """Test major bump takes precedence over other types."""
+ fragments: dict[str, list[str]] = {
+ "removal": ["1.removal.md"],
+ "feature": ["2.feature.md"],
+ "bugfix": ["3.bugfix.md"],
+ "deprecation": [],
+ "doc": [],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "major"
+
+
+def test_determine_bump_type_minor_feature() -> None:
+ """Test minor bump with feature fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": ["1.feature.md"],
+ "deprecation": [],
+ "bugfix": [],
+ "doc": [],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "minor"
+
+
+def test_determine_bump_type_minor_deprecation() -> None:
+ """Test minor bump with deprecation fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": [],
+ "deprecation": ["1.deprecation.md"],
+ "bugfix": [],
+ "doc": [],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "minor"
+
+
+def test_determine_bump_type_minor_with_patch() -> None:
+ """Test minor bump takes precedence over patch types."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": ["1.feature.md"],
+ "deprecation": [],
+ "bugfix": ["2.bugfix.md"],
+ "doc": ["3.doc.md"],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "minor"
+
+
+def test_determine_bump_type_patch_bugfix() -> None:
+ """Test patch bump with bugfix fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": [],
+ "deprecation": [],
+ "bugfix": ["1.bugfix.md"],
+ "doc": [],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "patch"
+
+
+def test_determine_bump_type_patch_doc() -> None:
+ """Test patch bump with doc fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": [],
+ "deprecation": [],
+ "bugfix": [],
+ "doc": ["1.doc.md"],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "patch"
+
+
+def test_determine_bump_type_patch_misc() -> None:
+ """Test patch bump with misc fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": [],
+ "deprecation": [],
+ "bugfix": [],
+ "doc": [],
+ "misc": ["1.misc.md"],
+ }
+ assert _determine_bump_type(fragments) == "patch"
+
+
+def test_determine_bump_type_patch_mixed() -> None:
+ """Test patch bump with multiple patch-level fragment types."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": [],
+ "deprecation": [],
+ "bugfix": ["1.bugfix.md"],
+ "doc": ["2.doc.md"],
+ "misc": ["3.misc.md"],
+ }
+ assert _determine_bump_type(fragments) == "patch"
+
+
+def test_version_from_fragments_exact(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test version scheme when exactly on a tag."""
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=0,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result == "1.2.3"
+
+
+def test_version_from_fragments_no_fragments(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test version scheme with no fragments falls back to guess-next-dev."""
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ # Should fall back to guess_next_dev_version behavior
+ assert result.startswith("1.2.4.dev5")
+
+
+def test_version_from_fragments_major_bump(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test version scheme with removal fragments (major bump)."""
+ (changelog_dir / "1.removal.md").write_text("Remove old API")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("2.0.0.dev5")
+
+
+def test_version_from_fragments_minor_bump(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test version scheme with feature fragments (minor bump)."""
+ (changelog_dir / "1.feature.md").write_text("Add new feature")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("1.3.0.dev5")
+
+
+def test_version_from_fragments_patch_bump(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test version scheme with bugfix fragments (patch bump)."""
+ (changelog_dir / "1.bugfix.md").write_text("Fix bug")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("1.2.4.dev5")
+
+
+def test_version_from_fragments_precedence(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test that removal > feature > bugfix precedence works."""
+ # Add all three types - removal should win
+ (changelog_dir / "1.removal.md").write_text("Remove API")
+ (changelog_dir / "2.feature.md").write_text("Add feature")
+ (changelog_dir / "3.bugfix.md").write_text("Fix bug")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ # Should use major bump
+ assert result.startswith("2.0.0.dev5")
+
+
+def test_version_from_fragments_minor_over_patch(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test that feature takes precedence over bugfix."""
+ (changelog_dir / "1.feature.md").write_text("Add feature")
+ (changelog_dir / "2.bugfix.md").write_text("Fix bug")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ # Should use minor bump
+ assert result.startswith("1.3.0.dev5")
+
+
+def test_version_from_fragments_deprecation_is_minor(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test that deprecation triggers a minor bump."""
+ (changelog_dir / "1.deprecation.md").write_text("Deprecate method")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("1.3.0.dev5")
+
+
+def test_version_from_fragments_doc_is_patch(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test that doc changes trigger a patch bump."""
+ (changelog_dir / "1.doc.md").write_text("Update docs")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("1.2.4.dev5")
+
+
+def test_version_from_fragments_misc_is_patch(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test that misc changes trigger a patch bump."""
+ (changelog_dir / "1.misc.md").write_text("Refactor")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("1.2.4.dev5")
+
+
+def test_version_from_fragments_major_from_0_x(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test major bump from 0.x version."""
+ (changelog_dir / "1.removal.md").write_text("Remove API")
+
+ version = ScmVersion(
+ tag=Version("0.5.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("1.0.0.dev5")
+
+
+def test_version_from_fragments_minor_from_0_x(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test minor bump from 0.x version."""
+ (changelog_dir / "1.feature.md").write_text("Add feature")
+
+ version = ScmVersion(
+ tag=Version("0.5.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("0.6.0.dev5")
+
+
+def test_version_from_fragments_missing_changelog_dir(
+ config: _config.Configuration,
+) -> None:
+ """Test version scheme when changelog.d directory doesn't exist."""
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ # Should fall back to guess-next-dev when directory is missing
+ result = version_from_fragments(version)
+ assert result.startswith("1.2.4.dev5")
+
+
+def test_version_from_fragments_dirty(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test version scheme with dirty working directory."""
+ (changelog_dir / "1.feature.md").write_text("Add feature")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=True,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ # Should still bump correctly, dirty flag affects local version
+ assert result.startswith("1.3.0.dev5")
diff --git a/testing/test_functions.py b/vcs-versioning/testing_vcs/test_version_schemes.py
similarity index 56%
rename from testing/test_functions.py
rename to vcs-versioning/testing_vcs/test_version_schemes.py
index b6b8a59e..6c03a8f6 100644
--- a/testing/test_functions.py
+++ b/vcs-versioning/testing_vcs/test_version_schemes.py
@@ -1,21 +1,12 @@
-from __future__ import annotations
-
-import shutil
-import subprocess
+"""Tests for core version scheme and formatting functionality."""
-from pathlib import Path
+from __future__ import annotations
import pytest
-
-from setuptools_scm import Configuration
-from setuptools_scm import dump_version
-from setuptools_scm import get_version
-from setuptools_scm._overrides import PRETEND_KEY
-from setuptools_scm._run_cmd import has_command
-from setuptools_scm.version import format_version
-from setuptools_scm.version import guess_next_version
-from setuptools_scm.version import meta
-from setuptools_scm.version import tag_to_version
+from vcs_versioning import Configuration
+from vcs_versioning._run_cmd import has_command
+from vcs_versioning._scm_version import meta, tag_to_version
+from vcs_versioning._version_schemes import format_version, guess_next_version
c = Configuration()
@@ -179,88 +170,17 @@ def test_format_version_with_build_metadata(
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)
- scm_version = meta(VERSIONS["exact"].tag, config=c)
- with pytest.raises(ValueError, match=r"^bad file format:"):
- dump_version(tmp_path, version, write_to, scm_version=scm_version)
-
-
@pytest.mark.parametrize(
- "version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625"]
+ ("tag", "expected_version"),
+ [
+ ("1.1", "1.1"),
+ ("release-1.1", "1.1"),
+ pytest.param("3.3.1-rc26", "3.3.1rc26", marks=pytest.mark.issue(266)),
+ ],
)
-def test_dump_version_works_with_pretend(
- version: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
-) -> None:
- monkeypatch.setenv(PRETEND_KEY, version)
- name = "VERSION.txt"
- target = tmp_path.joinpath(name)
- get_version(root=tmp_path, write_to=name)
- assert target.read_text(encoding="utf-8") == version
-
-
-def test_dump_version_modern(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
- version = "1.2.3"
- monkeypatch.setenv(PRETEND_KEY, version)
- name = "VERSION.txt"
-
- project = tmp_path.joinpath("project")
- target = project.joinpath(name)
- project.mkdir()
-
- get_version(root="..", relative_to=target, version_file=name)
- assert target.read_text(encoding="utf-8") == version
-
-
-def dump_a_version(tmp_path: Path) -> None:
- from setuptools_scm._integration.dump_version import write_version_to_path
-
- version = "1.2.3"
- scm_version = meta(version, config=c)
- write_version_to_path(
- tmp_path / "VERSION.py", template=None, version=version, scm_version=scm_version
- )
-
-
-def test_dump_version_on_old_python(tmp_path: Path) -> None:
- python37 = shutil.which("python3.7")
- if python37 is None:
- pytest.skip("python3.7 not found")
- dump_a_version(tmp_path)
- subprocess.run(
- [python37, "-c", "import VERSION;print(VERSION.version)"],
- cwd=tmp_path,
- check=True,
- )
-
-
-def test_dump_version_mypy(tmp_path: Path) -> None:
- mypy = shutil.which("mypy")
- if mypy is None:
- pytest.skip("mypy not found")
- dump_a_version(tmp_path)
- subprocess.run(
- [mypy, "--python-version=3.8", "--strict", "VERSION.py"],
- cwd=tmp_path,
- check=True,
- )
-
-
-def test_dump_version_flake8(tmp_path: Path) -> None:
- flake8 = shutil.which("flake8")
- if flake8 is None:
- pytest.skip("flake8 not found")
- dump_a_version(tmp_path)
- subprocess.run([flake8, "VERSION.py"], cwd=tmp_path, check=True)
-
-
-def test_dump_version_ruff(tmp_path: Path) -> None:
- ruff = shutil.which("ruff")
- if ruff is None:
- pytest.skip("ruff not found")
- dump_a_version(tmp_path)
- subprocess.run([ruff, "check", "--no-fix", "VERSION.py"], cwd=tmp_path, check=True)
+def test_tag_to_version(tag: str, expected_version: str) -> None:
+ version = str(tag_to_version(tag, c))
+ assert version == expected_version
def test_has_command() -> None:
@@ -280,74 +200,3 @@ def test_has_command_logs_stderr(caplog: pytest.LogCaptureFixture) -> None:
if "returned non-zero. This is stderr" in record.message:
found_it = True
assert found_it, "Did not find expected log record for "
-
-
-@pytest.mark.parametrize(
- ("tag", "expected_version"),
- [
- ("1.1", "1.1"),
- ("release-1.1", "1.1"),
- pytest.param("3.3.1-rc26", "3.3.1rc26", marks=pytest.mark.issue(266)),
- ],
-)
-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