diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0603955..bc24c78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,39 +10,48 @@ jobs: test: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + + - name: Set up uv + uses: astral-sh/setup-uv@v8.1.0 with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies + version: "latest" + enable-cache: true + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install dependencies with uv run: | - python -m pip install --upgrade pip - pip install -e . - pip install pytest black flake8 mypy types-requests - + uv sync --all-extras --dev --python ${{ matrix.python-version }} + - name: Lint with flake8 run: | - flake8 src/ --count --select=E9,F63,F7,F82 --show-source --statistics - flake8 src/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - + uv run flake8 src/ --count --select=E9,F63,F7,F82 --show-source --statistics + uv run flake8 src/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Check formatting with black - run: black --check src/ - + run: uv run black --check src tests + - name: Type check with mypy - run: mypy src/ - + run: uv run mypy src/ + + - name: Run tests + run: uv run pytest + - name: Test CLI installation run: | - article-cli --version - article-cli --help - + uv run article-cli --version + uv run article-cli --help + uv run article-cli doctor --help + uv run article-cli doctor --json > article-cli-doctor.json || true + python -m json.tool article-cli-doctor.json > /dev/null + - name: Generate test summary if: always() run: | @@ -51,6 +60,7 @@ jobs: echo "- ✅ Formatting (black): Passed" >> $GITHUB_STEP_SUMMARY echo "- ✅ Type checking (mypy): Passed" >> $GITHUB_STEP_SUMMARY echo "- ✅ CLI installation: Passed" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Doctor JSON smoke test: Passed" >> $GITHUB_STEP_SUMMARY build: needs: test @@ -59,23 +69,25 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 + + - name: Set up uv + uses: astral-sh/setup-uv@v8.1.0 with: - python-version: "3.11" - - - name: Install build dependencies - run: | - python -m pip install --upgrade pip - pip install build twine - + version: "latest" + enable-cache: true + + - name: Set up Python + run: uv python install 3.11 + + - name: Install dependencies with uv + run: uv sync --all-extras --dev --python 3.11 + - name: Build package - run: python -m build - + run: uv build + - name: Check package - run: twine check dist/* - + run: uv run twine check dist/* + - name: Upload artifacts uses: actions/upload-artifact@v4 with: @@ -93,4 +105,4 @@ jobs: echo "### 📋 Package Details" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY ls -la dist/ >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..942cfef --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,53 @@ +name: Documentation + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install documentation dependencies + run: npm ci + + - name: Build Antora site + run: npm run docs:build + + - name: Upload Pages artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v3 + with: + path: build/site + + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5c33ce5..9b5cc81 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,20 +13,19 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 + + - name: Set up uv + uses: astral-sh/setup-uv@v8.1.0 with: - python-version: "3.11" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - + version: "latest" + enable-cache: true + + - name: Set up Python + run: uv python install 3.11 + - name: Build package - run: python -m build - + run: uv build + - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 # Uses trusted publishing - no tokens needed! @@ -45,5 +44,5 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "### 📋 Installation" >> $GITHUB_STEP_SUMMARY echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY - echo "pip install --upgrade article-cli" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + echo "uv tool install article-cli" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 66bd807..0aef185 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,16 @@ share/python-wheels/ *.egg MANIFEST +# Node / Antora documentation +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Generated/downloaded font assets +fonts/ +fonts-test/ + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. @@ -139,4 +149,4 @@ dmypy.json Thumbs.db # Test configurations -test-config.toml \ No newline at end of file +test-config.toml diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0849f21..c5cc2e1 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -22,15 +22,17 @@ This guide helps you set up the article-cli package for development and distribu └── test_config.py ``` -2. **Install in development mode**: +2. **Install development dependencies with uv**: ```bash cd article-cli-package - pip install -e . + uv sync --all-extras --dev ``` -3. **Install development dependencies**: +3. **Run checks through uv**: ```bash - pip install -e ".[dev]" + uv run pytest + uv run black --check src tests + uv run mypy src ``` ## Building and Publishing @@ -38,11 +40,8 @@ This guide helps you set up the article-cli package for development and distribu ### Build the Package ```bash -# Install build tool -pip install build - # Build the package -python -m build +uv build ``` This creates `dist/` directory with: @@ -52,8 +51,8 @@ This creates `dist/` directory with: ### Test the Package Locally ```bash -# Install from wheel -pip install dist/article_cli-1.0.0-py3-none-any.whl +# Install the local wheel into uv's tool environment +uv tool install dist/article_cli-1.0.0-py3-none-any.whl --force # Test the CLI article-cli --help @@ -61,26 +60,21 @@ article-cli --help ### Publish to PyPI -1. **Install twine**: - ```bash - pip install twine - ``` - -2. **Test upload to TestPyPI first**: +1. **Test upload to TestPyPI first**: ```bash # Register at https://test.pypi.org/ first - twine upload --repository testpypi dist/* + uv run twine upload --repository testpypi dist/* ``` -3. **Install from TestPyPI to verify**: +2. **Install from TestPyPI to verify**: ```bash - pip install --index-url https://test.pypi.org/simple/ article-cli + uv tool install --index-url https://test.pypi.org/simple/ article-cli ``` -4. **Upload to real PyPI**: +3. **Upload to real PyPI**: ```bash # Register at https://pypi.org/ first - twine upload dist/* + uv run twine upload dist/* ``` ## Migration Strategy @@ -100,8 +94,8 @@ article-cli --help # Remove old a.cli rm a.cli - # Create requirements.txt - echo "article-cli>=1.0.0" > requirements.txt + # Create or update pyproject.toml with article-cli as a dependency + uv add article-cli # Update setup instructions in README ``` @@ -111,15 +105,12 @@ article-cli --help #!/bin/bash echo "Setting up article repository..." - # Install article-cli - pip install article-cli - - # Setup git hooks - article-cli setup + # Run article-cli from uv + uvx article-cli setup # Create local config if it doesn't exist if [ ! -f .article-cli.toml ]; then - article-cli config create + uvx article-cli config create echo "Please edit .article-cli.toml with your Zotero credentials" fi @@ -137,7 +128,7 @@ For each existing article repository: 2. **Install the package**: ```bash - pip install article-cli + uv tool install article-cli ``` 3. **Create local configuration**: @@ -196,7 +187,7 @@ auto_push = true ## Benefits of This Approach 1. **Centralized Maintenance**: Update once, benefit everywhere -2. **Easy Installation**: `pip install article-cli` +2. **Easy Installation**: `uv tool install article-cli` 3. **Configuration Management**: Project-specific and global settings 4. **Better Error Handling**: Improved user experience 5. **Documentation**: Comprehensive help and examples @@ -207,15 +198,15 @@ auto_push = true ```bash # Run tests -pytest tests/ +uv run pytest # Run with coverage -pytest --cov=article_cli tests/ +uv run pytest --cov=article_cli # Lint code -flake8 src/ -black src/ tests/ -mypy src/ +uv run flake8 src/ +uv run black src tests +uv run mypy src/ ``` ## Continuous Updates @@ -225,4 +216,4 @@ When you need to update the tool: 1. Make changes to the source code 2. Update version in `pyproject.toml` 3. Build and publish new version -4. Users update with: `pip install --upgrade article-cli` \ No newline at end of file +4. Users update with: `uv tool upgrade article-cli` diff --git a/PYPI_SETUP.md b/PYPI_SETUP.md index b62d9b8..5ec25dd 100644 --- a/PYPI_SETUP.md +++ b/PYPI_SETUP.md @@ -66,15 +66,15 @@ If automated publishing fails, you can upload the first version manually: ```bash # Build the package -python -m build +uv build # Upload to PyPI (you'll be prompted for credentials) -python -m twine upload dist/* +uv run twine upload dist/* # Or with token export TWINE_USERNAME=__token__ export TWINE_PASSWORD=pypi-your_token_here -python -m twine upload dist/* +uv run twine upload dist/* ``` ## Testing the Upload @@ -83,10 +83,10 @@ For testing, you can use TestPyPI: ```bash # Upload to TestPyPI first -python -m twine upload --repository testpypi dist/* +uv run twine upload --repository testpypi dist/* # Install from TestPyPI to test -pip install --index-url https://test.pypi.org/simple/ article-cli +uv tool install --index-url https://test.pypi.org/simple/ article-cli ``` ## Troubleshooting @@ -119,6 +119,6 @@ gh run list --limit 5 gh run view [run-id] --log # Test build locally -python -m build -python -m twine check dist/* -``` \ No newline at end of file +uv build +uv run twine check dist/* +``` diff --git a/README.md b/README.md index 06ef1a3..f31a240 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,19 @@ A command-line tool for managing LaTeX and Typst documents with git integration - **GitHub Actions Workflows**: Automated PDF compilation with XeLaTeX support, artifact upload, and GitHub releases - **Git Release Management**: Create, list, and delete releases with gitinfo2 support - **Zotero Integration**: Synchronize bibliography from Zotero with robust pagination and error handling +- **Lifecycle Commands**: Use `init`, `setup`, `doctor`, `bib update`, `compile`, `version`, and `release` - **LaTeX Build Management**: Clean build files and manage LaTeX compilation artifacts - **Git Hooks Setup**: Automated setup of git hooks for gitinfo2 integration +- **Repository Diagnostics**: `doctor` checks for git, hooks, build tools, Zotero, workflows, release readiness, and safe repairs with `--fix` - **Project Configuration**: Auto-generates pyproject.toml with article-cli settings -- **Documentation**: Creates README with build instructions and usage guide +- **Documentation**: Creates project README files and ships an Antora documentation site ## Installation ### From PyPI (recommended) ```bash -pip install article-cli +uv tool install article-cli ``` ### From Source @@ -34,9 +36,32 @@ pip install article-cli ```bash git clone https://github.com/feelpp/article.cli.git cd article.cli -pip install -e . +uv sync --all-extras --dev +uv run article-cli --help ``` +### Development Commands + +```bash +uv sync --all-extras --dev +uv run pytest +uv run black --check src tests +uv run mypy src +uv build +``` + +### Documentation Site + +The documentation site is built with Antora and the Feel++ Antora UI bundle: + +```bash +npm install +npm run docs:build +npm run docs:preview +``` + +The generated site is written to `build/site`. + ## Quick Start ### For New Projects @@ -62,8 +87,12 @@ pip install -e . 3. **Setup git hooks and update bibliography**: ```bash + article-cli doctor + article-cli doctor --fix + article-cli setup --dry-run article-cli setup - article-cli update-bibtex + article-cli bib update --dry-run + article-cli bib update ``` 4. **Commit and push** to trigger automated PDF compilation! @@ -72,6 +101,8 @@ pip install -e . 1. **Setup git hooks** (run once per repository): ```bash + article-cli doctor + article-cli doctor --fix article-cli setup ``` @@ -83,12 +114,15 @@ pip install -e . 3. **Update bibliography from Zotero**: ```bash - article-cli update-bibtex + article-cli bib update ``` -4. **Create a release**: +4. **Compile, refresh version metadata, and create a release**: ```bash - article-cli create v1.0.0 + article-cli compile + article-cli version + article-cli release v1.0.0 --dry-run + article-cli release v1.0.0 ``` ## Configuration @@ -111,9 +145,18 @@ group_id = "4678293" # Default for article.template output_file = "references.bib" [git] -auto_push = true +auto_push = false default_branch = "main" +[workflow] +runner_policy = "github" +github_runner = "ubuntu-24.04" +self_hosted_label = "self-texlive" +self_hosted_org = "" +bibliography = "off" +release = "github" +artifact_includes = [] + [latex] clean_extensions = [".aux", ".bbl", ".blg", ".log", ".out", ".synctex.gz"] @@ -169,6 +212,13 @@ article-cli init --title "My Article" --authors "Author" --tex-file article.tex # Force overwrite existing files article-cli init --title "My Article" --authors "Author" --force + +# Generate CI that checks Zotero bibliography freshness +article-cli init --title "My Article" --authors "Author" --ci-bib check + +# Generate CI with opt-in self-hosted runner discovery +article-cli init --title "My Article" --authors "Author" \ + --ci-runner-policy self-hosted-auto --ci-self-hosted-org my-org ``` The `init` command sets up: @@ -182,27 +232,48 @@ The `init` command sets up: ### Git Release Management ```bash -# Create a new release -article-cli create v1.2.3 +# Refresh gitinfo2 version metadata +article-cli version + +# Refresh metadata, compile, and check the PDF version text +article-cli version --compile --check-pdf + +# Preview a new release tag +article-cli release v1 --dry-run + +# Create a checked local release tag +article-cli release v1 + +# Check bibliography freshness during release, then push the tag +article-cli release v1 --bib check --push # List recent releases article-cli list --count 10 # Delete a release article-cli delete v1.2.3 + +# Deprecated alias retained for compatibility +article-cli create v1.2.3 ``` ### Bibliography Management ```bash +# Preview bibliography update +article-cli bib update --dry-run + # Update bibliography from Zotero -article-cli update-bibtex +article-cli bib update # Specify custom output file -article-cli update-bibtex --output my-refs.bib +article-cli bib update --output my-refs.bib # Skip backup creation -article-cli update-bibtex --no-backup +article-cli bib update --no-backup + +# Deprecated alias retained for compatibility +article-cli update-bibtex ``` ### LaTeX Compilation @@ -334,6 +405,15 @@ article-cli compile presentation.typ --font-path fonts/ ### Project Setup ```bash +# Diagnose repository readiness without modifying files +article-cli doctor + +# Emit machine-readable diagnostics for CI +article-cli doctor --json + +# Apply safe repairs for hooks, output directories, and gitHeadLocal.gin +article-cli doctor --fix + # Setup git hooks for gitinfo2 article-cli setup @@ -345,24 +425,28 @@ article-cli clean ```bash # Override configuration via command line -article-cli update-bibtex --api-key YOUR_KEY --group-id YOUR_GROUP +article-cli bib update --api-key YOUR_KEY --group-id YOUR_GROUP # Specify custom configuration file -article-cli --config custom-config.toml update-bibtex +article-cli --config custom-config.toml bib update ``` -## Version Format +## Release Tags + +The default paper release policy accepts short and full version tags: +- `vX` for paper milestones (e.g., `v1`) +- `vX.Y` or `vX.Y.Z` for more detailed paper releases +- `vX.Y.Z-rc.N`, `vX.Y.Z-beta.N`, or similar pre-release suffixes -Release versions must follow the semantic versioning format: -- `vX.Y.Z` for stable releases (e.g., `v1.2.3`) -- `vX.Y.Z-pre.N` for pre-releases (e.g., `v1.2.3-pre.1`) +Use `[tool.article-cli.release] tag_policy = "semver"` when a repository must require strict `vX.Y.Z` semantic-version tags. The release command does not commit, force-retag, push, or create a GitHub release unless the corresponding explicit flag is passed. ## Requirements -- Python 3.8+ +- Python 3.9+ - Git repository with gitinfo2 package (for LaTeX integration) - Zotero account with API access (for bibliography features) - Typst CLI (for Typst compilation) - install from https://typst.app/ +- Node.js 20+ for building the Antora documentation site ## License @@ -378,6 +462,13 @@ MIT License - see LICENSE file for details. ## Changelog +### v1.5.0 +- Add `doctor` diagnostics with JSON output and safe `--fix` repairs +- Split CLI implementation into command modules +- Make setup hooks more robust and worktree-safe +- Standardize development and CI workflows on `uv` +- Add an Antora documentation site using the Feel++ Antora UI + ### v1.4.0 - Add full Typst document compilation support - New `TypstCompiler` class for Typst documents @@ -411,4 +502,4 @@ MIT License - see LICENSE file for details. - Git release management - Zotero bibliography synchronization - LaTeX build file cleanup -- Configuration file support \ No newline at end of file +- Configuration file support diff --git a/antora-playbook.yml b/antora-playbook.yml new file mode 100644 index 0000000..7ec9355 --- /dev/null +++ b/antora-playbook.yml @@ -0,0 +1,22 @@ +site: + title: Article CLI + url: https://feelpp.github.io/article.cli + start_page: article-cli::index.adoc + +content: + sources: + - url: . + branches: HEAD + start_path: docs + +ui: + bundle: + url: https://github.com/feelpp/antora-ui/releases/download/v0.52/ui-bundle.zip + snapshot: true + +output: + dir: build/site + +runtime: + cache_dir: .cache/antora + fetch: true diff --git a/docs/antora.yml b/docs/antora.yml new file mode 100644 index 0000000..b918362 --- /dev/null +++ b/docs/antora.yml @@ -0,0 +1,17 @@ +name: article-cli +title: Article CLI +version: '1.5' +start_page: ROOT:index.adoc +nav: + - modules/ROOT/nav.adoc + +asciidoc: + attributes: + project-name: article-cli + cli-command: article-cli + python-supported: '3.9, 3.10, 3.11, 3.12, 3.13' + article-cli-version: '1.5.0' + template-version: '1' + zotero-group-example: '4709047' + page-pagination: '' + source-highlighter: highlight.js diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc new file mode 100644 index 0000000..0eb4bb9 --- /dev/null +++ b/docs/modules/ROOT/nav.adoc @@ -0,0 +1,13 @@ +* xref:index.adoc[Overview] +* xref:install.adoc[Install] +* xref:lifecycle.adoc[Paper Lifecycle] +* Tutorials +** xref:tutorials/new-paper.adoc[Initialize a New Paper] +** xref:tutorials/existing-paper.adoc[Set Up an Existing Paper] +** xref:tutorials/release-paper.adoc[Release a Paper] +* xref:configuration.adoc[Configuration] +* xref:migration.adoc[Migration Notes] +* Reference +** xref:reference/commands.adoc[Command Reference] +** xref:reference/templates.adoc[Templates and Styles] +** xref:reference/docs-site.adoc[Documentation Site] diff --git a/docs/modules/ROOT/pages/configuration.adoc b/docs/modules/ROOT/pages/configuration.adoc new file mode 100644 index 0000000..ccef2ab --- /dev/null +++ b/docs/modules/ROOT/pages/configuration.adoc @@ -0,0 +1,93 @@ += Configuration +:description: Configure article-cli through pyproject.toml, local config, CLI flags, and environment variables. + +`article-cli` resolves settings from command-line flags, project configuration, local configuration files, and environment variables. +Command-line flags are intended for explicit one-off overrides. +Repository policy should live in `pyproject.toml`. + +== Project Metadata + +[source,toml] +---- +[tool.article-cli.template] +version = "1" + +[tool.article-cli.project] +type = "article" +style = "ieee" + +[tool.article-cli.documents] +main = "main.tex" +additional = ["supplement.tex"] +---- + +Generated repositories record the template version so future migrations can identify the generator layout. + +== Bibliography + +[source,toml] +---- +[tool.article-cli.zotero] +group_id = "4709047" +collection_id = "" +output_file = "references.bib" +local_file = "local_references.bib" +deterministic = true +---- + +Use environment variables for secrets: + +[source,console] +---- +export ZOTERO_API_KEY="your-api-key" +export ZOTERO_GROUP_ID="4709047" +---- + +== Build + +[source,toml] +---- +[tool.article-cli.latex] +engine = "latexmk" +shell_escape = false +build_dir = "." + +[tool.article-cli.typst] +build_dir = "build" +font_paths = ["fonts"] +---- + +== Workflow + +[source,toml] +---- +[tool.article-cli.workflow] +runner_policy = "github" +github_runner = "ubuntu-24.04" +self_hosted_label = "self-texlive" +self_hosted_org = "" +bibliography = "off" +release = "github" +artifact_includes = [] +output_dir = "build" +---- + +`runner_policy` can keep generated workflows on GitHub-hosted runners or opt into self-hosted runners. +Bibliography behavior can be disabled, checked, updated, or required depending on repository policy. + +== Release + +[source,toml] +---- +[tool.article-cli.release] +tag_policy = "paper" +allow_dirty = false +compile = true +check_pdf = true +checksum = true +bibliography = "off" +github_release = false +---- + +The default `paper` tag policy accepts `v1`, `v1.0`, `v1.0.0`, and release-candidate suffixes. +Use `tag_policy = "semver"` for strict semantic-version tags. diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc new file mode 100644 index 0000000..d0d97bd --- /dev/null +++ b/docs/modules/ROOT/pages/index.adoc @@ -0,0 +1,33 @@ += Article CLI +:description: Manage reproducible LaTeX and Typst paper repositories with git, Zotero, CI, and release diagnostics. + +`article-cli` manages the operational lifecycle of scientific writing repositories. +It initializes LaTeX or Typst papers, installs gitinfo2 hooks, synchronizes Zotero references, compiles PDFs, records version metadata, and creates checked paper releases. + +The tool is designed for repositories where the paper source, bibliography policy, build configuration, CI workflow, and release metadata are part of the same reproducibility contract. + +== Main Capabilities + +* Initialize article, presentation, poster, and Typst repositories from versioned templates. +* Keep project policy in `pyproject.toml` under `[tool.article-cli]`. +* Install managed git hooks for `gitinfo2` without replacing unrelated user hook logic. +* Run `doctor` diagnostics, with optional safe repairs through `doctor --fix`. +* Compile LaTeX and Typst documents with clear engine, output, page-count, citation, reference, and overfull-box diagnostics. +* Synchronize deterministic BibTeX from Zotero, with optional local reference preservation. +* Create checked release tags that refresh metadata, compile the PDF, verify the version string, and optionally write checksums. +* Generate GitHub Actions workflows that use `uv`, build the document, upload artifacts, and optionally create GitHub releases. + +== Typical Workflow + +[source,console] +---- +uv tool install article-cli +article-cli init --title "My Paper" --authors "A. Author,B. Author" +article-cli doctor --fix +article-cli bib update +article-cli compile +article-cli release v1 --dry-run +article-cli release v1 +---- + +For the full workflow, see xref:lifecycle.adoc[]. diff --git a/docs/modules/ROOT/pages/install.adoc b/docs/modules/ROOT/pages/install.adoc new file mode 100644 index 0000000..abdaf2a --- /dev/null +++ b/docs/modules/ROOT/pages/install.adoc @@ -0,0 +1,57 @@ += Install +:description: Install article-cli with uv and set up a development checkout. + +== User Installation + +Install the published command with `uv`: + +[source,console] +---- +uv tool install article-cli +article-cli --help +---- + +Upgrade an existing tool installation with: + +[source,console] +---- +uv tool upgrade article-cli +---- + +== Development Checkout + +[source,console] +---- +git clone https://github.com/feelpp/article.cli.git +cd article.cli +uv sync --all-extras --dev +uv run article-cli --help +---- + +The supported Python versions are {python-supported}. + +== Development Checks + +[source,console] +---- +uv run pytest +uv run black --check src tests +uv run flake8 src/ --count --select=E9,F63,F7,F82 --show-source --statistics +uv run mypy src/ +uv build +---- + +The GitHub Actions workflow runs the test suite across Python 3.9 through 3.13. + +== Documentation Site + +The documentation is an Antora site using the Feel++ Antora UI bundle. + +[source,console] +---- +npm install +npm run docs:build +npm run docs:preview +---- + +The generated site is written to `build/site`. diff --git a/docs/modules/ROOT/pages/lifecycle.adoc b/docs/modules/ROOT/pages/lifecycle.adoc new file mode 100644 index 0000000..4bd35c7 --- /dev/null +++ b/docs/modules/ROOT/pages/lifecycle.adoc @@ -0,0 +1,60 @@ += Paper Lifecycle +:description: The article-cli command sequence for reproducible paper repositories. + +`article-cli` separates the paper lifecycle into explicit commands. +Each command owns a narrow part of the repository state. + +== Command Roles + +`init`:: +Create the repository files for a new paper: document template, project configuration, workflow, README, ignore rules, editor settings, and template metadata. + +`setup`:: +Install managed gitinfo2 hooks and refresh `gitHeadLocal.gin`. +The command is idempotent and preserves existing shell hook logic. + +`doctor`:: +Report repository, hook, build, bibliography, workflow, and release readiness. +Use `doctor --json` in CI and `doctor --fix` for safe local repairs. + +`bib update`:: +Refresh deterministic BibTeX from Zotero. +Use `--check` in CI or release preparation when the checked-in bibliography must already be current. + +`compile`:: +Build the configured document. +The command reports the selected engine, output directory, version metadata, page count when `pdfinfo` is available, and common LaTeX warning counts. + +`version`:: +Refresh and report gitinfo2 metadata without creating a tag. + +`release`:: +Create a checked paper release. +The release flow validates the tag, checks the working tree, optionally checks or updates the bibliography, refreshes metadata, compiles before and after tagging, verifies the PDF text, writes a checksum, and optionally pushes or creates a GitHub release. + +== Recommended Local Flow + +[source,console] +---- +article-cli doctor +article-cli doctor --fix +article-cli bib update --dry-run +article-cli bib update +article-cli compile +article-cli version +article-cli release v1 --dry-run +article-cli release v1 +---- + +== CI Flow + +Generated GitHub Actions workflows use `uv`, install `article-cli`, run `article-cli setup`, upload `article-cli doctor --json`, compile the document, and upload the resulting PDF. +Bibliography and release behavior are controlled by `[tool.article-cli.workflow]`. + +== Release Guarantees and Limits + +The release command is intentionally conservative. +It does not commit, force-retag, push, or create a GitHub release unless the corresponding flag or configuration enables that behavior. + +It verifies repository consistency, but it does not remove normal HPC or TeX variability. +The useful guarantee is traceability: the source state, generated metadata, PDF text, release tag, and checksum are checked together. diff --git a/docs/modules/ROOT/pages/migration.adoc b/docs/modules/ROOT/pages/migration.adoc new file mode 100644 index 0000000..ca0785f --- /dev/null +++ b/docs/modules/ROOT/pages/migration.adoc @@ -0,0 +1,90 @@ += Migration Notes +:description: Move older article-cli repositories to the current lifecycle. + +Older repositories may lack template metadata, generated workflow policy, managed hook blocks, or the release configuration introduced in the current lifecycle. + +== Recommended Migration + +1. Install the current tool. ++ +[source,console] +---- +uv tool install article-cli +---- + +2. Add template metadata if missing. ++ +[source,toml] +---- +[tool.article-cli.template] +version = "1" +---- + +3. Ensure the main document is configured. ++ +[source,toml] +---- +[tool.article-cli.documents] +main = "main.tex" +---- + +4. Add release policy. ++ +[source,toml] +---- +[tool.article-cli.release] +tag_policy = "paper" +compile = true +check_pdf = true +checksum = true +bibliography = "off" +github_release = false +---- + +5. Add workflow policy if the repository uses generated GitHub Actions. ++ +[source,toml] +---- +[tool.article-cli.workflow] +runner_policy = "github" +github_runner = "ubuntu-24.04" +bibliography = "off" +release = "github" +artifact_includes = [] +---- + +6. Run diagnostics and safe repairs. ++ +[source,console] +---- +article-cli doctor +article-cli doctor --fix +---- + +7. Compile and verify version metadata. ++ +[source,console] +---- +article-cli version +article-cli compile +---- + +== Common Changes + +`create` command:: +`article-cli create` is retained as a compatibility alias. +Use `article-cli release` in new documentation and scripts. + +Inline workflows:: +Generated workflows are now rendered from package Jinja2 templates and record `article-cli` minimum version and template version. + +Repository-owned hooks:: +The repository-owned `hooks/post-commit` source is created when missing. +Managed git hook blocks are installed into `.git/hooks` while preserving existing shell hook content. + +Bibliography files:: +Use `local_references.bib` for manual BibTeX entries that should not be lost when Zotero-managed `references.bib` is refreshed. + +Release tags:: +Paper tags such as `v1` are accepted by the default `paper` policy. +Use `semver` only for repositories that require strict `vX.Y.Z` tags. diff --git a/docs/modules/ROOT/pages/reference/commands.adoc b/docs/modules/ROOT/pages/reference/commands.adoc new file mode 100644 index 0000000..aa0dffc --- /dev/null +++ b/docs/modules/ROOT/pages/reference/commands.adoc @@ -0,0 +1,81 @@ += Command Reference +:description: High-level reference for article-cli commands. + +This page summarizes the command surface. +Run `article-cli COMMAND --help` for exact options. + +== Initialization + +[source,console] +---- +article-cli init --title "Title" --authors "A. Author,B. Author" +article-cli init --type presentation --theme numpex +article-cli init --type typst-article +article-cli init --style ieee +article-cli init --template path/to/custom-template.j2 +---- + +== Setup and Diagnostics + +[source,console] +---- +article-cli setup +article-cli setup --dry-run +article-cli doctor +article-cli doctor --fix +article-cli doctor --json +article-cli doctor --tag v1 +---- + +== Bibliography + +[source,console] +---- +article-cli bib update +article-cli bib update --dry-run +article-cli bib update --check +article-cli bib update --include-local --merged-output references.all.bib +---- + +== Build + +[source,console] +---- +article-cli compile +article-cli compile main.tex +article-cli compile --engine xelatex +article-cli compile --engine typst main.typ +article-cli compile --watch +article-cli compile --clean-first --clean-after +---- + +== Version and Release + +[source,console] +---- +article-cli version +article-cli version --dry-run +article-cli release v1 --dry-run +article-cli release v1 +article-cli release v1 --bib check --push +article-cli release v1 --github-release --push +article-cli list --count 10 +article-cli delete v1 +---- + +== Assets + +[source,console] +---- +article-cli install-fonts +article-cli install-theme --list +article-cli install-theme numpex +---- + +== Configuration + +[source,console] +---- +article-cli config show +article-cli --config .article-cli.toml config show +---- diff --git a/docs/modules/ROOT/pages/reference/docs-site.adoc b/docs/modules/ROOT/pages/reference/docs-site.adoc new file mode 100644 index 0000000..51974c2 --- /dev/null +++ b/docs/modules/ROOT/pages/reference/docs-site.adoc @@ -0,0 +1,50 @@ += Documentation Site +:description: Build and publish the Antora documentation site. + +The documentation site is built with Antora and the Feel++ Antora UI bundle. + +== Local Build + +[source,console] +---- +npm install +npm run docs:build +---- + +The site is generated in `build/site`. + +Preview locally with: + +[source,console] +---- +npm run docs:preview +---- + +== Playbook + +The root `antora-playbook.yml` points to the local `docs` component and uses the Feel++ UI bundle: + +[source,yaml] +---- +ui: + bundle: + url: https://github.com/feelpp/antora-ui/releases/download/v0.52/ui-bundle.zip + snapshot: true +---- + +== Component Layout + +[source,text] +---- +docs/ +|-- antora.yml +`-- modules/ + `-- ROOT/ + |-- nav.adoc + `-- pages/ +---- + +== Publishing + +The `docs.yml` GitHub Actions workflow builds the site and deploys `build/site` to GitHub Pages on pushes to `main`. +Pull requests build the site without deploying it. diff --git a/docs/modules/ROOT/pages/reference/templates.adoc b/docs/modules/ROOT/pages/reference/templates.adoc new file mode 100644 index 0000000..660b5b9 --- /dev/null +++ b/docs/modules/ROOT/pages/reference/templates.adoc @@ -0,0 +1,58 @@ += Templates and Styles +:description: article-cli template metadata, built-in styles, and custom templates. + +Generated repositories record the template version in `pyproject.toml`: + +[source,toml] +---- +[tool.article-cli.template] +version = "1" +---- + +This metadata gives future migration tooling a stable way to identify which repository layout generated the paper. + +== Built-In Project Types + +`article`:: +LaTeX article repository. + +`presentation`:: +LaTeX Beamer repository. + +`poster`:: +LaTeX poster repository. + +`typst-article`:: +Typst article repository. + +`typst-presentation`:: +Typst presentation repository. + +`typst-poster`:: +Typst poster repository. + +== Article Styles + +The article initializer supports built-in styles such as `default`, `ieee`, and `lncs`. + +[source,console] +---- +article-cli init --title "Paper" --authors "A. Author" --style ieee +article-cli init --title "Paper" --authors "A. Author" --style lncs +---- + +== Custom Templates + +Use a Jinja2 template for local or conference-specific requirements: + +[source,console] +---- +article-cli init --title "Paper" --authors "A. Author" --template templates/paper.tex.j2 +---- + +Custom templates receive the same context as built-in templates, including title, authors, project type, style, and template version. + +== Workflow Templates + +Generated GitHub Actions workflows are package templates. +The workflow records the template version and minimum `article-cli` version in comments and installs `article-cli` through `uv`. diff --git a/docs/modules/ROOT/pages/tutorials/existing-paper.adoc b/docs/modules/ROOT/pages/tutorials/existing-paper.adoc new file mode 100644 index 0000000..3cb47ce --- /dev/null +++ b/docs/modules/ROOT/pages/tutorials/existing-paper.adoc @@ -0,0 +1,73 @@ += Set Up an Existing Paper +:description: Bring an existing LaTeX or Typst repository under article-cli management. + +Use this flow when the paper already has source files and possibly an existing bibliography. + +== Add Project Configuration + +Create or update `pyproject.toml`: + +[source,toml] +---- +[tool.article-cli.documents] +main = "main.tex" + +[tool.article-cli.latex] +engine = "latexmk" +build_dir = "." + +[tool.article-cli.zotero] +group_id = "4709047" +output_file = "references.bib" +local_file = "local_references.bib" +deterministic = true + +[tool.article-cli.release] +tag_policy = "paper" +compile = true +check_pdf = true +checksum = true +---- + +For Typst, set the main document to a `.typ` file and configure `[tool.article-cli.typst]` if a build directory or font paths are needed. + +== Preserve Existing References + +If the repository already has a curated `references.bib`, do not overwrite it blindly. +Move manual entries that are not managed by Zotero into `local_references.bib`, then run: + +[source,console] +---- +article-cli bib update --dry-run +article-cli bib update --include-local --merged-output references.all.bib +---- + +Use the merged file only if the paper is configured to cite both Zotero-managed and local entries. + +== Install Hooks and Check Readiness + +[source,console] +---- +article-cli doctor +article-cli doctor --fix +article-cli setup +---- + +`setup` and `doctor --fix` are safe to rerun. +Existing shell hooks are preserved and the article-cli managed gitinfo2 block is updated in place. + +== Compile + +[source,console] +---- +article-cli compile +---- + +If compilation fails, run: + +[source,console] +---- +article-cli doctor +---- + +The report highlights missing tools, wrong engine selection, missing output directories, stale version metadata, missing bibliography files, and workflow issues. diff --git a/docs/modules/ROOT/pages/tutorials/new-paper.adoc b/docs/modules/ROOT/pages/tutorials/new-paper.adoc new file mode 100644 index 0000000..ee661f1 --- /dev/null +++ b/docs/modules/ROOT/pages/tutorials/new-paper.adoc @@ -0,0 +1,77 @@ += Initialize a New Paper +:description: Create a new LaTeX or Typst paper repository with article-cli. + +This tutorial starts from an empty or existing Git repository. + +== Create the Repository + +[source,console] +---- +mkdir my-paper +cd my-paper +git init +article-cli init --title "My Paper" --authors "A. Author,B. Author" +---- + +For Typst: + +[source,console] +---- +article-cli init --title "My Paper" --authors "A. Author" --type typst-article +---- + +For a specific LaTeX style: + +[source,console] +---- +article-cli init --title "My Paper" --authors "A. Author" --style ieee +article-cli init --title "My Paper" --authors "A. Author" --style lncs +---- + +== Inspect and Repair Setup + +[source,console] +---- +article-cli doctor +article-cli doctor --fix +---- + +`doctor --fix` can create the configured output directory, create the repository-owned hook source, install managed git hooks, and refresh `gitHeadLocal.gin`. +It does not commit, tag, push, or overwrite unrelated user hook logic. + +== Configure Zotero + +Use environment variables for credentials: + +[source,console] +---- +export ZOTERO_API_KEY="your-api-key" +export ZOTERO_GROUP_ID="{zotero-group-example}" +---- + +Project defaults live in `pyproject.toml`. + +== Build + +[source,console] +---- +article-cli bib update --dry-run +article-cli bib update +article-cli compile +---- + +== First Commit + +[source,console] +---- +git add . +git commit -m "Initialize paper repository" +---- + +After the first commit, rerun: + +[source,console] +---- +article-cli version +article-cli compile +---- diff --git a/docs/modules/ROOT/pages/tutorials/release-paper.adoc b/docs/modules/ROOT/pages/tutorials/release-paper.adoc new file mode 100644 index 0000000..2e40a10 --- /dev/null +++ b/docs/modules/ROOT/pages/tutorials/release-paper.adoc @@ -0,0 +1,71 @@ += Release a Paper +:description: Create a checked paper release with article-cli. + +The release command binds the tag, source state, gitinfo2 metadata, compiled PDF, and optional checksum together. + +== Check the Repository + +[source,console] +---- +article-cli doctor --tag v1 +article-cli release v1 --dry-run +---- + +The dry run does not create tags, commits, builds, checksums, pushes, or GitHub releases. + +== Create a Local Release + +[source,console] +---- +article-cli release v1 +---- + +By default, the command: + +* validates the tag according to `[tool.article-cli.release].tag_policy`; +* rejects dirty files other than `gitHeadLocal.gin`; +* refreshes gitinfo2 metadata; +* compiles before tagging; +* creates the annotated tag; +* refreshes metadata again; +* compiles after tagging; +* checks that the PDF text contains the release tag; +* writes a `.sha256` sidecar when checksums are enabled. + +== Bibliography Policy + +To require the checked-in bibliography to be current: + +[source,console] +---- +article-cli release v1 --bib check +---- + +To update the bibliography during the release: + +[source,console] +---- +article-cli release v1 --bib update +---- + +== Push and GitHub Release + +[source,console] +---- +article-cli release v1 --push +article-cli release v1 --github-release --push +---- + +These operations are explicit. +The default release command leaves pushing and GitHub release creation to the user. + +== If the Post-Tag Check Fails + +If a failure occurs after the local tag has been created, the command prints rollback guidance: + +[source,console] +---- +git tag -d v1 +---- + +Fix the issue, rerun the dry run, then recreate the release. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1e7801a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4098 @@ +{ + "name": "article-cli-docs", + "version": "1.5.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "article-cli-docs", + "version": "1.5.0", + "devDependencies": { + "@antora/cli": "3.1.14", + "@antora/site-generator": "3.1.14", + "handlebars-utils": "1.0.6", + "http-server": "14.1.1" + } + }, + "node_modules/@antora/asciidoc-loader": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/asciidoc-loader/-/asciidoc-loader-3.1.14.tgz", + "integrity": "sha512-4xxisnoBFrlLNY6f3xZtyyfgm+tBLsqesTcEStfc8jtXUMYJ4b2DWIzo1vULmxvZ7yY5+Q7YqEvS5o6kIWAG0A==", + "dev": true, + "dependencies": { + "@antora/logger": "3.1.14", + "@antora/user-require-helper": "~3.0", + "@asciidoctor/core": "~2.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/cli": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/cli/-/cli-3.1.14.tgz", + "integrity": "sha512-I6WcygMU2bFInjdURJjkYjo7K5M8B3lBB53v9OO0IcY0LhEY8Wa7IlZ7wVinf5qEjHvaYzRGTZVl6RsJtVt7Sw==", + "dev": true, + "dependencies": { + "@antora/logger": "3.1.14", + "@antora/playbook-builder": "3.1.14", + "@antora/user-require-helper": "~3.0", + "commander": "~11.1" + }, + "bin": { + "antora": "bin/antora" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/content-aggregator": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/content-aggregator/-/content-aggregator-3.1.14.tgz", + "integrity": "sha512-FVuBgnrGPiktYqK1WHbGF8O8l4m5KHlkxoJumrbacgFo8SKuiRFEo31zalxrCUsv8QM3UBEgX+LdHrve/9CGLg==", + "dev": true, + "dependencies": { + "@antora/expand-path-helper": "~3.0", + "@antora/logger": "3.1.14", + "@antora/user-require-helper": "~3.0", + "braces": "~3.0", + "cache-directory": "~2.0", + "fast-glob": "~3.3", + "hpagent": "~1.2", + "isomorphic-git": "~1.25", + "js-yaml": "~4.1", + "multi-progress": "~4.0", + "picomatch": "~4.0", + "progress": "~2.0", + "should-proxy": "~1.0", + "simple-get": "~4.0", + "vinyl": "~3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/content-classifier": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/content-classifier/-/content-classifier-3.1.14.tgz", + "integrity": "sha512-y8Fk+KU1lqD3aawOu3ZFK92YfOZ1k3YBJhLI9QIFM6Ck4STPnf7AwYbhfOtjODlwer5/OhFmfhjUB2hn7onGnA==", + "dev": true, + "dependencies": { + "@antora/asciidoc-loader": "3.1.14", + "@antora/logger": "3.1.14", + "mime-types": "~2.1", + "vinyl": "~3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/document-converter": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/document-converter/-/document-converter-3.1.14.tgz", + "integrity": "sha512-f6wFnL+489DI0ZDgoxYWzbxxWqPviRiJ56OHS1NixEfvJ7OpRBDPEbX1xnsIeiyFBgqX4+nY92MsCWKTa+Gf3w==", + "dev": true, + "dependencies": { + "@antora/asciidoc-loader": "3.1.14" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/expand-path-helper": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@antora/expand-path-helper/-/expand-path-helper-3.0.0.tgz", + "integrity": "sha512-7PdEIhk97v85/CSm3HynCsX14TR6oIVz1s233nNLsiWubE8tTnpPt4sNRJR+hpmIZ6Bx9c6QDp3XIoiyu/WYYA==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/file-publisher": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/file-publisher/-/file-publisher-3.1.14.tgz", + "integrity": "sha512-fTaAnkyKSOlsxQM1TBFCAmiERA6Q67XleDCD2bMPVgfcENmo0Xfx59KwCHaA92IcRSmMftydlXHPaFxNh0UVsg==", + "dev": true, + "dependencies": { + "@antora/expand-path-helper": "~3.0", + "@antora/user-require-helper": "~3.0", + "vinyl": "~3.0", + "yazl": "~2.5" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/logger": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/logger/-/logger-3.1.14.tgz", + "integrity": "sha512-kVEeGqZbXR903hPIm+BlN97fLdQ3LoUzE/BOPZ6vRp9m9Mmbnm67Kg7fSYkfTMLB0S2UWpAPFg22RdsU5ZoAzA==", + "dev": true, + "dependencies": { + "@antora/expand-path-helper": "~3.0", + "pino": "~9.2", + "pino-pretty": "~11.2", + "sonic-boom": "~4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/navigation-builder": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/navigation-builder/-/navigation-builder-3.1.14.tgz", + "integrity": "sha512-/637YLGD7oUHGSfEfszXkk4ASfIhDAg5Xs9035J1dV07XYRlGqmtUb15rtapbcECpcQFjCyM5jFQYSNNvLrGcQ==", + "dev": true, + "dependencies": { + "@antora/asciidoc-loader": "3.1.14" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/page-composer": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/page-composer/-/page-composer-3.1.14.tgz", + "integrity": "sha512-RfA+67TxCqUPrQbZdrfjgLpHh8MR2z2du7cyF3HGX4N6DpqEBvz81NHHl3rA3fj6BQZPQbGm2OYAMU6wzJ6Pog==", + "dev": true, + "dependencies": { + "@antora/logger": "3.1.14", + "handlebars": "~4.7", + "require-from-string": "~2.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/playbook-builder": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/playbook-builder/-/playbook-builder-3.1.14.tgz", + "integrity": "sha512-Ss2r7In00u/n9Da+JOxEqIE8NeRosf+f+agzH3Te09JV/mpgZKxEOE5V/VuP+TNNq4ww1eu5aOS8DiU2PYwj4Q==", + "dev": true, + "dependencies": { + "@iarna/toml": "~2.2", + "convict": "~6.2", + "js-yaml": "~4.1", + "json5": "~2.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/redirect-producer": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/redirect-producer/-/redirect-producer-3.1.14.tgz", + "integrity": "sha512-5koAwRk1cZrvE/qfOWKXqb3jtxrZbWA5EYHYGFEoato5By3cbC42blH4Bre9/48pjyS6znFpbZhYUBpT7PRhZA==", + "dev": true, + "dependencies": { + "vinyl": "~3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/site-generator": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/site-generator/-/site-generator-3.1.14.tgz", + "integrity": "sha512-hQIUVtM9+xwleYWc4fIRZmiKl2p+ItOJuUm2+Hkdh07BZsySxkMOxxCyZsvTn9rc+4R94CYqDQCYElwFwdB2WA==", + "dev": true, + "dependencies": { + "@antora/asciidoc-loader": "3.1.14", + "@antora/content-aggregator": "3.1.14", + "@antora/content-classifier": "3.1.14", + "@antora/document-converter": "3.1.14", + "@antora/file-publisher": "3.1.14", + "@antora/logger": "3.1.14", + "@antora/navigation-builder": "3.1.14", + "@antora/page-composer": "3.1.14", + "@antora/playbook-builder": "3.1.14", + "@antora/redirect-producer": "3.1.14", + "@antora/site-mapper": "3.1.14", + "@antora/site-publisher": "3.1.14", + "@antora/ui-loader": "3.1.14", + "@antora/user-require-helper": "~3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/site-mapper": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/site-mapper/-/site-mapper-3.1.14.tgz", + "integrity": "sha512-3qbETtwadl+fWREjzrBUxPUorMcMiZ+hdkB1El9z7it9KzKh0Yp7Je0+2uTxGX+Lov9uik48dZJ9e/mr5PeaRQ==", + "dev": true, + "dependencies": { + "@antora/content-classifier": "3.1.14", + "vinyl": "~3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/site-publisher": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/site-publisher/-/site-publisher-3.1.14.tgz", + "integrity": "sha512-8apyEmgepUc7ms9CTEIPwN3tGtWwLqR6fbLMLs7hibqmOSR880Ut/4GRGb97sqcGQXSHdIyWK2oJKzRl1Akb6Q==", + "dev": true, + "dependencies": { + "@antora/file-publisher": "3.1.14" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/ui-loader": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/ui-loader/-/ui-loader-3.1.14.tgz", + "integrity": "sha512-LVvTdKQOB44CmJ1JQDu8sJf6rrLZMxPAWWackdg2JtGyGHHpd80/MBcv4BSFk7//cJQ13Oqm/7JCbhD51KAFjg==", + "dev": true, + "dependencies": { + "@antora/expand-path-helper": "~3.0", + "braces": "~3.0", + "cache-directory": "~2.0", + "fast-glob": "~3.3", + "hpagent": "~1.2", + "js-yaml": "~4.1", + "picomatch": "~4.0", + "should-proxy": "~1.0", + "simple-get": "~4.0", + "vinyl": "~3.0", + "yauzl": "~3.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@antora/user-require-helper": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@antora/user-require-helper/-/user-require-helper-3.0.0.tgz", + "integrity": "sha512-KIXb8WYhnrnwH7Jj21l1w+et9k5GvcgcqvLOwxqWLEd0uVZOiMFdqFjqbVm3M+zcrs1JXWMeh2LLvxBbQs3q/Q==", + "dev": true, + "dependencies": { + "@antora/expand-path-helper": "~3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@asciidoctor/core": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-2.2.9.tgz", + "integrity": "sha512-tIPRHo1T2SFmAm+j77cDsj0RuaszP7xJxsaVTTAF5CwKyTbazw9TnIVlpIWM5yWfIWAWcAZy92RcnPgMJwny1w==", + "dev": true, + "dependencies": { + "asciidoctor-opal-runtime": "0.3.4", + "unxhr": "~1.2" + }, + "engines": { + "node": ">=8.11", + "npm": ">=5.0.0", + "yarn": ">=1.1.0" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asciidoctor-opal-runtime": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.4.tgz", + "integrity": "sha512-zqd6zn1LV+PZ69AP/kEbB00zuPHMIAJY3IX8+aZV+X1qOwatYvKGjsMmdMc5ApfhtkjZ4mYkqiTPJWnEnBiMJg==", + "dev": true, + "dependencies": { + "fast-glob": "~3.3", + "unxhr": "~1.2" + }, + "engines": { + "node": ">=8.11" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cache-directory": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cache-directory/-/cache-directory-2.0.0.tgz", + "integrity": "sha512-7YKEapH+2Uikde8hySyfobXBqPKULDyHNl/lhKm7cKf/GJFdG/tU/WpLrOg2y9aUrQrWUilYqawFIiGJPS6gDA==", + "dev": true, + "dependencies": { + "xdg-basedir": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==", + "dev": true + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/convict": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.5.tgz", + "integrity": "sha512-JtXpxqDqJ8P0UwEHwhxLzCIXQy97vlYBZR222Sbzb1q1Erex9ASrztJ29SyhWFQjod1AeFBaPzEEC8YvtZMIYg==", + "dev": true, + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "yargs-parser": "^20.2.7" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==", + "dev": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "dev": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars-utils": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/handlebars-utils/-/handlebars-utils-1.0.6.tgz", + "integrity": "sha512-d5mmoQXdeEqSKMtQQZ9WkiUcO1E3tPbWxluCK9hVgIDPzQa9WsKo3Lbe/sGflTe7TomHEeZaOgwIkyIr1kfzkw==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0", + "typeof-article": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true + }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dev": true, + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isomorphic-git": { + "version": "1.25.10", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.25.10.tgz", + "integrity": "sha512-IxGiaKBwAdcgBXwIcxJU6rHLk+NrzYaaPKXXQffcA0GW3IUrQXdUPDXDo+hkGVcYruuz/7JlGBiuaeTCgIgivQ==", + "dev": true, + "dependencies": { + "async-lock": "^1.4.1", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^3.4.0", + "sha.js": "^2.4.9", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/multi-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/multi-progress/-/multi-progress-4.0.0.tgz", + "integrity": "sha512-9zcjyOou3FFCKPXsmkbC3ethv51SFPoA4dJD6TscIp2pUmy26kBDZW6h9XofPELrzseSkuD7r0V+emGEeo39Pg==", + "dev": true, + "peerDependencies": { + "progress": "^2.0.0" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pino": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.2.0.tgz", + "integrity": "sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==", + "dev": true, + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "dev": true, + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.2.2.tgz", + "integrity": "sha512-2FnyGir8nAJAqD3srROdrF1J5BIcMT4nwj7hHSc60El6Uxlym00UbCCd8pYIterstVBFlMyF1yFV8XdGIPbj4A==", + "dev": true, + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "dev": true + }, + "node_modules/portfinder": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", + "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==", + "dev": true, + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sha.js/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/should-proxy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/should-proxy/-/should-proxy-1.0.4.tgz", + "integrity": "sha512-RPQhIndEIVUCjkfkQ6rs6sOR6pkxJWCNdxtfG5pP0RVgUYbK5911kLTF0TNcCC0G3YCGd492rMollFT2aTd9iQ==", + "dev": true + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", + "integrity": "sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==", + "dev": true, + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dev": true, + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "dev": true, + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typeof-article": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/typeof-article/-/typeof-article-0.1.1.tgz", + "integrity": "sha512-Vn42zdX3FhmUrzEmitX3iYyLb+Umwpmv8fkZRIknYh84lmdrwqZA5xYaoKiIj2Rc5i/5wcDrpUmZcbk1U51vTw==", + "dev": true, + "dependencies": { + "kind-of": "^3.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/typeof-article/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/unxhr": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.2.0.tgz", + "integrity": "sha512-6cGpm8NFXPD9QbSNx0cD2giy7teZ6xOkCUH3U89WKVkL9N9rBrWjlCwhR94Re18ZlAop4MOc3WU1M3Hv/bgpIw==", + "dev": true, + "engines": { + "node": ">=8.11" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vinyl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", + "dev": true, + "dependencies": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha512-1Dly4xqlulvPD3fZUQJLY+FUIeqN3N2MM3uqe4rCJftAvOjFa3jFGfctOgluGx4ahPbUCsZkmJILiP0Vi4T6lQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.3.tgz", + "integrity": "sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + }, + "dependencies": { + "@antora/asciidoc-loader": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/asciidoc-loader/-/asciidoc-loader-3.1.14.tgz", + "integrity": "sha512-4xxisnoBFrlLNY6f3xZtyyfgm+tBLsqesTcEStfc8jtXUMYJ4b2DWIzo1vULmxvZ7yY5+Q7YqEvS5o6kIWAG0A==", + "dev": true, + "requires": { + "@antora/logger": "3.1.14", + "@antora/user-require-helper": "~3.0", + "@asciidoctor/core": "~2.2" + } + }, + "@antora/cli": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/cli/-/cli-3.1.14.tgz", + "integrity": "sha512-I6WcygMU2bFInjdURJjkYjo7K5M8B3lBB53v9OO0IcY0LhEY8Wa7IlZ7wVinf5qEjHvaYzRGTZVl6RsJtVt7Sw==", + "dev": true, + "requires": { + "@antora/logger": "3.1.14", + "@antora/playbook-builder": "3.1.14", + "@antora/user-require-helper": "~3.0", + "commander": "~11.1" + } + }, + "@antora/content-aggregator": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/content-aggregator/-/content-aggregator-3.1.14.tgz", + "integrity": "sha512-FVuBgnrGPiktYqK1WHbGF8O8l4m5KHlkxoJumrbacgFo8SKuiRFEo31zalxrCUsv8QM3UBEgX+LdHrve/9CGLg==", + "dev": true, + "requires": { + "@antora/expand-path-helper": "~3.0", + "@antora/logger": "3.1.14", + "@antora/user-require-helper": "~3.0", + "braces": "~3.0", + "cache-directory": "~2.0", + "fast-glob": "~3.3", + "hpagent": "~1.2", + "isomorphic-git": "~1.25", + "js-yaml": "~4.1", + "multi-progress": "~4.0", + "picomatch": "~4.0", + "progress": "~2.0", + "should-proxy": "~1.0", + "simple-get": "~4.0", + "vinyl": "~3.0" + } + }, + "@antora/content-classifier": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/content-classifier/-/content-classifier-3.1.14.tgz", + "integrity": "sha512-y8Fk+KU1lqD3aawOu3ZFK92YfOZ1k3YBJhLI9QIFM6Ck4STPnf7AwYbhfOtjODlwer5/OhFmfhjUB2hn7onGnA==", + "dev": true, + "requires": { + "@antora/asciidoc-loader": "3.1.14", + "@antora/logger": "3.1.14", + "mime-types": "~2.1", + "vinyl": "~3.0" + } + }, + "@antora/document-converter": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/document-converter/-/document-converter-3.1.14.tgz", + "integrity": "sha512-f6wFnL+489DI0ZDgoxYWzbxxWqPviRiJ56OHS1NixEfvJ7OpRBDPEbX1xnsIeiyFBgqX4+nY92MsCWKTa+Gf3w==", + "dev": true, + "requires": { + "@antora/asciidoc-loader": "3.1.14" + } + }, + "@antora/expand-path-helper": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@antora/expand-path-helper/-/expand-path-helper-3.0.0.tgz", + "integrity": "sha512-7PdEIhk97v85/CSm3HynCsX14TR6oIVz1s233nNLsiWubE8tTnpPt4sNRJR+hpmIZ6Bx9c6QDp3XIoiyu/WYYA==", + "dev": true + }, + "@antora/file-publisher": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/file-publisher/-/file-publisher-3.1.14.tgz", + "integrity": "sha512-fTaAnkyKSOlsxQM1TBFCAmiERA6Q67XleDCD2bMPVgfcENmo0Xfx59KwCHaA92IcRSmMftydlXHPaFxNh0UVsg==", + "dev": true, + "requires": { + "@antora/expand-path-helper": "~3.0", + "@antora/user-require-helper": "~3.0", + "vinyl": "~3.0", + "yazl": "~2.5" + } + }, + "@antora/logger": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/logger/-/logger-3.1.14.tgz", + "integrity": "sha512-kVEeGqZbXR903hPIm+BlN97fLdQ3LoUzE/BOPZ6vRp9m9Mmbnm67Kg7fSYkfTMLB0S2UWpAPFg22RdsU5ZoAzA==", + "dev": true, + "requires": { + "@antora/expand-path-helper": "~3.0", + "pino": "~9.2", + "pino-pretty": "~11.2", + "sonic-boom": "~4.0" + } + }, + "@antora/navigation-builder": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/navigation-builder/-/navigation-builder-3.1.14.tgz", + "integrity": "sha512-/637YLGD7oUHGSfEfszXkk4ASfIhDAg5Xs9035J1dV07XYRlGqmtUb15rtapbcECpcQFjCyM5jFQYSNNvLrGcQ==", + "dev": true, + "requires": { + "@antora/asciidoc-loader": "3.1.14" + } + }, + "@antora/page-composer": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/page-composer/-/page-composer-3.1.14.tgz", + "integrity": "sha512-RfA+67TxCqUPrQbZdrfjgLpHh8MR2z2du7cyF3HGX4N6DpqEBvz81NHHl3rA3fj6BQZPQbGm2OYAMU6wzJ6Pog==", + "dev": true, + "requires": { + "@antora/logger": "3.1.14", + "handlebars": "~4.7", + "require-from-string": "~2.0" + } + }, + "@antora/playbook-builder": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/playbook-builder/-/playbook-builder-3.1.14.tgz", + "integrity": "sha512-Ss2r7In00u/n9Da+JOxEqIE8NeRosf+f+agzH3Te09JV/mpgZKxEOE5V/VuP+TNNq4ww1eu5aOS8DiU2PYwj4Q==", + "dev": true, + "requires": { + "@iarna/toml": "~2.2", + "convict": "~6.2", + "js-yaml": "~4.1", + "json5": "~2.2" + } + }, + "@antora/redirect-producer": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/redirect-producer/-/redirect-producer-3.1.14.tgz", + "integrity": "sha512-5koAwRk1cZrvE/qfOWKXqb3jtxrZbWA5EYHYGFEoato5By3cbC42blH4Bre9/48pjyS6znFpbZhYUBpT7PRhZA==", + "dev": true, + "requires": { + "vinyl": "~3.0" + } + }, + "@antora/site-generator": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/site-generator/-/site-generator-3.1.14.tgz", + "integrity": "sha512-hQIUVtM9+xwleYWc4fIRZmiKl2p+ItOJuUm2+Hkdh07BZsySxkMOxxCyZsvTn9rc+4R94CYqDQCYElwFwdB2WA==", + "dev": true, + "requires": { + "@antora/asciidoc-loader": "3.1.14", + "@antora/content-aggregator": "3.1.14", + "@antora/content-classifier": "3.1.14", + "@antora/document-converter": "3.1.14", + "@antora/file-publisher": "3.1.14", + "@antora/logger": "3.1.14", + "@antora/navigation-builder": "3.1.14", + "@antora/page-composer": "3.1.14", + "@antora/playbook-builder": "3.1.14", + "@antora/redirect-producer": "3.1.14", + "@antora/site-mapper": "3.1.14", + "@antora/site-publisher": "3.1.14", + "@antora/ui-loader": "3.1.14", + "@antora/user-require-helper": "~3.0" + } + }, + "@antora/site-mapper": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/site-mapper/-/site-mapper-3.1.14.tgz", + "integrity": "sha512-3qbETtwadl+fWREjzrBUxPUorMcMiZ+hdkB1El9z7it9KzKh0Yp7Je0+2uTxGX+Lov9uik48dZJ9e/mr5PeaRQ==", + "dev": true, + "requires": { + "@antora/content-classifier": "3.1.14", + "vinyl": "~3.0" + } + }, + "@antora/site-publisher": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/site-publisher/-/site-publisher-3.1.14.tgz", + "integrity": "sha512-8apyEmgepUc7ms9CTEIPwN3tGtWwLqR6fbLMLs7hibqmOSR880Ut/4GRGb97sqcGQXSHdIyWK2oJKzRl1Akb6Q==", + "dev": true, + "requires": { + "@antora/file-publisher": "3.1.14" + } + }, + "@antora/ui-loader": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/@antora/ui-loader/-/ui-loader-3.1.14.tgz", + "integrity": "sha512-LVvTdKQOB44CmJ1JQDu8sJf6rrLZMxPAWWackdg2JtGyGHHpd80/MBcv4BSFk7//cJQ13Oqm/7JCbhD51KAFjg==", + "dev": true, + "requires": { + "@antora/expand-path-helper": "~3.0", + "braces": "~3.0", + "cache-directory": "~2.0", + "fast-glob": "~3.3", + "hpagent": "~1.2", + "js-yaml": "~4.1", + "picomatch": "~4.0", + "should-proxy": "~1.0", + "simple-get": "~4.0", + "vinyl": "~3.0", + "yauzl": "~3.1" + } + }, + "@antora/user-require-helper": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@antora/user-require-helper/-/user-require-helper-3.0.0.tgz", + "integrity": "sha512-KIXb8WYhnrnwH7Jj21l1w+et9k5GvcgcqvLOwxqWLEd0uVZOiMFdqFjqbVm3M+zcrs1JXWMeh2LLvxBbQs3q/Q==", + "dev": true, + "requires": { + "@antora/expand-path-helper": "~3.0" + } + }, + "@asciidoctor/core": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-2.2.9.tgz", + "integrity": "sha512-tIPRHo1T2SFmAm+j77cDsj0RuaszP7xJxsaVTTAF5CwKyTbazw9TnIVlpIWM5yWfIWAWcAZy92RcnPgMJwny1w==", + "dev": true, + "requires": { + "asciidoctor-opal-runtime": "0.3.4", + "unxhr": "~1.2" + } + }, + "@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "asciidoctor-opal-runtime": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.4.tgz", + "integrity": "sha512-zqd6zn1LV+PZ69AP/kEbB00zuPHMIAJY3IX8+aZV+X1qOwatYvKGjsMmdMc5ApfhtkjZ4mYkqiTPJWnEnBiMJg==", + "dev": true, + "requires": { + "fast-glob": "~3.3", + "unxhr": "~1.2" + } + }, + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true + }, + "atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true + }, + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "requires": { + "possible-typed-array-names": "^1.0.0" + } + }, + "b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "requires": {} + }, + "bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "requires": {} + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, + "cache-directory": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cache-directory/-/cache-directory-2.0.0.tgz", + "integrity": "sha512-7YKEapH+2Uikde8hySyfobXBqPKULDyHNl/lhKm7cKf/GJFdG/tU/WpLrOg2y9aUrQrWUilYqawFIiGJPS6gDA==", + "dev": true, + "requires": { + "xdg-basedir": "^3.0.0" + } + }, + "call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==", + "dev": true + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true + }, + "convict": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.5.tgz", + "integrity": "sha512-JtXpxqDqJ8P0UwEHwhxLzCIXQy97vlYBZR222Sbzb1q1Erex9ASrztJ29SyhWFQjod1AeFBaPzEEC8YvtZMIYg==", + "dev": true, + "requires": { + "lodash.clonedeep": "^4.5.0", + "yargs-parser": "^20.2.7" + } + }, + "corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true + }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true + }, + "dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true + }, + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==", + "dev": true + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0" + } + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "requires": { + "bare-events": "^2.7.0" + } + }, + "fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "dev": true + }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + } + }, + "fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true + }, + "for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "requires": { + "is-callable": "^1.2.7" + } + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true + }, + "handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, + "handlebars-utils": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/handlebars-utils/-/handlebars-utils-1.0.6.tgz", + "integrity": "sha512-d5mmoQXdeEqSKMtQQZ9WkiUcO1E3tPbWxluCK9hVgIDPzQa9WsKo3Lbe/sGflTe7TomHEeZaOgwIkyIr1kfzkw==", + "dev": true, + "requires": { + "kind-of": "^6.0.0", + "typeof-article": "^0.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.3" + } + }, + "hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true + }, + "hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dev": true, + "requires": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.16" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isomorphic-git": { + "version": "1.25.10", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.25.10.tgz", + "integrity": "sha512-IxGiaKBwAdcgBXwIcxJU6rHLk+NrzYaaPKXXQffcA0GW3IUrQXdUPDXDo+hkGVcYruuz/7JlGBiuaeTCgIgivQ==", + "dev": true, + "requires": { + "async-lock": "^1.4.1", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^3.4.0", + "sha.js": "^2.4.9", + "simple-get": "^4.0.1" + } + }, + "joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true + }, + "js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true + }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "dependencies": { + "picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "multi-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/multi-progress/-/multi-progress-4.0.0.tgz", + "integrity": "sha512-9zcjyOou3FFCKPXsmkbC3ethv51SFPoA4dJD6TscIp2pUmy26kBDZW6h9XofPELrzseSkuD7r0V+emGEeo39Pg==", + "dev": true, + "requires": {} + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true + }, + "on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pino": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.2.0.tgz", + "integrity": "sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==", + "dev": true, + "requires": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + } + }, + "pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "dev": true, + "requires": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + } + } + }, + "pino-pretty": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.2.2.tgz", + "integrity": "sha512-2FnyGir8nAJAqD3srROdrF1J5BIcMT4nwj7hHSc60El6Uxlym00UbCCd8pYIterstVBFlMyF1yFV8XdGIPbj4A==", + "dev": true, + "requires": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + } + } + }, + "pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "dev": true + }, + "portfinder": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", + "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==", + "dev": true, + "requires": { + "async": "^3.2.6", + "debug": "^4.3.6" + } + }, + "possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true + }, + "process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "requires": { + "side-channel": "^1.1.0" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true + }, + "secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "should-proxy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/should-proxy/-/should-proxy-1.0.4.tgz", + "integrity": "sha512-RPQhIndEIVUCjkfkQ6rs6sOR6pkxJWCNdxtfG5pP0RVgUYbK5911kLTF0TNcCC0G3YCGd492rMollFT2aTd9iQ==", + "dev": true + }, + "side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + } + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "sonic-boom": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", + "integrity": "sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==", + "dev": true, + "requires": { + "atomic-sleep": "^1.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true + }, + "streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dev": true, + "requires": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "requires": { + "streamx": "^2.12.5" + } + }, + "text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "requires": { + "b4a": "^1.6.4" + } + }, + "thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "dev": true, + "requires": { + "real-require": "^0.2.0" + } + }, + "to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "requires": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + } + }, + "typeof-article": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/typeof-article/-/typeof-article-0.1.1.tgz", + "integrity": "sha512-Vn42zdX3FhmUrzEmitX3iYyLb+Umwpmv8fkZRIknYh84lmdrwqZA5xYaoKiIj2Rc5i/5wcDrpUmZcbk1U51vTw==", + "dev": true, + "requires": { + "kind-of": "^3.1.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "optional": true + }, + "union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "requires": { + "qs": "^6.4.0" + } + }, + "unxhr": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.2.0.tgz", + "integrity": "sha512-6cGpm8NFXPD9QbSNx0cD2giy7teZ6xOkCUH3U89WKVkL9N9rBrWjlCwhR94Re18ZlAop4MOc3WU1M3Hv/bgpIw==", + "dev": true + }, + "url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "vinyl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + } + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + }, + "which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha512-1Dly4xqlulvPD3fZUQJLY+FUIeqN3N2MM3uqe4rCJftAvOjFa3jFGfctOgluGx4ahPbUCsZkmJILiP0Vi4T6lQ==", + "dev": true + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + }, + "yauzl": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.3.tgz", + "integrity": "sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + } + }, + "yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..557a6eb --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "article-cli-docs", + "version": "1.5.0", + "private": true, + "description": "Antora documentation site for article-cli", + "scripts": { + "docs:build": "antora antora-playbook.yml", + "docs:clean": "rm -rf build/site .cache/antora", + "docs:preview": "http-server build/site -c-1" + }, + "devDependencies": { + "@antora/cli": "3.1.14", + "@antora/site-generator": "3.1.14", + "handlebars-utils": "1.0.6", + "http-server": "14.1.1" + } +} diff --git a/pyproject.toml b/pyproject.toml index 8bd85ac..3bef4de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,40 +4,57 @@ build-backend = "hatchling.build" [project] name = "article-cli" -version = "1.4.0" +version = "1.5.0" authors = [ {name = "Christophe Prud'homme", email = "prudhomm@cemosis.fr"}, ] description = "CLI tool for managing LaTeX and Typst documents with git integration and Zotero bibliography" readme = "README.md" license = {file = "LICENSE"} -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "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 :: Scientific/Engineering", "Topic :: Text Processing :: Markup :: LaTeX", ] keywords = ["latex", "typst", "git", "zotero", "bibliography", "academic", "research", "presentations"] dependencies = [ "requests>=2.28.0", + "PyYAML>=6.0", + "Jinja2>=3.1", + "tomli>=2.0.0; python_version < '3.11'", ] [project.optional-dependencies] dev = [ "pytest>=7.0", "pytest-cov", - "black>=23.0", + "black==25.11.0", "flake8>=6.0", - "mypy>=1.0", + "mypy>=1.0,<2.0", + "build>=1.2.0", + "twine>=5.0.0", + "types-requests", +] + +[dependency-groups] +dev = [ + "pytest>=7.0", + "pytest-cov", + "black==25.11.0", + "flake8>=6.0", + "mypy>=1.0,<2.0", + "build>=1.2.0", + "twine>=5.0.0", "types-requests", ] @@ -63,7 +80,7 @@ include = [ [tool.black] line-length = 88 -target-version = ['py38'] +target-version = ['py39'] include = '\.pyi?$' extend-exclude = ''' /( @@ -101,6 +118,7 @@ ignore_missing_imports = true [tool.pytest.ini_options] testpaths = ["tests"] +pythonpath = ["src"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] @@ -108,4 +126,4 @@ addopts = [ "--strict-markers", "--strict-config", "--verbose", -] \ No newline at end of file +] diff --git a/src/article_cli/__init__.py b/src/article_cli/__init__.py index 443dd42..2af7b7f 100644 --- a/src/article_cli/__init__.py +++ b/src/article_cli/__init__.py @@ -9,7 +9,7 @@ - Git hooks setup """ -__version__ = "1.4.0" +__version__ = "1.5.0" __author__ = "Christophe Prud'homme" __email__ = "prudhomm@cemosis.fr" diff --git a/src/article_cli/cli.py b/src/article_cli/cli.py index 3d40f58..0689ea9 100644 --- a/src/article_cli/cli.py +++ b/src/article_cli/cli.py @@ -1,51 +1,69 @@ """ -Main CLI interface for article-cli +Main CLI entry point for article-cli. -Provides command-line interface for managing LaTeX articles with git and Zotero integration. +Command-specific parser and execution logic lives in article_cli.commands. """ import argparse import sys from pathlib import Path -from typing import Optional +from typing import Callable, List, Optional from . import __version__ +from .commands import COMMAND_MODULES +from .commands import bibtex as bibtex_command +from .commands import clean as clean_command +from .commands import compile as compile_command +from .commands import config as config_command +from .commands import doctor as doctor_command +from .commands import fonts as fonts_command +from .commands import init as init_command +from .commands import release as release_command +from .commands import setup as setup_command +from .commands import themes as themes_command from .config import Config -from .zotero import ZoteroBibTexUpdater, print_error, print_info -from .git_manager import GitManager -from .repository_setup import RepositorySetup +from .reporting import print_error + +CommandHandler = Callable[[argparse.Namespace, Config], int] def create_parser() -> argparse.ArgumentParser: - """Create and configure the argument parser""" + """Create and configure the argument parser.""" parser = argparse.ArgumentParser( prog="article-cli", description="CLI tool for managing LaTeX articles with git and Zotero integration", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: + # Paper lifecycle %(prog)s init --title "My Article" --authors "John Doe,Jane Smith" - %(prog)s setup # Setup git hooks - %(prog)s clean # Clean build files - %(prog)s compile main.tex # Compile with latexmk - %(prog)s compile --engine pdflatex # Compile with pdflatex - %(prog)s compile --shell-escape # Enable shell escape + %(prog)s setup --dry-run # Preview setup actions + %(prog)s setup # Install managed gitinfo2 hooks + %(prog)s doctor # Diagnose repository readiness + %(prog)s bib update --dry-run # Preview Zotero bibliography update + %(prog)s bib update --check # Check checked-in bibliography freshness + %(prog)s bib update # Update references from Zotero + %(prog)s compile # Compile configured main document + %(prog)s version # Refresh and report git metadata + %(prog)s version --compile --check-pdf # Refresh, compile, and inspect PDF text + %(prog)s release v1 --dry-run # Preview release tag creation + %(prog)s release v1 # Create checked local release tag + + # Common utilities + %(prog)s compile --engine pdflatex # Override configured engine %(prog)s compile --watch # Watch and auto-recompile %(prog)s compile presentation.typ # Compile Typst document - %(prog)s compile --engine typst --watch # Watch Typst file - %(prog)s compile --font-path fonts/ # Typst with custom fonts - %(prog)s create v1.0.0 # Create release v1.0.0 - %(prog)s list --count 10 # List 10 recent releases - %(prog)s delete v1.0.0 # Delete release - %(prog)s update-bibtex # Update from Zotero + %(prog)s clean # Clean build files + %(prog)s list --count 10 # List 10 recent release tags + %(prog)s delete v1 # Delete release tag + %(prog)s update-bibtex # Deprecated alias for bib update + %(prog)s create v1 # Deprecated alias for release %(prog)s config create # Create sample config file - %(prog)s install-fonts # Install fonts for XeLaTeX %(prog)s install-fonts --list # List installed fonts - %(prog)s install-theme numpex # Install numpex Beamer/Typst theme %(prog)s install-theme --list # List available themes Environment variables: - ZOTERO_API_KEY : Your Zotero API key (required for update-bibtex) + ZOTERO_API_KEY : Your Zotero API key (required for bib update) ZOTERO_USER_ID : Your Zotero user ID ZOTERO_GROUP_ID : Your Zotero group ID """, @@ -54,620 +72,78 @@ def create_parser() -> argparse.ArgumentParser: parser.add_argument( "--version", action="version", version=f"%(prog)s {__version__}" ) - parser.add_argument("--config", type=Path, help="Path to configuration file") subparsers = parser.add_subparsers(dest="command", help="Command to execute") - - # Init command - init_parser = subparsers.add_parser( - "init", help="Initialize repository with workflows and configuration" - ) - init_parser.add_argument("--title", required=True, help="Article title") - init_parser.add_argument( - "--authors", - required=True, - help='Comma-separated list of authors (e.g., "John Doe,Jane Smith")', - ) - init_parser.add_argument( - "--group-id", - default="4678293", - help="Zotero group ID (default: 4678293 for article.template)", - ) - init_parser.add_argument( - "--tex-file", - help="Main .tex file (auto-detected if not specified)", - ) - init_parser.add_argument( - "--force", - action="store_true", - help="Overwrite existing files", - ) - init_parser.add_argument( - "--type", - choices=[ - "article", - "presentation", - "poster", - "typst-presentation", - "typst-poster", - ], - default="article", - help="Project type (default: article). Use 'presentation' for Beamer, 'typst-presentation' for Typst slides.", - ) - init_parser.add_argument( - "--theme", - default="", - help="Theme for presentations (e.g., 'numpex'). Works with both Beamer and Typst.", - ) - init_parser.add_argument( - "--aspect-ratio", - choices=["169", "43", "1610"], - default="169", - help="Aspect ratio for presentations (default: 169 for 16:9).", - ) - - # Setup command - subparsers.add_parser("setup", help="Setup git hooks for gitinfo2") - - # Clean command - subparsers.add_parser("clean", help="Clean LaTeX build files") - - # Compile command - compile_parser = subparsers.add_parser( - "compile", help="Compile LaTeX document using latexmk" - ) - compile_parser.add_argument( - "tex_file", - nargs="?", - help="Document file to compile (.tex or .typ, auto-detected if not specified)", - ) - compile_parser.add_argument( - "--engine", - choices=["latexmk", "pdflatex", "xelatex", "lualatex", "typst"], - default="latexmk", - help="Compilation engine (default: latexmk). Use typst for .typ files, xelatex/lualatex for custom fonts.", - ) - compile_parser.add_argument( - "--font-path", - action="append", - dest="font_paths", - help="Additional font path for Typst (can be specified multiple times)", - ) - compile_parser.add_argument( - "--shell-escape", - action="store_true", - help="Enable shell escape (for code highlighting, etc.)", - ) - compile_parser.add_argument( - "--clean-first", - action="store_true", - help="Clean build files before compilation", - ) - compile_parser.add_argument( - "--clean-after", action="store_true", help="Clean build files after compilation" - ) - compile_parser.add_argument( - "--watch", - action="store_true", - help="Watch for changes and recompile automatically", - ) - compile_parser.add_argument( - "--output-dir", help="Output directory for compiled files" - ) - - # Create command - create_parser = subparsers.add_parser("create", help="Create a new release") - create_parser.add_argument("version", help="Version tag (e.g., v1.0.0)") - create_parser.add_argument( - "--push", action="store_true", help="Automatically push the release" - ) - - # List command - list_parser = subparsers.add_parser("list", help="List releases") - list_parser.add_argument( - "--count", type=int, default=5, help="Number of releases to show" - ) - - # Delete command - delete_parser = subparsers.add_parser("delete", help="Delete a release") - delete_parser.add_argument("version", help="Version tag to delete") - delete_parser.add_argument( - "--remote", action="store_true", help="Also delete from remote" - ) - - # Update-bibtex command - bibtex_parser = subparsers.add_parser( - "update-bibtex", help="Update BibTeX from Zotero" - ) - bibtex_parser.add_argument("--api-key", help="Zotero API key") - bibtex_parser.add_argument("--user-id", help="Zotero user ID") - bibtex_parser.add_argument("--group-id", help="Zotero group ID") - bibtex_parser.add_argument( - "--output", default="references.bib", help="Output BibTeX file" - ) - bibtex_parser.add_argument( - "--no-backup", action="store_true", help="Skip backup creation" - ) - - # Config command - config_parser = subparsers.add_parser("config", help="Configuration management") - config_subparsers = config_parser.add_subparsers( - dest="config_command", help="Config subcommands" - ) - - config_create_parser = config_subparsers.add_parser( - "create", help="Create sample configuration file" - ) - config_create_parser.add_argument("--path", type=Path, help="Path for config file") - - config_subparsers.add_parser("show", help="Show current configuration") - - # Install-fonts command - fonts_parser = subparsers.add_parser( - "install-fonts", help="Download and install fonts for XeLaTeX projects" - ) - fonts_parser.add_argument( - "--dir", - type=Path, - help="Directory to install fonts (default: fonts/)", - ) - fonts_parser.add_argument( - "--force", - action="store_true", - help="Re-download fonts even if already installed", - ) - fonts_parser.add_argument( - "--list", - action="store_true", - dest="list_fonts", - help="List installed fonts instead of installing", - ) - - # Install-theme command - theme_parser = subparsers.add_parser( - "install-theme", help="Download and install Beamer themes for presentations" - ) - theme_parser.add_argument( - "theme_name", - nargs="?", - help="Name of theme to install (e.g., 'numpex')", - ) - theme_parser.add_argument( - "--dir", - type=Path, - help="Directory to install theme (default: current directory)", - ) - theme_parser.add_argument( - "--force", - action="store_true", - help="Re-download theme even if already installed", - ) - theme_parser.add_argument( - "--list", - action="store_true", - dest="list_themes", - help="List available themes instead of installing", - ) - theme_parser.add_argument( - "--url", - help="Custom URL to download theme from (use with theme_name)", - ) + for command_module in COMMAND_MODULES: + command_module.add_parser(subparsers) return parser def handle_init_command(args: argparse.Namespace, config: Config) -> int: - """Handle the init command""" - try: - # Parse comma-separated authors - authors = [a.strip() for a in args.authors.split(",")] - - repo_setup = RepositorySetup() - return ( - 0 - if repo_setup.init_repository( - title=args.title, - authors=authors, - group_id=args.group_id, - force=args.force, - main_tex_file=args.tex_file, - project_type=args.type, - theme=args.theme, - aspect_ratio=args.aspect_ratio, - ) - else 1 - ) - except Exception as e: - print_error(f"Failed to initialize repository: {e}") - return 1 + """Compatibility wrapper for the init command.""" + return init_command.run(args, config) def handle_setup_command(config: Config) -> int: - """Handle the setup command""" - try: - git_manager = GitManager() - return 0 if git_manager.setup_hooks() else 1 - except ValueError as e: - print_error(str(e)) - return 1 + """Compatibility wrapper for the setup command.""" + return setup_command.run(argparse.Namespace(), config) def handle_clean_command(config: Config) -> int: - """Handle the clean command""" - try: - git_manager = GitManager() - latex_config = config.get_latex_config() - return ( - 0 if git_manager.clean_latex_files(latex_config["clean_extensions"]) else 1 - ) - except ValueError as e: - print_error(str(e)) - return 1 + """Compatibility wrapper for the clean command.""" + return clean_command.run(argparse.Namespace(), config) def handle_compile_command(args: argparse.Namespace, config: Config) -> int: - """Handle the compile command""" - try: - # Determine engine from args or auto-detect from file extension - engine = args.engine - doc_file = args.tex_file - - # Auto-detect file if not provided - if not doc_file: - # If engine is typst, look for .typ files first - if engine == "typst": - doc_file = _auto_detect_typ_file() - if not doc_file: - doc_file = _auto_detect_tex_file() - else: - doc_file = _auto_detect_tex_file() - if not doc_file: - doc_file = _auto_detect_typ_file() - - if not doc_file: - print_error( - "No .tex or .typ file specified and none found in current directory" - ) - return 1 - - # Validate file exists - doc_path = Path(doc_file) - if not doc_path.exists(): - print_error(f"Document file not found: {doc_file}") - return 1 - - # Auto-detect engine from file extension if not explicitly set to typst - if doc_path.suffix == ".typ" and engine != "typst": - print_info(f"Detected Typst file, switching engine to typst") - engine = "typst" - elif doc_path.suffix == ".tex" and engine == "typst": - print_error(f"Cannot use Typst engine with .tex file: {doc_file}") - return 1 - - # Use appropriate compiler - if engine == "typst": - from .typst_compiler import TypstCompiler - - typst_compiler = TypstCompiler(config) - - # Compile the document - success = typst_compiler.compile( - typ_file=doc_file, - output_dir=args.output_dir, - font_paths=args.font_paths, - watch=args.watch, - ) - else: - from .latex_compiler import LaTeXCompiler - - latex_compiler = LaTeXCompiler(config) - - # Clean before compilation if requested - if args.clean_first: - print_info("Cleaning build files before compilation...") - git_manager = GitManager() - latex_config = config.get_latex_config() - git_manager.clean_latex_files(latex_config["clean_extensions"]) - - # Compile the document - success = latex_compiler.compile( - tex_file=doc_file, - engine=engine, - shell_escape=args.shell_escape, - output_dir=args.output_dir, - watch=args.watch, - ) - - # Clean after compilation if requested - if args.clean_after and success: - print_info("Cleaning build files after compilation...") - git_manager = GitManager() - latex_config = config.get_latex_config() - git_manager.clean_latex_files(latex_config["clean_extensions"]) - - return 0 if success else 1 - - except Exception as e: - print_error(f"Compilation failed: {e}") - return 1 - - -def _auto_detect_tex_file() -> Optional[str]: - """Auto-detect main .tex file in current directory""" - current_dir = Path.cwd() - tex_files = list(current_dir.glob("*.tex")) - - if not tex_files: - return None - - if len(tex_files) == 1: - return tex_files[0].name - - # Multiple .tex files - prefer common patterns - for pattern in ["main.tex", "article.tex", f"{current_dir.name}.tex"]: - if (current_dir / pattern).exists(): - return pattern - - # Return first .tex file found - print_info(f"Multiple .tex files found, using: {tex_files[0].name}") - return tex_files[0].name - - -def _auto_detect_typ_file() -> Optional[str]: - """Auto-detect main .typ file in current directory""" - current_dir = Path.cwd() - typ_files = list(current_dir.glob("*.typ")) - - if not typ_files: - return None - - if len(typ_files) == 1: - return typ_files[0].name - - # Multiple .typ files - prefer common patterns - for pattern in [ - "main.typ", - "presentation.typ", - "presentation.template.typ", - f"{current_dir.name}.typ", - ]: - if (current_dir / pattern).exists(): - return pattern - - # Return first .typ file found - print_info(f"Multiple .typ files found, using: {typ_files[0].name}") - return typ_files[0].name + """Compatibility wrapper for the compile command.""" + return compile_command.run(args, config) def handle_create_command(args: argparse.Namespace, config: Config) -> int: - """Handle the create command""" - try: - git_manager = GitManager() - git_config = config.get_git_config() - auto_push = args.push or git_config.get("auto_push", False) - return 0 if git_manager.create_release(args.version, auto_push) else 1 - except ValueError as e: - print_error(str(e)) - return 1 + """Compatibility wrapper for the create command.""" + return release_command.run_create(args, config) def handle_list_command(args: argparse.Namespace, config: Config) -> int: - """Handle the list command""" - try: - git_manager = GitManager() - return 0 if git_manager.list_releases(args.count) else 1 - except ValueError as e: - print_error(str(e)) - return 1 + """Compatibility wrapper for the list command.""" + return release_command.run_list(args, config) def handle_delete_command(args: argparse.Namespace, config: Config) -> int: - """Handle the delete command""" - try: - git_manager = GitManager() - return 0 if git_manager.delete_release(args.version, args.remote) else 1 - except ValueError as e: - print_error(str(e)) - return 1 + """Compatibility wrapper for the delete command.""" + return release_command.run_delete(args, config) def handle_update_bibtex_command(args: argparse.Namespace, config: Config) -> int: - """Handle the update-bibtex command""" - try: - # Get and validate Zotero configuration - zotero_config = config.validate_zotero_config(args) - - updater = ZoteroBibTexUpdater( - api_key=zotero_config["api_key"], - user_id=zotero_config["user_id"], - group_id=zotero_config["group_id"], - output_file=zotero_config["output_file"], - ) - - return 0 if updater.update(backup=not args.no_backup) else 1 - - except ValueError as e: - print_error(str(e)) - return 1 + """Compatibility wrapper for the update-bibtex command.""" + return bibtex_command.run(args, config) def handle_config_command(args: argparse.Namespace, config: Config) -> int: - """Handle config subcommands""" - if args.config_command == "create": - try: - path = args.path or Path.cwd() / ".article-cli.toml" - config.create_sample_config(path) - return 0 - except Exception as e: - print_error(f"Failed to create config file: {e}") - return 1 + """Compatibility wrapper for the config command.""" + return config_command.run(args, config) - elif args.config_command == "show": - try: - print_info("Current configuration:") - zotero_config = config.get_zotero_config() - git_config = config.get_git_config() - latex_config = config.get_latex_config() - - print("\n[Zotero]") - print(f" API Key: {'***' if zotero_config['api_key'] else 'Not set'}") - print(f" User ID: {zotero_config['user_id'] or 'Not set'}") - - # Show group ID with name if available - if zotero_config["group_id"]: - group_display = zotero_config["group_id"] - - # Try to get group name if API key is available - if zotero_config["api_key"]: - try: - from .zotero import ZoteroBibTexUpdater - - updater = ZoteroBibTexUpdater( - api_key=zotero_config["api_key"], - group_id=zotero_config["group_id"], - ) - group_name = updater.get_group_name() - if group_name: - group_display = ( - f"{zotero_config['group_id']} ({group_name})" - ) - except Exception: - pass # Silently fall back to just showing ID - - print(f" Group ID: {group_display}") - else: - print(f" Group ID: Not set") - - print(f" Output File: {zotero_config['output_file']}") - - print("\n[Git]") - print(f" Auto Push: {git_config['auto_push']}") - print(f" Default Branch: {git_config['default_branch']}") - - print("\n[LaTeX]") - print( - f" Clean Extensions: {len(latex_config['clean_extensions'])} extensions" - ) - print(f" Build Directory: {latex_config['build_dir']}") - print(f" Engine: {latex_config['engine']}") - print(f" Shell Escape: {latex_config['shell_escape']}") - print(f" Timeout: {latex_config['timeout']}s") - - return 0 - except Exception as e: - print_error(f"Failed to show configuration: {e}") - return 1 - else: - print_error("Unknown config command") - return 1 +def handle_doctor_command(args: argparse.Namespace, config: Config) -> int: + """Compatibility wrapper for the doctor command.""" + return doctor_command.run(args, config) -def handle_install_fonts_command(args: argparse.Namespace, config: Config) -> int: - """Handle the install-fonts command""" - try: - from .fonts import FontInstaller - fonts_config = config.get_fonts_config() - - # Override directory if specified on command line - fonts_dir = ( - args.dir if args.dir else Path(fonts_config.get("directory", "fonts")) - ) - - # List installed fonts if requested - if args.list_fonts: - installer = FontInstaller(fonts_dir=fonts_dir) - installed = installer.list_installed() - - if not installed: - print_info(f"No fonts installed in {fonts_dir}") - return 0 - - print_info(f"Installed fonts in {fonts_dir}:") - for font_name in installed: - font_files = installer.get_font_files(font_name) - print(f" - {font_name} ({len(font_files)} font files)") - - return 0 - - # Install fonts - sources = fonts_config.get("sources", []) - installer = FontInstaller(fonts_dir=fonts_dir, sources=sources) - - success = installer.install_all(force=args.force) - return 0 if success else 1 - - except Exception as e: - print_error(f"Font installation failed: {e}") - return 1 +def handle_install_fonts_command(args: argparse.Namespace, config: Config) -> int: + """Compatibility wrapper for the install-fonts command.""" + return fonts_command.run(args, config) def handle_install_theme_command(args: argparse.Namespace, config: Config) -> int: - """Handle the install-theme command""" - try: - from .themes import ThemeInstaller - - themes_config = config.get_themes_config() - - # Override directory if specified on command line - themes_dir = args.dir if args.dir else Path(themes_config.get("directory", ".")) - - # Get configured sources - sources = themes_config.get("sources", {}) - - installer = ThemeInstaller(themes_dir=themes_dir, sources=sources) - - # List available themes if requested - if args.list_themes: - available = installer.list_available() - installed = installer.list_installed() - - print_info("Available themes:") - for theme in available: - name = theme["name"] - desc = theme.get("description", "") - engine = theme.get("engine", "pdflatex") - fonts = ( - " (requires custom fonts)" if theme.get("requires_fonts") else "" - ) - installed_marker = ( - " [installed]" if any(i["name"] == name for i in installed) else "" - ) - print(f" - {name}: {desc}") - print(f" Engine: {engine}{fonts}{installed_marker}") - - return 0 - - # Theme name is required for installation - if not args.theme_name: - print_error("Theme name is required. Use --list to see available themes.") - return 1 - - # Install from custom URL if provided - if args.url: - success = installer.install_from_url( - name=args.theme_name, - url=args.url, - force=args.force, - ) - else: - # Install from known sources - success = installer.install_theme( - name=args.theme_name, - force=args.force, - ) - - return 0 if success else 1 + """Compatibility wrapper for the install-theme command.""" + return themes_command.run(args, config) - except Exception as e: - print_error(f"Theme installation failed: {e}") - return 1 - -def main(argv: Optional[list] = None) -> int: +def main(argv: Optional[List[str]] = None) -> int: """ - Main entry point for article-cli + Main entry point for article-cli. Args: argv: Command line arguments (defaults to sys.argv) @@ -682,52 +158,19 @@ def main(argv: Optional[list] = None) -> int: parser.print_help() return 1 - # Initialize configuration try: - config = Config(args.config) + quiet_config = args.command == "doctor" and bool(getattr(args, "json", False)) + config = Config(args.config, quiet=quiet_config) except Exception as e: print_error(f"Configuration error: {e}") return 1 - # Route to appropriate command handler try: - if args.command == "init": - return handle_init_command(args, config) - - elif args.command == "setup": - return handle_setup_command(config) - - elif args.command == "clean": - return handle_clean_command(config) - - elif args.command == "compile": - return handle_compile_command(args, config) - - elif args.command == "create": - return handle_create_command(args, config) - - elif args.command == "list": - return handle_list_command(args, config) - - elif args.command == "delete": - return handle_delete_command(args, config) - - elif args.command == "update-bibtex": - return handle_update_bibtex_command(args, config) - - elif args.command == "config": - return handle_config_command(args, config) - - elif args.command == "install-fonts": - return handle_install_fonts_command(args, config) - - elif args.command == "install-theme": - return handle_install_theme_command(args, config) - - else: + handler = getattr(args, "handler", None) + if handler is None: print_error(f"Unknown command: {args.command}") return 1 - + return _run_handler(handler, args, config) except KeyboardInterrupt: print("\n\nInterrupted by user") return 130 @@ -736,5 +179,12 @@ def main(argv: Optional[list] = None) -> int: return 1 +def _run_handler( + handler: CommandHandler, args: argparse.Namespace, config: Config +) -> int: + """Run a command handler with a narrow type boundary for argparse defaults.""" + return handler(args, config) + + if __name__ == "__main__": sys.exit(main()) diff --git a/src/article_cli/command_runner.py b/src/article_cli/command_runner.py new file mode 100644 index 0000000..b5e4e73 --- /dev/null +++ b/src/article_cli/command_runner.py @@ -0,0 +1,67 @@ +""" +Subprocess execution boundary for article-cli services. +""" + +import subprocess +from pathlib import Path +from typing import IO, Optional, Sequence, Union + +Command = Sequence[Union[str, Path]] + + +class CommandRunner: + """Thin wrapper around subprocess for service-level dependency injection.""" + + def run( + self, + command: Command, + cwd: Optional[Path] = None, + capture_output: bool = True, + text: bool = True, + timeout: Optional[int] = None, + check: bool = False, + ) -> subprocess.CompletedProcess: + """Run a command and return the completed process.""" + return subprocess.run( + [str(part) for part in command], + cwd=cwd, + capture_output=capture_output, + text=text, + timeout=timeout, + check=check, + ) + + def popen( + self, + command: Command, + cwd: Optional[Path] = None, + stdout: Optional[int] = subprocess.PIPE, + stderr: Optional[int] = subprocess.STDOUT, + universal_newlines: bool = True, + bufsize: int = 1, + ) -> subprocess.Popen: + """Start a long-running command.""" + return subprocess.Popen( + [str(part) for part in command], + cwd=cwd, + stdout=stdout, + stderr=stderr, + universal_newlines=universal_newlines, + bufsize=bufsize, + ) + + def stream_lines(self, process: subprocess.Popen) -> None: + """Stream process stdout line by line until it exits.""" + stream: Optional[IO[str]] = process.stdout + if stream is None: + return + + while True: + output = stream.readline() + if output == "" and process.poll() is not None: + break + if output: + print(output.strip()) + + +DEFAULT_RUNNER = CommandRunner() diff --git a/src/article_cli/commands/__init__.py b/src/article_cli/commands/__init__.py new file mode 100644 index 0000000..92ca893 --- /dev/null +++ b/src/article_cli/commands/__init__.py @@ -0,0 +1,31 @@ +""" +Command modules for article-cli. +""" + +from . import ( + bibtex, + clean, + compile, + config, + doctor, + fonts, + init, + release, + setup, + themes, + version, +) + +COMMAND_MODULES = [ + init, + setup, + clean, + compile, + doctor, + version, + release, + bibtex, + config, + fonts, + themes, +] diff --git a/src/article_cli/commands/bibtex.py b/src/article_cli/commands/bibtex.py new file mode 100644 index 0000000..83bac17 --- /dev/null +++ b/src/article_cli/commands/bibtex.py @@ -0,0 +1,108 @@ +""" +Zotero BibTeX update command. +""" + +import argparse +from typing import Any + +from ..config import Config +from ..reporting import print_error +from ..services.bibliography import BibliographyService, BibliographyUpdateOptions +from ..zotero import ZoteroBibTexUpdater + + +def add_parser(subparsers: Any) -> None: + """Register bibliography command parsers.""" + bib_parser = subparsers.add_parser( + "bib", + help="Manage bibliography data", + description="Manage bibliography data for the paper lifecycle.", + ) + bib_subparsers = bib_parser.add_subparsers( + dest="bib_command", help="Bibliography command" + ) + bib_subparsers.required = True + + update_parser = bib_subparsers.add_parser( + "update", + help="Update BibTeX from Zotero", + description="Update BibTeX references from Zotero.", + ) + _add_update_arguments(update_parser) + update_parser.set_defaults(handler=run) + + alias_parser = subparsers.add_parser( + "update-bibtex", + help="Deprecated alias for 'bib update'", + description="Deprecated alias for 'article-cli bib update'.", + ) + _add_update_arguments(alias_parser) + alias_parser.set_defaults(handler=run) + + +def _add_update_arguments(parser: argparse.ArgumentParser) -> None: + """Add shared BibTeX update arguments.""" + parser.add_argument("--api-key", help="Zotero API key") + parser.add_argument("--user-id", help="Zotero user ID") + parser.add_argument("--group-id", help="Zotero group ID") + parser.add_argument( + "--collection", + help="Zotero collection or subcollection key to export", + ) + parser.add_argument("--output", default=None, help="Output BibTeX file") + parser.add_argument( + "--local-file", + help="Local BibTeX file with manual entries (default: local_references.bib)", + ) + parser.add_argument( + "--include-local", + action="store_true", + help="Include local BibTeX entries in the output or merged output", + ) + parser.add_argument( + "--merged-output", + help="Write a combined Zotero + local BibTeX file to this path", + ) + parser.add_argument("--no-backup", action="store_true", help="Skip backup creation") + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate configuration and report the planned update without writing files", + ) + parser.add_argument( + "--check", + action="store_true", + help="Check whether bibliography files are up to date without writing files", + ) + parser.add_argument( + "--check-citations", + action="store_true", + help="Check that citation keys in project sources exist in the bibliography", + ) + parser.add_argument( + "--timestamp", + action="store_true", + help="Include a generated timestamp in the BibTeX header", + ) + + +def run(args: argparse.Namespace, config: Config) -> int: + """Handle the update-bibtex command.""" + try: + service = BibliographyService(config, updater_cls=ZoteroBibTexUpdater) + success = service.update( + args, + BibliographyUpdateOptions( + no_backup=bool(getattr(args, "no_backup", False)), + dry_run=bool(getattr(args, "dry_run", False)), + check=bool(getattr(args, "check", False)), + include_local=bool(getattr(args, "include_local", False)), + check_citations=bool(getattr(args, "check_citations", False)), + timestamp=bool(getattr(args, "timestamp", False)), + ), + ) + return 0 if success else 1 + + except (RuntimeError, ValueError) as e: + print_error(str(e)) + return 1 diff --git a/src/article_cli/commands/clean.py b/src/article_cli/commands/clean.py new file mode 100644 index 0000000..8cfd9aa --- /dev/null +++ b/src/article_cli/commands/clean.py @@ -0,0 +1,28 @@ +""" +LaTeX build cleanup command. +""" + +import argparse +from typing import Any + +from ..config import Config +from ..git_manager import GitManager +from ..reporting import print_error +from ..services.git import GitService + + +def add_parser(subparsers: Any) -> None: + """Register the clean command parser.""" + parser = subparsers.add_parser("clean", help="Clean LaTeX build files") + parser.set_defaults(handler=run) + + +def run(args: argparse.Namespace, config: Config) -> int: + """Handle the clean command.""" + try: + service = GitService(manager_cls=GitManager) + latex_config = config.get_latex_config() + return 0 if service.clean_latex_files(latex_config["clean_extensions"]) else 1 + except (RuntimeError, ValueError) as e: + print_error(str(e)) + return 1 diff --git a/src/article_cli/commands/compile.py b/src/article_cli/commands/compile.py new file mode 100644 index 0000000..5482576 --- /dev/null +++ b/src/article_cli/commands/compile.py @@ -0,0 +1,89 @@ +""" +Document compilation command. +""" + +import argparse +from typing import Any + +from ..config import Config +from ..reporting import print_error +from ..services.compiler import CompileOptions, CompilerService + + +def add_parser(subparsers: Any) -> None: + """Register the compile command parser.""" + parser = subparsers.add_parser( + "compile", help="Compile LaTeX document using latexmk" + ) + parser.add_argument( + "tex_file", + nargs="?", + help="Document file to compile (.tex or .typ, auto-detected if not specified)", + ) + parser.add_argument( + "--engine", + choices=["latexmk", "pdflatex", "xelatex", "lualatex", "typst"], + default=None, + help=( + "Compilation engine. Defaults to project config or latexmk. " + "Use typst for .typ files, xelatex/lualatex for custom fonts." + ), + ) + parser.add_argument( + "--font-path", + action="append", + dest="font_paths", + help="Additional font path for Typst (can be specified multiple times)", + ) + shell_escape_group = parser.add_mutually_exclusive_group() + shell_escape_group.add_argument( + "--shell-escape", + dest="shell_escape", + action="store_true", + default=None, + help="Enable shell escape (for code highlighting, etc.)", + ) + shell_escape_group.add_argument( + "--no-shell-escape", + dest="shell_escape", + action="store_false", + help="Disable shell escape even if enabled in project config", + ) + parser.add_argument( + "--clean-first", + action="store_true", + help="Clean build files before compilation", + ) + parser.add_argument( + "--clean-after", action="store_true", help="Clean build files after compilation" + ) + parser.add_argument( + "--watch", + action="store_true", + help="Watch for changes and recompile automatically", + ) + parser.add_argument("--output-dir", help="Output directory for compiled files") + parser.set_defaults(handler=run) + + +def run(args: argparse.Namespace, config: Config) -> int: + """Handle the compile command.""" + try: + service = CompilerService(config) + success = service.compile( + CompileOptions( + document=args.tex_file, + engine=args.engine, + shell_escape=args.shell_escape, + output_dir=args.output_dir, + font_paths=args.font_paths, + clean_first=args.clean_first, + clean_after=args.clean_after, + watch=args.watch, + ) + ) + return 0 if success else 1 + + except Exception as e: + print_error(f"Compilation failed: {e}") + return 1 diff --git a/src/article_cli/commands/config.py b/src/article_cli/commands/config.py new file mode 100644 index 0000000..22f369a --- /dev/null +++ b/src/article_cli/commands/config.py @@ -0,0 +1,102 @@ +""" +Configuration management command. +""" + +import argparse +from pathlib import Path +from typing import Any + +from ..config import Config +from ..reporting import print_error, print_info + + +def add_parser(subparsers: Any) -> None: + """Register the config command parser.""" + parser = subparsers.add_parser("config", help="Configuration management") + config_subparsers = parser.add_subparsers( + dest="config_command", help="Config subcommands" + ) + + create_parser = config_subparsers.add_parser( + "create", help="Create sample configuration file" + ) + create_parser.add_argument("--path", type=Path, help="Path for config file") + + config_subparsers.add_parser("show", help="Show current configuration") + parser.set_defaults(handler=run) + + +def run(args: argparse.Namespace, config: Config) -> int: + """Handle config subcommands.""" + if args.config_command == "create": + try: + path = args.path or Path.cwd() / ".article-cli.toml" + config.create_sample_config(path) + return 0 + except Exception as e: + print_error(f"Failed to create config file: {e}") + return 1 + + if args.config_command == "show": + return _show_config(config) + + print_error("Unknown config command") + return 1 + + +def _show_config(config: Config) -> int: + """Print the effective configuration.""" + try: + print_info("Current configuration:") + zotero_config = config.get_zotero_config() + git_config = config.get_git_config() + latex_config = config.get_latex_config() + + print("\n[Zotero]") + print(f" API Key: {'***' if zotero_config['api_key'] else 'Not set'}") + print(f" User ID: {zotero_config['user_id'] or 'Not set'}") + + if zotero_config["group_id"]: + group_display = zotero_config["group_id"] + + if zotero_config["api_key"]: + try: + from ..zotero import ZoteroBibTexUpdater + + updater = ZoteroBibTexUpdater( + api_key=zotero_config["api_key"], + group_id=zotero_config["group_id"], + ) + group_name = updater.get_group_name() + if group_name: + group_display = f"{zotero_config['group_id']} ({group_name})" + except Exception: + pass + + print(f" Group ID: {group_display}") + else: + print(" Group ID: Not set") + + print(f" Collection ID: {zotero_config['collection_id'] or 'Not set'}") + print(f" Output File: {zotero_config['output_file']}") + print(f" Local File: {zotero_config['local_file']}") + print( + f" Merged Output File: {zotero_config['merged_output_file'] or 'Not set'}" + ) + print(f" Deterministic Output: {zotero_config['deterministic']}") + + print("\n[Git]") + print(f" Auto Push: {git_config['auto_push']}") + print(f" Default Branch: {git_config['default_branch']}") + + print("\n[LaTeX]") + print(f" Clean Extensions: {len(latex_config['clean_extensions'])} extensions") + print(f" Build Directory: {latex_config['build_dir']}") + print(f" Engine: {latex_config['engine']}") + print(f" Shell Escape: {latex_config['shell_escape']}") + print(f" Timeout: {latex_config['timeout']}s") + + return 0 + except Exception as e: + print_error(f"Failed to show configuration: {e}") + return 1 diff --git a/src/article_cli/commands/doctor.py b/src/article_cli/commands/doctor.py new file mode 100644 index 0000000..3f6c4de --- /dev/null +++ b/src/article_cli/commands/doctor.py @@ -0,0 +1,67 @@ +""" +Project diagnostics command. +""" + +import argparse +from typing import Any + +from ..config import Config +from ..doctor import DoctorService, print_doctor_report, report_to_json + + +def add_parser(subparsers: Any) -> None: + """Register the doctor command parser.""" + parser = subparsers.add_parser( + "doctor", + help="Diagnose repository setup, build, bibliography, and release readiness", + ) + parser.add_argument( + "document", + nargs="?", + help="Main document to check (.tex or .typ). Defaults to project config or auto-detection.", + ) + parser.add_argument( + "--engine", + choices=["latexmk", "pdflatex", "xelatex", "lualatex", "typst"], + help="Compilation engine to validate. Defaults to project config.", + ) + parser.add_argument( + "--output-dir", + help="Output directory to validate. Defaults to project config.", + ) + parser.add_argument( + "--tag", + help="Release tag to validate for release readiness.", + ) + parser.add_argument( + "--fix", + action="store_true", + help=( + "Apply safe repairs: create output directories, install managed " + "git hooks, and refresh gitHeadLocal.gin." + ), + ) + parser.add_argument( + "--json", + action="store_true", + help="Emit stable machine-readable JSON.", + ) + parser.set_defaults(handler=run) + + +def run(args: argparse.Namespace, config: Config) -> int: + """Handle the doctor command.""" + report = DoctorService(config).run( + document=args.document, + engine=args.engine, + output_dir=args.output_dir, + tag=args.tag, + fix=args.fix, + ) + + if args.json: + print(report_to_json(report)) + else: + print_doctor_report(report) + + return 0 if report.ok else 1 diff --git a/src/article_cli/commands/fonts.py b/src/article_cli/commands/fonts.py new file mode 100644 index 0000000..231dd41 --- /dev/null +++ b/src/article_cli/commands/fonts.py @@ -0,0 +1,70 @@ +""" +Font installation command. +""" + +import argparse +from pathlib import Path +from typing import Any + +from ..config import Config +from ..reporting import print_error, print_info + + +def add_parser(subparsers: Any) -> None: + """Register the install-fonts command parser.""" + parser = subparsers.add_parser( + "install-fonts", help="Download and install fonts for XeLaTeX projects" + ) + parser.add_argument( + "--dir", + type=Path, + help="Directory to install fonts (default: fonts/)", + ) + parser.add_argument( + "--force", + action="store_true", + help="Re-download fonts even if already installed", + ) + parser.add_argument( + "--list", + action="store_true", + dest="list_fonts", + help="List installed fonts instead of installing", + ) + parser.set_defaults(handler=run) + + +def run(args: argparse.Namespace, config: Config) -> int: + """Handle the install-fonts command.""" + try: + from ..fonts import FontInstaller + + fonts_config = config.get_fonts_config() + fonts_dir = ( + args.dir if args.dir else Path(fonts_config.get("directory", "fonts")) + ) + + if args.list_fonts: + installer = FontInstaller(fonts_dir=fonts_dir) + installed = installer.list_installed() + + if not installed: + print_info(f"No fonts installed in {fonts_dir}") + return 0 + + print_info(f"Installed fonts in {fonts_dir}:") + for font_name in installed: + font_files = installer.get_font_files(font_name) + print(f" - {font_name} ({len(font_files)} font files)") + + return 0 + + sources = fonts_config.get("sources", []) + installer = FontInstaller(fonts_dir=fonts_dir, sources=sources) + + success = installer.install_all(force=args.force) + return 0 if success else 1 + + except Exception as e: + print_error(f"Font installation failed: {e}") + return 1 diff --git a/src/article_cli/commands/init.py b/src/article_cli/commands/init.py new file mode 100644 index 0000000..a3e0256 --- /dev/null +++ b/src/article_cli/commands/init.py @@ -0,0 +1,156 @@ +""" +Repository initialization command. +""" + +import argparse +from typing import Any + +from ..config import Config +from ..reporting import print_error +from ..repository_setup import RepositorySetup +from ..services.workflow import WorkflowService + + +def add_parser(subparsers: Any) -> None: + """Register the init command parser.""" + parser = subparsers.add_parser( + "init", help="Initialize repository with workflows and configuration" + ) + parser.add_argument("--title", required=True, help="Article title") + parser.add_argument( + "--authors", + required=True, + help='Comma-separated list of authors (e.g., "John Doe,Jane Smith")', + ) + parser.add_argument( + "--group-id", + default="4678293", + help="Zotero group ID (default: 4678293 for article.template)", + ) + parser.add_argument( + "--tex-file", + help="Main .tex or .typ file (auto-detected if not specified)", + ) + parser.add_argument( + "--force", + action="store_true", + help="Overwrite existing files", + ) + parser.add_argument( + "--type", + choices=[ + "article", + "typst-article", + "presentation", + "poster", + "typst-presentation", + "typst-poster", + ], + default="article", + help=( + "Project type (default: article). Use 'typst-article' for a Typst " + "article, 'presentation' for Beamer, and 'typst-presentation' for " + "Typst slides." + ), + ) + parser.add_argument( + "--theme", + default="", + help="Theme for presentations (e.g., 'numpex'). Works with both Beamer and Typst.", + ) + parser.add_argument( + "--aspect-ratio", + choices=["169", "43", "1610"], + default="169", + help="Aspect ratio for presentations (default: 169 for 16:9).", + ) + parser.add_argument( + "--style", + default="default", + help=( + "Built-in article style to use when creating a new source file " + "(default: default; examples: lncs, ieee)." + ), + ) + parser.add_argument( + "--template", + default="", + help=( + "Path to a custom Jinja2 source template for TeX or Typst articles. " + "Overrides --style for the generated source file." + ), + ) + parser.add_argument( + "--ci-bib", + choices=["off", "check", "update", "required"], + default="off", + help="Generated CI bibliography policy (default: off).", + ) + parser.add_argument( + "--ci-runner-policy", + choices=["github", "self-hosted", "self-hosted-auto"], + default="github", + help="Generated CI runner policy (default: github).", + ) + parser.add_argument( + "--ci-github-runner", + default="ubuntu-24.04", + help="GitHub-hosted runner for generated CI (default: ubuntu-24.04).", + ) + parser.add_argument( + "--ci-self-hosted-label", + default="self-texlive", + help="Self-hosted runner label when generated CI opts into it.", + ) + parser.add_argument( + "--ci-self-hosted-org", + default="", + help="GitHub organization used for opt-in self-hosted runner discovery.", + ) + parser.add_argument( + "--ci-release", + choices=["github", "off"], + default="github", + help="Generated CI release policy (default: github).", + ) + parser.add_argument( + "--ci-artifact", + action="append", + default=[], + help="Extra artifact path/glob to include in generated CI artifacts.", + ) + parser.set_defaults(handler=run) + + +def run(args: argparse.Namespace, config: Config) -> int: + """Handle the init command.""" + try: + authors = [a.strip() for a in args.authors.split(",")] + + service = WorkflowService(setup_cls=RepositorySetup) + return ( + 0 + if service.initialize_repository( + title=args.title, + authors=authors, + group_id=args.group_id, + force=args.force, + main_tex_file=args.tex_file, + project_type=args.type, + theme=args.theme, + aspect_ratio=args.aspect_ratio, + style=args.style, + template=args.template, + ci_bibliography=args.ci_bib, + ci_runner_policy=args.ci_runner_policy, + ci_github_runner=args.ci_github_runner, + ci_self_hosted_label=args.ci_self_hosted_label, + ci_self_hosted_org=args.ci_self_hosted_org, + ci_release_policy=args.ci_release, + ci_artifact_includes=args.ci_artifact, + ) + else 1 + ) + except Exception as e: + print_error(f"Failed to initialize repository: {e}") + return 1 diff --git a/src/article_cli/commands/release.py b/src/article_cli/commands/release.py new file mode 100644 index 0000000..cd09a15 --- /dev/null +++ b/src/article_cli/commands/release.py @@ -0,0 +1,172 @@ +""" +Release tag commands. +""" + +import argparse +from typing import Any + +from ..config import Config +from ..git_manager import GitManager +from ..reporting import print_error, print_warning +from ..services.release import ReleaseOptions, ReleaseService + + +def add_parser(subparsers: Any) -> None: + """Register release-related command parsers.""" + release_parser = subparsers.add_parser( + "release", + help="Create a release tag", + description="Create a release tag for the current paper state.", + ) + _add_release_arguments(release_parser) + release_parser.set_defaults(handler=run_release) + + create_parser = subparsers.add_parser( + "create", + help="Deprecated alias for 'release'", + description="Deprecated alias for 'article-cli release'.", + ) + _add_release_arguments(create_parser) + create_parser.set_defaults(handler=run_create) + + list_parser = subparsers.add_parser("list", help="List releases") + list_parser.add_argument( + "--count", type=int, default=5, help="Number of releases to show" + ) + list_parser.set_defaults(handler=run_list) + + delete_parser = subparsers.add_parser("delete", help="Delete a release") + delete_parser.add_argument("version", help="Version tag to delete") + delete_parser.add_argument( + "--remote", action="store_true", help="Also delete from remote" + ) + delete_parser.set_defaults(handler=run_delete) + + +def _add_release_arguments(parser: argparse.ArgumentParser) -> None: + """Add shared release arguments.""" + parser.add_argument("version", help="Version tag (e.g., v1.0.0)") + parser.add_argument( + "--push", action="store_true", help="Automatically push the release" + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate and report the release plan without creating a tag", + ) + parser.add_argument( + "--force", + action="store_true", + help="Move an existing local tag explicitly", + ) + parser.add_argument( + "--commit", + action="store_true", + help="Commit gitHeadLocal.gin before tagging", + ) + parser.add_argument( + "--allow-dirty", + action="store_true", + help="Allow release with dirty files other than gitHeadLocal.gin", + ) + parser.add_argument( + "--no-compile", + action="store_true", + help="Do not compile before and after tagging", + ) + parser.add_argument( + "--no-pdf-check", + action="store_true", + help="Do not check that the PDF text contains the release tag", + ) + parser.add_argument( + "--bib", + choices=["off", "check", "update"], + help="Bibliography policy for the release", + ) + parser.add_argument( + "--github-release", + action="store_true", + help="Create a GitHub release with gh after local checks pass", + ) + parser.add_argument( + "--no-checksum", + action="store_true", + help="Do not write a sha256 checksum sidecar for the PDF", + ) + parser.add_argument( + "--tag-policy", + choices=["paper", "semver", "loose"], + help="Tag validation policy", + ) + parser.add_argument("--document", help="Document to compile for release") + parser.add_argument("--engine", help="Compilation engine override") + parser.add_argument("--output-dir", help="Compilation output directory") + parser.add_argument( + "--shell-escape", + action=argparse.BooleanOptionalAction, + default=None, + help="Override shell-escape for release compilation", + ) + + +def run_release(args: argparse.Namespace, config: Config) -> int: + """Handle the release command.""" + try: + service = ReleaseService(config, manager_cls=GitManager) + auto_push = getattr(args, "push", False) + return ( + 0 + if service.release( + ReleaseOptions( + tag=args.version, + auto_push=auto_push, + dry_run=getattr(args, "dry_run", False), + force=getattr(args, "force", False), + commit=getattr(args, "commit", False), + allow_dirty=True if getattr(args, "allow_dirty", False) else None, + compile_pdf=False if getattr(args, "no_compile", False) else None, + check_pdf=False if getattr(args, "no_pdf_check", False) else None, + bibliography=getattr(args, "bib", None), + github_release=( + True if getattr(args, "github_release", False) else None + ), + checksum=False if getattr(args, "no_checksum", False) else None, + tag_policy=getattr(args, "tag_policy", None), + document=getattr(args, "document", None), + engine=getattr(args, "engine", None), + output_dir=getattr(args, "output_dir", None), + shell_escape=getattr(args, "shell_escape", None), + ) + ) + else 1 + ) + except (RuntimeError, ValueError) as e: + print_error(str(e)) + return 1 + + +def run_create(args: argparse.Namespace, config: Config) -> int: + """Handle the deprecated create command.""" + print_warning("'create' is deprecated; use 'article-cli release '.") + return run_release(args, config) + + +def run_list(args: argparse.Namespace, config: Config) -> int: + """Handle the list command.""" + try: + service = ReleaseService(manager_cls=GitManager) + return 0 if service.list(args.count) else 1 + except ValueError as e: + print_error(str(e)) + return 1 + + +def run_delete(args: argparse.Namespace, config: Config) -> int: + """Handle the delete command.""" + try: + service = ReleaseService(manager_cls=GitManager) + return 0 if service.delete(args.version, args.remote) else 1 + except ValueError as e: + print_error(str(e)) + return 1 diff --git a/src/article_cli/commands/setup.py b/src/article_cli/commands/setup.py new file mode 100644 index 0000000..4b9735f --- /dev/null +++ b/src/article_cli/commands/setup.py @@ -0,0 +1,32 @@ +""" +Git hook setup command. +""" + +import argparse +from typing import Any + +from ..config import Config +from ..git_manager import GitManager +from ..reporting import print_error +from ..services.git import GitService + + +def add_parser(subparsers: Any) -> None: + """Register the setup command parser.""" + parser = subparsers.add_parser("setup", help="Setup git hooks for gitinfo2") + parser.add_argument( + "--dry-run", + action="store_true", + help="Report setup actions without creating hooks or metadata files", + ) + parser.set_defaults(handler=run) + + +def run(args: argparse.Namespace, config: Config) -> int: + """Handle the setup command.""" + try: + service = GitService(manager_cls=GitManager) + return 0 if service.setup_hooks(dry_run=getattr(args, "dry_run", False)) else 1 + except (RuntimeError, ValueError) as e: + print_error(str(e)) + return 1 diff --git a/src/article_cli/commands/themes.py b/src/article_cli/commands/themes.py new file mode 100644 index 0000000..daee7df --- /dev/null +++ b/src/article_cli/commands/themes.py @@ -0,0 +1,97 @@ +""" +Theme installation command. +""" + +import argparse +from pathlib import Path +from typing import Any + +from ..config import Config +from ..reporting import print_error, print_info + + +def add_parser(subparsers: Any) -> None: + """Register the install-theme command parser.""" + parser = subparsers.add_parser( + "install-theme", help="Download and install Beamer themes for presentations" + ) + parser.add_argument( + "theme_name", + nargs="?", + help="Name of theme to install (e.g., 'numpex')", + ) + parser.add_argument( + "--dir", + type=Path, + help="Directory to install theme (default: current directory)", + ) + parser.add_argument( + "--force", + action="store_true", + help="Re-download theme even if already installed", + ) + parser.add_argument( + "--list", + action="store_true", + dest="list_themes", + help="List available themes instead of installing", + ) + parser.add_argument( + "--url", + help="Custom URL to download theme from (use with theme_name)", + ) + parser.set_defaults(handler=run) + + +def run(args: argparse.Namespace, config: Config) -> int: + """Handle the install-theme command.""" + try: + from ..themes import ThemeInstaller + + themes_config = config.get_themes_config() + themes_dir = args.dir if args.dir else Path(themes_config.get("directory", ".")) + sources = themes_config.get("sources", {}) + + installer = ThemeInstaller(themes_dir=themes_dir, sources=sources) + + if args.list_themes: + available = installer.list_available() + installed = installer.list_installed() + + print_info("Available themes:") + for theme in available: + name = theme["name"] + desc = theme.get("description", "") + engine = theme.get("engine", "pdflatex") + fonts = ( + " (requires custom fonts)" if theme.get("requires_fonts") else "" + ) + installed_marker = ( + " [installed]" if any(i["name"] == name for i in installed) else "" + ) + print(f" - {name}: {desc}") + print(f" Engine: {engine}{fonts}{installed_marker}") + + return 0 + + if not args.theme_name: + print_error("Theme name is required. Use --list to see available themes.") + return 1 + + if args.url: + success = installer.install_from_url( + name=args.theme_name, + url=args.url, + force=args.force, + ) + else: + success = installer.install_theme( + name=args.theme_name, + force=args.force, + ) + + return 0 if success else 1 + + except Exception as e: + print_error(f"Theme installation failed: {e}") + return 1 diff --git a/src/article_cli/commands/version.py b/src/article_cli/commands/version.py new file mode 100644 index 0000000..288d89c --- /dev/null +++ b/src/article_cli/commands/version.py @@ -0,0 +1,83 @@ +""" +Version metadata command. +""" + +import argparse +from typing import Any + +from ..config import Config +from ..git_manager import GitManager +from ..reporting import print_error +from ..services.gitinfo import GitInfoService, VersionOptions + + +def add_parser(subparsers: Any) -> None: + """Register the version command parser.""" + parser = subparsers.add_parser( + "version", + help="Refresh and report git version metadata", + description=( + "Refresh gitinfo2 metadata and report the current git version state " + "without creating a tag." + ), + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Report the current git version state without refreshing files", + ) + parser.add_argument( + "--compile", + action="store_true", + help="Compile the document after refreshing version metadata", + ) + parser.add_argument( + "--check-pdf", + action="store_true", + help="Check that the PDF text contains the current git description", + ) + parser.add_argument( + "--tag", + help="Expected version string to check in the PDF instead of git describe", + ) + parser.add_argument( + "--checksum", + action="store_true", + help="Write a sha256 checksum sidecar for the resolved PDF", + ) + parser.add_argument("--document", help="Document to compile or inspect") + parser.add_argument("--engine", help="Compilation engine override") + parser.add_argument("--output-dir", help="Compilation output directory") + parser.add_argument( + "--shell-escape", + action=argparse.BooleanOptionalAction, + default=None, + help="Override shell-escape for version compilation", + ) + parser.set_defaults(handler=run) + + +def run(args: argparse.Namespace, config: Config) -> int: + """Handle the version command.""" + try: + service = GitInfoService(config, manager_cls=GitManager) + return ( + 0 + if service.refresh( + VersionOptions( + dry_run=getattr(args, "dry_run", False), + compile_pdf=getattr(args, "compile", False), + check_pdf=getattr(args, "check_pdf", False), + checksum=getattr(args, "checksum", False), + tag=getattr(args, "tag", None), + document=getattr(args, "document", None), + engine=getattr(args, "engine", None), + output_dir=getattr(args, "output_dir", None), + shell_escape=getattr(args, "shell_escape", None), + ) + ) + else 1 + ) + except (RuntimeError, ValueError) as e: + print_error(str(e)) + return 1 diff --git a/src/article_cli/config.py b/src/article_cli/config.py index 118d37a..a6036d9 100644 --- a/src/article_cli/config.py +++ b/src/article_cli/config.py @@ -21,14 +21,19 @@ class Config: """Configuration manager for article-cli""" - def __init__(self, config_file: Optional[Union[str, Path]] = None): + def __init__( + self, config_file: Optional[Union[str, Path]] = None, quiet: bool = False + ): """ Initialize configuration manager Args: config_file: Optional path to configuration file + quiet: Suppress informational config-loading messages """ self.config_file = config_file + self.quiet = quiet + self.loaded_config_file: Optional[Path] = None self._config_data: Dict[str, Any] = {} self._load_config() @@ -60,6 +65,7 @@ def _find_config_file(self) -> Optional[Path]: def _load_config(self) -> None: """Load configuration from file if it exists""" config_path = self._find_config_file() + self.loaded_config_file = config_path.resolve() if config_path else None if config_path and tomllib: try: @@ -73,9 +79,10 @@ def _load_config(self) -> None: "article-cli", {} ) if self._config_data: - print( - f"Loaded configuration from: {config_path} [tool.article-cli]" - ) + if not self.quiet: + print( + f"Loaded configuration from: {config_path} [tool.article-cli]" + ) else: # Fallback: look for legacy sections at root level legacy_sections = ["zotero", "git", "latex"] @@ -83,19 +90,25 @@ def _load_config(self) -> None: k: v for k, v in full_config.items() if k in legacy_sections } if self._config_data: - print(f"Loaded legacy configuration from: {config_path}") + if not self.quiet: + print( + f"Loaded legacy configuration from: {config_path}" + ) else: # Dedicated config file - use as-is self._config_data = full_config - print(f"Loaded configuration from: {config_path}") + if not self.quiet: + print(f"Loaded configuration from: {config_path}") except Exception as e: - print(f"Warning: Could not load config file {config_path}: {e}") + if not self.quiet: + print(f"Warning: Could not load config file {config_path}: {e}") self._config_data = {} elif config_path and not tomllib: - print( - "Warning: TOML support not available. Install with: pip install tomli" - ) + if not self.quiet: + print( + "Warning: TOML support not available. Install dependencies with: uv sync" + ) self._config_data = {} else: self._config_data = {} @@ -125,15 +138,21 @@ def get( return default - def get_zotero_config(self) -> Dict[str, Optional[str]]: + def get_zotero_config(self) -> Dict[str, Any]: """Get Zotero-specific configuration""" return { "api_key": self.get("zotero", "api_key", env_var="ZOTERO_API_KEY"), "user_id": self.get("zotero", "user_id", env_var="ZOTERO_USER_ID"), "group_id": self.get("zotero", "group_id", env_var="ZOTERO_GROUP_ID"), + "collection_id": self.get( + "zotero", "collection_id", "", env_var="ZOTERO_COLLECTION_ID" + ), "output_file": self.get( "zotero", "output_file", "references.bib", env_var="BIBTEX_FILE" ), + "local_file": self.get("zotero", "local_file", "local_references.bib"), + "merged_output_file": self.get("zotero", "merged_output_file", ""), + "deterministic": self.get("zotero", "deterministic", True), } def get_git_config(self) -> Dict[str, Any]: @@ -143,6 +162,18 @@ def get_git_config(self) -> Dict[str, Any]: "default_branch": self.get("git", "default_branch", "main"), } + def get_release_config(self) -> Dict[str, Any]: + """Get release workflow configuration.""" + return { + "tag_policy": self.get("release", "tag_policy", "paper"), + "allow_dirty": self.get("release", "allow_dirty", False), + "compile": self.get("release", "compile", True), + "check_pdf": self.get("release", "check_pdf", True), + "checksum": self.get("release", "checksum", True), + "bibliography": self.get("release", "bibliography", "off"), + "github_release": self.get("release", "github_release", False), + } + def get_latex_config(self) -> Dict[str, Any]: """Get LaTeX-specific configuration""" default_extensions = [ @@ -178,6 +209,8 @@ def get_project_config(self) -> Dict[str, Any]: """Get project-level configuration""" return { "project_type": self.get("project", "type", "article"), + "style": self.get("project", "style", "default"), + "template": self.get("project", "template", ""), } def get_documents_config(self) -> Dict[str, Any]: @@ -193,6 +226,15 @@ def get_workflow_config(self) -> Dict[str, Any]: "output_dir": self.get("workflow", "output_dir", ""), "fonts_dir": self.get("workflow", "fonts_dir", ""), "install_fonts": self.get("workflow", "install_fonts", False), + "runner_policy": self.get("workflow", "runner_policy", "github"), + "github_runner": self.get("workflow", "github_runner", "ubuntu-24.04"), + "self_hosted_label": self.get( + "workflow", "self_hosted_label", "self-texlive" + ), + "self_hosted_org": self.get("workflow", "self_hosted_org", ""), + "bibliography": self.get("workflow", "bibliography", "off"), + "release": self.get("workflow", "release", "github"), + "artifact_includes": self.get("workflow", "artifact_includes", []), } def get_presentation_config(self) -> Dict[str, Any]: @@ -255,9 +297,7 @@ def get_typst_config(self) -> Dict[str, Any]: "build_dir": self.get("typst", "build_dir", ""), } - def validate_zotero_config( - self, args: argparse.Namespace - ) -> Dict[str, Optional[str]]: + def validate_zotero_config(self, args: argparse.Namespace) -> Dict[str, Any]: """ Validate and merge Zotero configuration from args and config @@ -279,8 +319,14 @@ def validate_zotero_config( config["user_id"] = args.user_id if hasattr(args, "group_id") and args.group_id: config["group_id"] = args.group_id + if hasattr(args, "collection") and args.collection: + config["collection_id"] = args.collection if hasattr(args, "output") and args.output: config["output_file"] = args.output + if hasattr(args, "local_file") and args.local_file: + config["local_file"] = args.local_file + if hasattr(args, "merged_output") and args.merged_output: + config["merged_output_file"] = args.merged_output # Validate required fields if not config["api_key"]: @@ -328,13 +374,36 @@ def create_sample_config(self, path: Optional[Path] = None) -> Path: # Output file for bibliography output_file = "references.bib" +# Optional Zotero collection or subcollection key +# collection_id = "" + +# Optional local/manual entries. Use article-cli bib update --include-local +# to merge them into the output or --merged-output to write a separate file. +local_file = "local_references.bib" +# merged_output_file = "" + +# Deterministic output omits timestamps and writes only when content changes. +deterministic = true + [git] # Automatically push after creating releases -auto_push = true +auto_push = false # Default branch name default_branch = "main" +[release] +# Tag policy: "paper" accepts v1, v1.0, v1.0.0 and prerelease suffixes. +# Use "semver" for strict vX.Y.Z, or "loose" for any non-space tag. +tag_policy = "paper" +allow_dirty = false +compile = true +check_pdf = true +checksum = true +# bibliography policy: "off", "check", or "update" +bibliography = "off" +github_release = false + [latex] # File extensions to clean clean_extensions = [ @@ -358,9 +427,14 @@ def create_sample_config(self, path: Optional[Path] = None) -> Path: timeout = 300 [project] -# Project type: "article", "presentation", or "poster" +# Project type: "article", "typst-article", "presentation", "typst-presentation", +# "poster", or "typst-poster" type = "article" +# Built-in source style for generated article files, e.g. "default", "lncs", "ieee". +# Use article-cli init --template PATH for a project-specific Jinja2 template. +style = "default" + # Presentation-specific settings (only used when type = "presentation") [presentation] # Beamer theme (e.g., "numpex", "metropolis", "default") @@ -396,6 +470,22 @@ def create_sample_config(self, path: Optional[Path] = None) -> Path: # Workflow settings for GitHub Actions [workflow] +# Runner policy: "github", "self-hosted", or "self-hosted-auto". +runner_policy = "github" +github_runner = "ubuntu-24.04" +self_hosted_label = "self-texlive" +# Self-hosted auto-discovery only runs when this organization is set. +self_hosted_org = "" + +# Bibliography policy in generated CI: "off", "check", "update", or "required". +bibliography = "off" + +# Release policy in generated CI: "github" or "off". +release = "github" + +# Extra artifact path globs to include in generated CI artifacts. +artifact_includes = [] + # Output directory for compiled files (empty string means root directory) # output_dir = "build" diff --git a/src/article_cli/doctor.py b/src/article_cli/doctor.py new file mode 100644 index 0000000..6468b4d --- /dev/null +++ b/src/article_cli/doctor.py @@ -0,0 +1,968 @@ +""" +Project diagnostics and safe repairs for article-cli. +""" + +from __future__ import annotations + +import contextlib +import io +import importlib +import json +import os +import re +import shutil +import subprocess +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional + +from .config import Config +from .git_hooks import ( + MANAGED_HOOK_START, + render_gitinfo2_metadata, +) +from .git_manager import GitManager +from .project_context import ProjectContext + + +@dataclass +class DoctorCheck: + """Single diagnostic check.""" + + category: str + name: str + status: str + message: str + next_command: Optional[str] = None + details: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + """Return a stable JSON-compatible representation.""" + data: Dict[str, Any] = { + "category": self.category, + "name": self.name, + "status": self.status, + "message": self.message, + } + if self.next_command: + data["next_command"] = self.next_command + if self.details: + data["details"] = self.details + return data + + +@dataclass +class DoctorReport: + """Complete doctor report.""" + + context: Dict[str, Any] + checks: List[DoctorCheck] + + @property + def error_count(self) -> int: + """Number of blocking errors.""" + return sum(1 for check in self.checks if check.status == "error") + + @property + def warning_count(self) -> int: + """Number of warnings.""" + return sum(1 for check in self.checks if check.status == "warning") + + @property + def ok(self) -> bool: + """Whether no blocking errors were found.""" + return self.error_count == 0 + + @property + def next_commands(self) -> List[str]: + """Deduplicated list of suggested next commands.""" + commands = [] + for check in self.checks: + if check.next_command and check.next_command not in commands: + commands.append(check.next_command) + return commands + + def to_dict(self) -> Dict[str, Any]: + """Return a stable JSON-compatible representation.""" + return { + "ok": self.ok, + "summary": { + "errors": self.error_count, + "warnings": self.warning_count, + }, + "context": self.context, + "checks": [check.to_dict() for check in self.checks], + "next_commands": self.next_commands, + } + + +class DoctorService: + """Run diagnostics and optional safe repairs for an article repository.""" + + def __init__(self, config: Config, cwd: Optional[Path] = None): + self.config = config + self.cwd = (cwd or Path.cwd()).resolve() + self._reset_diagnostics() + + def _reset_diagnostics(self) -> None: + """Reset mutable diagnostic state before a run.""" + self.context: Dict[str, Any] = {"cwd": str(self.cwd)} + self.checks: List[DoctorCheck] = [] + self.repo_root: Optional[Path] = None + self.hooks_path: Optional[Path] = None + self.main_document: Optional[Path] = None + self.engine: str = "latexmk" + self.output_dir: Optional[Path] = None + self.project_context: Optional[ProjectContext] = None + self.dirty_tracked_files: List[str] = [] + + def run( + self, + document: Optional[str] = None, + engine: Optional[str] = None, + output_dir: Optional[str] = None, + tag: Optional[str] = None, + fix: bool = False, + ) -> DoctorReport: + """Run all diagnostics.""" + self._reset_diagnostics() + self._run_diagnostics(document, engine, output_dir, tag) + + if fix: + fix_checks = self._apply_safe_fixes() + self._reset_diagnostics() + self._run_diagnostics(document, engine, output_dir, tag) + self.context["fix_applied"] = True + self.checks = fix_checks + self.checks + + return DoctorReport(context=self.context, checks=self.checks) + + def _run_diagnostics( + self, + document: Optional[str], + engine: Optional[str], + output_dir: Optional[str], + tag: Optional[str], + ) -> None: + """Run the diagnostic checks against the current filesystem state.""" + self._check_git() + self._resolve_project_context(document, engine, output_dir) + self._check_git_metadata(tag) + self._check_build_readiness() + self._check_bibliography() + self._check_workflow() + self._check_release_readiness(tag) + + def _apply_safe_fixes(self) -> List[DoctorCheck]: + """Apply non-destructive repairs and return repair diagnostics.""" + checks: List[DoctorCheck] = [] + self._fix_output_directory(checks) + self._fix_git_setup(checks) + return checks + + def _fix_output_directory(self, checks: List[DoctorCheck]) -> None: + """Create the configured output directory when it is safe to do so.""" + if self.output_dir is None: + checks.append( + DoctorCheck( + "fix", + "output-directory", + "info", + "Output directory is the project root; no repair needed.", + ) + ) + return + + if self.output_dir.exists() and self.output_dir.is_dir(): + checks.append( + DoctorCheck( + "fix", + "output-directory", + "info", + "Output directory already exists.", + details={"path": str(self.output_dir)}, + ) + ) + return + + if self.output_dir.exists(): + checks.append( + DoctorCheck( + "fix", + "output-directory", + "error", + "Output path exists but is not a directory; not modified.", + details={"path": str(self.output_dir)}, + ) + ) + return + + try: + self.output_dir.mkdir(parents=True, exist_ok=True) + except OSError as e: + checks.append( + DoctorCheck( + "fix", + "output-directory", + "error", + f"Could not create output directory: {e}", + details={"path": str(self.output_dir)}, + ) + ) + return + + checks.append( + DoctorCheck( + "fix", + "output-directory", + "ok", + "Created output directory.", + details={"path": str(self.output_dir)}, + ) + ) + + def _fix_git_setup(self, checks: List[DoctorCheck]) -> None: + """Install managed git hooks and refresh gitinfo2 metadata.""" + if self.repo_root is None: + checks.append( + DoctorCheck( + "fix", + "git-hooks", + "warning", + "No Git repository found; hooks and gitHeadLocal.gin were not changed.", + next_command="git init", + ) + ) + return + + try: + manager = GitManager(self.repo_root) + with contextlib.redirect_stdout(io.StringIO()): + ok = manager.setup_hooks(dry_run=False) + except Exception as e: + checks.append( + DoctorCheck( + "fix", + "git-hooks", + "error", + f"Could not install managed git hooks: {e}", + ) + ) + return + + if ok: + checks.append( + DoctorCheck( + "fix", + "git-hooks", + "ok", + "Installed or updated managed gitinfo2 hooks safely.", + ) + ) + else: + checks.append( + DoctorCheck( + "fix", + "git-hooks", + "error", + "Managed git hooks could not be installed.", + ) + ) + return + + metadata_path = self.repo_root / "gitHeadLocal.gin" + if metadata_path.exists(): + checks.append( + DoctorCheck( + "fix", + "gitHeadLocal", + "ok", + "Refreshed gitHeadLocal.gin.", + details={"path": str(metadata_path)}, + ) + ) + else: + checks.append( + DoctorCheck( + "fix", + "gitHeadLocal", + "warning", + "gitHeadLocal.gin was not refreshed; make an initial commit first.", + next_command="git commit", + ) + ) + + def _check_git(self) -> None: + """Resolve and check git repository state.""" + result = self._git(["rev-parse", "--show-toplevel"], cwd=self.cwd) + if result is None or result.returncode != 0: + self._add( + "git", + "repository", + "error", + "Current directory is not inside a Git repository.", + next_command="git init", + ) + self.context["git_root"] = None + return + + self.repo_root = Path(result.stdout.strip()).resolve() + self.context["repository_root"] = str(self.repo_root) + self.context["git_root"] = str(self.repo_root) + self._add("git", "repository", "ok", "Git repository found.") + + hooks_result = self._git(["rev-parse", "--git-path", "hooks"]) + if hooks_result and hooks_result.returncode == 0: + self.hooks_path = self._resolve_git_path(hooks_result.stdout.strip()) + self.context["git_hooks_path"] = str(self.hooks_path) + self._add("git", "hooks-path", "ok", "Git hooks path resolved.") + else: + self._add( + "git", + "hooks-path", + "error", + "Could not resolve Git hooks path.", + next_command="article-cli setup", + ) + + branch = self._git_output(["symbolic-ref", "--quiet", "--short", "HEAD"]) + commit = self._git_output(["rev-parse", "--short", "HEAD"]) + describe = self._git_output( + ["describe", "--tags", "--long", "--always", "--dirty=-*"] + ) + self.context["current_branch"] = branch or "DETACHED" + self.context["current_commit"] = commit + self.context["git_describe"] = describe + + status = self._git( + [ + "status", + "--porcelain", + "--untracked-files=no", + "--", + ".", + ":(exclude)gitHeadLocal.gin", + ] + ) + if status is None or status.returncode != 0: + self._add("git", "working-tree", "warning", "Could not inspect git status.") + return + + self.dirty_tracked_files = [ + line.strip() for line in status.stdout.splitlines() if line.strip() + ] + self.context["dirty_tracked_files"] = self.dirty_tracked_files + if self.dirty_tracked_files: + self._add( + "git", + "working-tree", + "warning", + "Tracked files are modified, ignoring gitHeadLocal.gin.", + details={"files": self.dirty_tracked_files}, + ) + else: + self._add( + "git", + "working-tree", + "ok", + "No dirty tracked files other than gitHeadLocal.gin.", + ) + + def _resolve_project_context( + self, + document: Optional[str], + engine: Optional[str], + output_dir: Optional[str], + ) -> None: + """Resolve document, engine, config, project type, and output path.""" + project_root = self.repo_root or self.cwd + config_file = self.config.loaded_config_file + self.context["config_file"] = str(config_file) if config_file else None + + self.project_context = ProjectContext.resolve( + self.config, + cwd=project_root, + document=document, + engine=engine, + output_dir=output_dir, + ) + self.context.update(self.project_context.as_doctor_context()) + + self.main_document = self.project_context.document + if self.main_document is not None: + self.context["main_document"] = str(self.main_document) + + self.engine = self.project_context.engine + resolved_output = self.project_context.output_dir + if resolved_output is not None and str(resolved_output) != ".": + output_path = resolved_output + if not output_path.is_absolute(): + output_path = project_root / output_path + self.output_dir = output_path.resolve() + self.context["output_directory"] = str(self.output_dir) + else: + self.output_dir = None + self.context["output_directory"] = "." + + def _check_git_metadata(self, tag: Optional[str]) -> None: + """Check hooks and gitinfo2 metadata without modifying files.""" + if self.repo_root is None: + return + + source_hook = self.repo_root / "hooks" / "post-commit" + if source_hook.exists(): + self._add("git", "hook-source", "ok", "Repository hook source exists.") + else: + self._add( + "git", + "hook-source", + "warning", + "Repository hook source hooks/post-commit is missing.", + next_command="article-cli setup", + ) + + if self.hooks_path is None: + return + + missing_hooks = [] + for hook_name in ["post-commit", "post-checkout", "post-merge"]: + hook_path = self.hooks_path / hook_name + if not hook_path.exists(): + missing_hooks.append(hook_name) + continue + if not os.access(hook_path, os.X_OK): + self._add( + "git", + f"installed-hook:{hook_name}", + "warning", + f"Installed hook {hook_name} is not executable.", + next_command="article-cli setup", + ) + continue + content = hook_path.read_text(errors="replace") + if MANAGED_HOOK_START not in content and "gitHeadInfo.gin" not in content: + self._add( + "git", + f"installed-hook:{hook_name}", + "warning", + f"Installed hook {hook_name} does not appear to refresh gitinfo2.", + next_command="article-cli setup", + ) + else: + self._add( + "git", + f"installed-hook:{hook_name}", + "ok", + f"Installed hook {hook_name} is executable.", + ) + + if missing_hooks: + self._add( + "git", + "installed-hooks", + "warning", + "One or more gitinfo2 hooks are missing.", + next_command="article-cli setup", + details={"missing": missing_hooks}, + ) + + metadata_path = self.repo_root / "gitHeadLocal.gin" + if not metadata_path.exists(): + self._add( + "git", + "gitHeadLocal", + "warning", + "gitHeadLocal.gin is missing.", + next_command="article-cli compile", + ) + return + + expected_metadata = render_gitinfo2_metadata(self.repo_root) + if expected_metadata is None: + self._add( + "git", + "gitHeadLocal", + "warning", + "Could not render current gitinfo2 metadata.", + next_command="article-cli compile", + ) + return + + current_metadata = metadata_path.read_text(errors="replace").strip() + if current_metadata == expected_metadata.strip(): + self._add( + "git", + "gitHeadLocal", + "ok", + "gitHeadLocal.gin matches the current git state.", + ) + else: + status = "error" if tag else "warning" + self._add( + "git", + "gitHeadLocal", + status, + "gitHeadLocal.gin does not match the current git state.", + next_command="article-cli compile", + ) + + def _check_build_readiness(self) -> None: + """Check document and toolchain readiness.""" + if self.main_document is None: + self._add( + "build", + "main-document", + "error", + "No main .tex or .typ document was configured or detected.", + next_command="article-cli init --title TITLE --authors AUTHOR", + ) + return + + if self.main_document.exists(): + self._add("build", "main-document", "ok", "Main document exists.") + else: + self._add( + "build", + "main-document", + "error", + f"Main document does not exist: {self.main_document}", + ) + return + + if self.main_document.suffix == ".typ" and self.engine != "typst": + self._add( + "build", + "engine", + "error", + "A Typst document requires the typst engine.", + ) + elif self.main_document.suffix == ".tex" and self.engine == "typst": + self._add( + "build", + "engine", + "error", + "A LaTeX document cannot be compiled with the typst engine.", + ) + else: + self._add( + "build", + "engine", + "ok", + f"Selected engine: {self.engine}.", + ) + + required_tools = self._required_tools() + for tool in required_tools: + self._check_tool(tool, required=True) + + if self.engine != "typst": + for tool in ["bibtex", "biber"]: + self._check_tool(tool, required=False) + + self._check_output_directory() + + def _check_bibliography(self) -> None: + """Check Zotero and BibTeX configuration.""" + zotero_config = self.config.get_zotero_config() + output_file = zotero_config.get("output_file") or "references.bib" + project_root = self.repo_root or self.cwd + output_path = Path(output_file) + if not output_path.is_absolute(): + output_path = project_root / output_path + + self.context["bibliography_output"] = str(output_path) + if output_path.exists(): + self._add("bibliography", "output-file", "ok", "Bibliography file exists.") + else: + self._add( + "bibliography", + "output-file", + "warning", + f"Bibliography file does not exist: {output_file}.", + next_command="article-cli bib update", + ) + + if zotero_config.get("api_key"): + self._add("bibliography", "zotero-api-key", "ok", "Zotero API key is set.") + else: + self._add( + "bibliography", + "zotero-api-key", + "warning", + "Zotero API key is not set.", + ) + + if zotero_config.get("user_id") or zotero_config.get("group_id"): + self._add( + "bibliography", + "zotero-library", + "ok", + "Zotero user or group id is configured.", + ) + else: + self._add( + "bibliography", + "zotero-library", + "warning", + "Neither Zotero user id nor group id is configured.", + ) + + collection_id = self.config.get("zotero", "collection_id") + self.context["zotero_collection_id"] = collection_id + + local_files = [ + path.name + for path in [ + project_root / "local_references.bib", + project_root / "references.local.bib", + ] + if path.exists() + ] + if local_files: + self._add( + "bibliography", + "local-references", + "ok", + "Local bibliography preservation file detected.", + details={"files": local_files}, + ) + else: + self._add( + "bibliography", + "local-references", + "info", + "No local bibliography preservation file detected.", + ) + + def _check_workflow(self) -> None: + """Check generated GitHub Actions workflow readiness.""" + project_root = self.repo_root or self.cwd + workflow_path = project_root / ".github" / "workflows" / "latex.yml" + self.context["workflow"] = str(workflow_path) + + if not workflow_path.exists(): + self._add( + "workflow", + "latex-yml", + "warning", + ".github/workflows/latex.yml is missing.", + next_command="article-cli init --title TITLE --authors AUTHOR", + ) + return + + self._add("workflow", "latex-yml", "ok", "GitHub Actions workflow exists.") + raw_workflow = workflow_path.read_text(errors="replace") + + try: + yaml = importlib.import_module("yaml") + yaml.safe_load(raw_workflow) + self._add("workflow", "yaml-parse", "ok", "Workflow YAML parses.") + except Exception as e: + self._add( + "workflow", + "yaml-parse", + "error", + f"Workflow YAML does not parse: {e}", + ) + + if self.main_document and self.main_document.name in raw_workflow: + self._add( + "workflow", + "main-document", + "ok", + "Workflow references the main document name.", + ) + elif self.main_document: + self._add( + "workflow", + "main-document", + "warning", + "Workflow does not reference the resolved main document name.", + ) + + if "setup-uv" in raw_workflow or re.search(r"\buv\b", raw_workflow): + self._add("workflow", "package-manager", "ok", "Workflow uses uv.") + else: + self._add( + "workflow", + "package-manager", + "warning", + "Workflow does not appear to use uv.", + ) + + release_automation = bool(self.config.get("release", "automation", False)) + has_release_trigger = ( + "tags:" in raw_workflow or "action-gh-release" in raw_workflow + ) + if release_automation and not has_release_trigger: + self._add( + "workflow", + "release-trigger", + "warning", + "Release automation is enabled but no release trigger was detected.", + ) + elif has_release_trigger: + self._add( + "workflow", + "release-trigger", + "ok", + "Workflow contains release trigger or release action.", + ) + else: + self._add( + "workflow", + "release-trigger", + "info", + "Release automation is not enabled in project config.", + ) + + def _check_release_readiness(self, tag: Optional[str]) -> None: + """Check lightweight release readiness signals.""" + if self.repo_root is None: + return + + if tag: + tag_result = self._git(["rev-parse", "--verify", f"refs/tags/{tag}"]) + if tag_result and tag_result.returncode == 0: + self._add( + "release", + "tag", + "error", + f"Tag already exists: {tag}.", + ) + else: + self._add("release", "tag", "ok", f"Tag is available: {tag}.") + + if self.dirty_tracked_files: + self._add( + "release", + "working-tree", + "error", + "Tracked files must be clean before release.", + details={"files": self.dirty_tracked_files}, + ) + + pdf_path = self._expected_pdf_path() + if pdf_path is None: + return + self.context["expected_pdf"] = str(pdf_path) + + if pdf_path.exists(): + self._add("release", "pdf", "ok", "Expected PDF exists.") + self._check_pdf_version(pdf_path, tag) + else: + self._add( + "release", + "pdf", + "error" if tag else "warning", + f"Expected PDF is missing: {pdf_path.name}.", + next_command="article-cli compile", + ) + + def _check_pdf_version(self, pdf_path: Path, tag: Optional[str]) -> None: + """Try to inspect PDF text for a requested tag.""" + if not tag: + return + if shutil.which("pdftotext") is None: + self._add( + "release", + "pdf-version", + "warning", + "pdftotext is unavailable; PDF version string was not checked.", + ) + return + + result = subprocess.run( + ["pdftotext", str(pdf_path), "-"], + cwd=self.repo_root or self.cwd, + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + self._add( + "release", + "pdf-version", + "warning", + "Could not extract text from PDF for version checking.", + ) + return + + if tag in result.stdout: + self._add( + "release", + "pdf-version", + "ok", + f"PDF text contains requested tag {tag}.", + ) + else: + self._add( + "release", + "pdf-version", + "warning", + f"PDF text does not contain requested tag {tag}.", + next_command="article-cli compile", + ) + + def _required_tools(self) -> List[str]: + """Return required tools for the selected engine.""" + if self.engine == "typst": + return ["typst"] + if self.engine in {"pdflatex", "xelatex", "lualatex"}: + return [self.engine] + return ["latexmk"] + + def _check_tool(self, tool: str, required: bool) -> None: + """Check whether a command-line tool exists.""" + if shutil.which(tool): + self._add("build", f"tool:{tool}", "ok", f"Tool available: {tool}.") + return + + self._add( + "build", + f"tool:{tool}", + "error" if required else "warning", + f"Tool not found: {tool}.", + ) + + def _check_output_directory(self) -> None: + """Check whether output directory exists or can be created.""" + if self.output_dir is None: + self._add( + "build", + "output-directory", + "ok", + "Output directory is the project root.", + ) + return + + if self.output_dir.exists() and self.output_dir.is_dir(): + self._add("build", "output-directory", "ok", "Output directory exists.") + return + + if self.output_dir.exists() and not self.output_dir.is_dir(): + self._add( + "build", + "output-directory", + "error", + "Configured output path exists but is not a directory.", + ) + return + + parent = self.output_dir.parent + if parent.exists() and os.access(parent, os.W_OK): + self._add( + "build", + "output-directory", + "ok", + "Output directory does not exist yet but can be created.", + ) + else: + self._add( + "build", + "output-directory", + "error", + "Output directory parent is missing or not writable.", + ) + + def _expected_pdf_path(self) -> Optional[Path]: + """Return expected PDF path for the resolved main document.""" + if self.main_document is None: + return None + pdf_name = self.main_document.with_suffix(".pdf").name + if self.output_dir is not None: + return self.output_dir / pdf_name + return self.main_document.with_suffix(".pdf") + + def _git( + self, args: List[str], cwd: Optional[Path] = None + ) -> Optional[subprocess.CompletedProcess[str]]: + """Run git and return the completed process.""" + try: + return subprocess.run( + ["git", *args], + cwd=cwd or self.repo_root or self.cwd, + capture_output=True, + text=True, + check=False, + ) + except (FileNotFoundError, OSError): + return None + + def _git_output(self, args: List[str]) -> Optional[str]: + """Run git and return stripped stdout on success.""" + result = self._git(args) + if result is None or result.returncode != 0: + return None + output = result.stdout.strip() + return output or None + + def _resolve_git_path(self, path_text: str) -> Path: + """Resolve a git metadata path relative to the repository root.""" + path = Path(path_text) + if not path.is_absolute(): + path = (self.repo_root or self.cwd) / path + return path.resolve() + + def _add( + self, + category: str, + name: str, + status: str, + message: str, + next_command: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + ) -> None: + """Append a diagnostic check.""" + self.checks.append( + DoctorCheck( + category=category, + name=name, + status=status, + message=message, + next_command=next_command, + details=details or {}, + ) + ) + + +def print_doctor_report(report: DoctorReport) -> None: + """Print a human-readable doctor report.""" + print("article-cli doctor") + print("\nContext:") + for key in sorted(report.context): + value = report.context[key] + if value is not None: + print(f" {key}: {value}") + + print("\nChecks:") + for check in report.checks: + label = _status_label(check.status) + print(f" {label} {check.category}.{check.name}: {check.message}") + if check.next_command: + print(f" next: {check.next_command}") + + print( + f"\nSummary: {report.error_count} error(s), " + f"{report.warning_count} warning(s)" + ) + if report.next_commands: + print("\nSuggested next commands:") + for command in report.next_commands: + print(f" - {command}") + + +def report_to_json(report: DoctorReport) -> str: + """Serialize a doctor report as stable JSON.""" + return json.dumps(report.to_dict(), indent=2, sort_keys=True) + + +def _status_label(status: str) -> str: + """Return a short ASCII label for a check status.""" + labels = { + "ok": "[OK]", + "warning": "[WARN]", + "error": "[ERROR]", + "info": "[INFO]", + } + return labels.get(status, "[INFO]") diff --git a/src/article_cli/fonts.py b/src/article_cli/fonts.py index 7604975..70effda 100644 --- a/src/article_cli/fonts.py +++ b/src/article_cli/fonts.py @@ -11,7 +11,7 @@ import requests -from .zotero import print_error, print_info, print_success, print_warning +from .reporting import print_error, print_info, print_success, print_warning # Default font sources for common themes # Note: Marianne font from French government requires manual download due to Cloudflare protection. diff --git a/src/article_cli/git_hooks.py b/src/article_cli/git_hooks.py new file mode 100644 index 0000000..49c5505 --- /dev/null +++ b/src/article_cli/git_hooks.py @@ -0,0 +1,413 @@ +""" +Git hook templates and helpers for article-cli. +""" + +from __future__ import annotations + +import stat +import shutil +import subprocess +from pathlib import Path +from typing import List, Optional + +from .reporting import print_info, print_success, print_warning + +MANAGED_HOOK_START = "# >>> article-cli gitinfo2 hook >>>" +MANAGED_HOOK_END = "# <<< article-cli gitinfo2 hook <<<" + +GITINFO2_POST_COMMIT_HOOK = """#!/bin/sh +# Copyright 2015 Brent Longborough +# Part of gitinfo2 package Version 2 +# Release 2.0.7 2015-11-22 +# Please read gitinfo2.pdf for licencing and other details +# ----------------------------------------------------- +# Post-{commit,checkout,merge} hook for the gitinfo2 package +# +# Get the first tag found in the history from the current HEAD +FIRSTTAG=$(git describe --tags --always --dirty='-*' 2>/dev/null) +# Get the first tag in history that looks like a Release +RELTAG=$(git describe --tags --long --always --dirty='-*' --match 'v[0-9]*' 2>/dev/null) +GIT_HEAD_INFO=$(git rev-parse --git-path gitHeadInfo.gin) +mkdir -p "$(dirname "$GIT_HEAD_INFO")" +# Hoover up the metadata +git --no-pager log -1 --date=short --decorate=short \\ + --pretty=format:"\\usepackage[% + shash={%h}, + lhash={%H}, + authname={%an}, + authemail={%ae}, + authsdate={%ad}, + authidate={%ai}, + authudate={%at}, + commname={%cn}, + commemail={%ce}, + commsdate={%cd}, + commidate={%ci}, + commudate={%ct}, + refnames={%d}, + firsttagdescribe={$FIRSTTAG}, + reltag={$RELTAG} + ]{gitexinfo}" HEAD > "$GIT_HEAD_INFO" +""" + + +def ensure_gitinfo2_hook_source(repo_path: Path, force: bool = False) -> Path: + """ + Create hooks/post-commit for gitinfo2 if needed. + + Args: + repo_path: Repository root path + force: Overwrite existing hook if True + + Returns: + Path to hooks/post-commit + """ + hooks_dir = repo_path / "hooks" + hooks_dir.mkdir(parents=True, exist_ok=True) + + post_commit_path = hooks_dir / "post-commit" + if post_commit_path.exists() and not force: + print_info("hooks/post-commit already exists (use --force to overwrite)") + return post_commit_path + + post_commit_path.write_text(GITINFO2_POST_COMMIT_HOOK) + post_commit_path.chmod( + post_commit_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + ) + + print_success(f"Created: {post_commit_path.relative_to(repo_path)}") + print_info("Made post-commit hook executable") + + return post_commit_path + + +def install_managed_gitinfo2_hook(source_hook: Path, destination_hook: Path) -> str: + """ + Install or update the article-cli managed gitinfo2 hook block. + + Existing shell hooks are preserved. If a hook already contains an article-cli + managed block, only that block is replaced. + + Args: + source_hook: Repository-owned hooks/post-commit source hook. + destination_hook: Actual Git hook path resolved through git. + + Returns: + One of "created", "updated", "merged", or "skipped". + """ + source_content = source_hook.read_text() + managed_block = _managed_hook_block(source_content) + destination_hook.parent.mkdir(parents=True, exist_ok=True) + + if not destination_hook.exists(): + destination_hook.write_text(f"#!/bin/sh\n{managed_block}") + _make_executable(destination_hook) + return "created" + + existing_content = destination_hook.read_text() + if MANAGED_HOOK_START in existing_content and MANAGED_HOOK_END in existing_content: + updated_content = _replace_managed_block(existing_content, managed_block) + status = "updated" if updated_content != existing_content else "skipped" + destination_hook.write_text(updated_content) + _make_executable(destination_hook) + return status + + if not _is_shell_hook(existing_content): + companion = destination_hook.with_name(f"{destination_hook.name}.article-cli") + companion.write_text(f"#!/bin/sh\n{managed_block}") + _make_executable(companion) + print_warning( + f"Existing non-shell hook preserved: {destination_hook.name}; " + f"created managed companion hook: {companion.name}" + ) + return "skipped" + + separator = "\n\n" if existing_content.strip() else "" + destination_hook.write_text( + f"{existing_content.rstrip()}{separator}{managed_block}" + ) + _make_executable(destination_hook) + return "merged" + + +def gitinfo2_metadata_summary(start_path: Path) -> Optional[str]: + """ + Return a concise summary of the local gitinfo2 metadata, if available. + + Args: + start_path: Repository root or a path inside the repository + + Returns: + Human-readable summary, or None when gitHeadLocal.gin is unavailable. + """ + repo_path = _find_git_root(start_path) or start_path.resolve() + metadata_path = repo_path / "gitHeadLocal.gin" + if not metadata_path.exists(): + return None + + content = metadata_path.read_text() + parts = [] + for key, label in [ + ("shash", "commit"), + ("firsttagdescribe", "version"), + ("reltag", "release"), + ]: + value = _extract_gitinfo2_option(content, key) + if value: + parts.append(f"{label} {value}") + + return "; ".join(parts) if parts else None + + +def refresh_gitinfo2_metadata(start_path: Path) -> bool: + """ + Refresh gitinfo2 metadata and the local copy used by \\usepackage[local]{gitinfo2}. + + Args: + start_path: Repository root or a path inside the repository + + Returns: + True when gitHeadLocal.gin was refreshed, False otherwise + """ + repo_path = _find_git_root(start_path) or start_path.resolve() + git_head_info = _git_path(repo_path, "gitHeadInfo.gin") + if git_head_info is None: + git_head_info = _legacy_git_file(repo_path, "gitHeadInfo.gin") + if git_head_info is None: + return False + + metadata = _render_gitinfo2_metadata(repo_path) + if metadata is not None: + git_head_info.parent.mkdir(parents=True, exist_ok=True) + git_head_info.write_text(metadata) + shutil.copyfile(git_head_info, repo_path / "gitHeadLocal.gin") + return True + + hook_path = _find_gitinfo2_hook(repo_path) + if hook_path is None: + return False + + try: + subprocess.run( + ["sh", str(hook_path)], + check=True, + cwd=repo_path, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + print_warning(f"Failed to refresh gitinfo2 metadata: {e}") + return False + + git_head_info = _git_path(repo_path, "gitHeadInfo.gin") + if git_head_info is None: + git_head_info = _legacy_git_file(repo_path, "gitHeadInfo.gin") + if git_head_info is None: + return False + if not git_head_info.exists(): + return False + + shutil.copyfile(git_head_info, repo_path / "gitHeadLocal.gin") + return True + + +def render_gitinfo2_metadata(repo_path: Path) -> Optional[str]: + """Render gitinfo2 metadata without writing any files.""" + return _render_gitinfo2_metadata(repo_path) + + +def _render_gitinfo2_metadata(repo_path: Path) -> Optional[str]: + """Render gitinfo2 metadata directly from git.""" + dirty_suffix = "-*" if _is_dirty_for_gitinfo2(repo_path) else "" + firsttag = _git_output(repo_path, ["describe", "--tags", "--always"]) + reltag = _git_output( + repo_path, + [ + "describe", + "--tags", + "--long", + "--always", + "--match", + "v[0-9]*", + ], + ) + if firsttag is None or reltag is None: + return None + + firsttag = f"{firsttag}{dirty_suffix}" + reltag = f"{reltag}{dirty_suffix}" + pretty_format = ( + "\\usepackage[%\n" + " shash={%h},\n" + " lhash={%H},\n" + " authname={%an},\n" + " authemail={%ae},\n" + " authsdate={%ad},\n" + " authidate={%ai},\n" + " authudate={%at},\n" + " commname={%cn},\n" + " commemail={%ce},\n" + " commsdate={%cd},\n" + " commidate={%ci},\n" + " commudate={%ct},\n" + " refnames={%d},\n" + f" firsttagdescribe={{{firsttag}}},\n" + f" reltag={{{reltag}}}\n" + " ]{gitexinfo}" + ) + return _git_output( + repo_path, + [ + "--no-pager", + "log", + "-1", + "--date=short", + "--decorate=short", + f"--pretty=format:{pretty_format}", + "HEAD", + ], + ) + + +def _is_dirty_for_gitinfo2(repo_path: Path) -> bool: + """Return True when tracked files other than gitHeadLocal.gin are dirty.""" + result = _run_git( + repo_path, + [ + "status", + "--porcelain", + "--untracked-files=no", + "--", + ".", + ":(exclude)gitHeadLocal.gin", + ], + ) + return bool(result and result.stdout.strip()) + + +def _find_git_root(start_path: Path) -> Optional[Path]: + """Find the repository root with git, if available.""" + cwd = start_path if start_path.is_dir() else start_path.parent + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + check=False, + cwd=cwd, + capture_output=True, + text=True, + ) + except (FileNotFoundError, OSError): + return None + + if result.returncode != 0: + return None + + root = result.stdout.strip() + return Path(root).resolve() if root else None + + +def _git_path(repo_path: Path, git_path: str) -> Optional[Path]: + """Resolve a path inside Git's metadata directory.""" + result = _run_git(repo_path, ["rev-parse", "--git-path", git_path]) + if result is None or result.returncode != 0: + return None + + path_text = result.stdout.strip() + if not path_text: + return None + path = Path(path_text) + if not path.is_absolute(): + path = repo_path / path + return path.resolve() + + +def _legacy_git_file(repo_path: Path, name: str) -> Optional[Path]: + """Return .git/name for test fixtures and non-worktree repositories.""" + git_dir = repo_path / ".git" + if git_dir.is_dir(): + return git_dir / name + return None + + +def _git_output(repo_path: Path, args: List[str]) -> Optional[str]: + """Run git and return stdout without the trailing newline.""" + result = _run_git(repo_path, args) + if result is None or result.returncode != 0: + return None + return result.stdout.rstrip("\n") + + +def _run_git( + repo_path: Path, args: List[str] +) -> Optional[subprocess.CompletedProcess[str]]: + """Run git and return the completed process.""" + try: + return subprocess.run( + ["git", *args], + check=False, + cwd=repo_path, + capture_output=True, + text=True, + ) + except (FileNotFoundError, OSError): + return None + + +def _find_gitinfo2_hook(repo_path: Path) -> Optional[Path]: + """Find the gitinfo2 post-commit hook generated or installed by article-cli.""" + git_hook_path = _git_path(repo_path, "hooks/post-commit") + for hook_path in [ + repo_path / "hooks" / "post-commit", + git_hook_path, + _legacy_git_file(repo_path, "hooks/post-commit"), + ]: + if hook_path and hook_path.exists(): + return hook_path + return None + + +def _managed_hook_block(source_content: str) -> str: + """Build the managed hook block without a nested shebang.""" + source_without_shebang = _strip_shebang(source_content).rstrip() + return f"{MANAGED_HOOK_START}\n{source_without_shebang}\n{MANAGED_HOOK_END}\n" + + +def _replace_managed_block(content: str, replacement_block: str) -> str: + """Replace an existing managed hook block.""" + start = content.index(MANAGED_HOOK_START) + end = content.index(MANAGED_HOOK_END, start) + len(MANAGED_HOOK_END) + return f"{content[:start]}{replacement_block.rstrip()}{content[end:]}" + + +def _strip_shebang(content: str) -> str: + """Remove the first shebang line from hook content.""" + if not content.startswith("#!"): + return content + _, separator, rest = content.partition("\n") + return rest if separator else "" + + +def _is_shell_hook(content: str) -> bool: + """Return True if the existing hook can safely receive shell code.""" + first_line = content.splitlines()[0] if content.splitlines() else "" + if not first_line.startswith("#!"): + return True + return any(shell in first_line for shell in ["sh", "bash", "dash", "zsh"]) + + +def _make_executable(path: Path) -> None: + """Ensure all executable bits are present on a hook.""" + path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + +def _extract_gitinfo2_option(content: str, key: str) -> Optional[str]: + """Extract a gitinfo2 option value from gitHeadLocal.gin content.""" + marker = f"{key}={{" + start = content.find(marker) + if start == -1: + return None + start += len(marker) + end = content.find("}", start) + if end == -1: + return None + return content[start:end] diff --git a/src/article_cli/git_manager.py b/src/article_cli/git_manager.py index f689f7f..863346c 100644 --- a/src/article_cli/git_manager.py +++ b/src/article_cli/git_manager.py @@ -10,7 +10,13 @@ from typing import List, Optional import shutil -from .zotero import print_success, print_error, print_warning, print_info, Colors +from .git_hooks import ( + ensure_gitinfo2_hook_source, + gitinfo2_metadata_summary, + install_managed_gitinfo2_hook, + refresh_gitinfo2_metadata, +) +from .reporting import Colors, print_error, print_info, print_success, print_warning class GitManager: @@ -23,72 +29,90 @@ def __init__(self, repo_root: Optional[Path] = None): Args: repo_root: Repository root directory (defaults to current directory) """ - self.repo_root = repo_root or Path.cwd() - self._validate_git_repo() + self.repo_root = self._resolve_git_root(repo_root or Path.cwd()) - def _validate_git_repo(self) -> None: - """Validate that the current directory is a git repository""" - git_dir = self.repo_root / ".git" - if not git_dir.exists(): - raise ValueError(f"Not a git repository: {self.repo_root}") - - def setup_hooks(self) -> bool: + def _resolve_git_root(self, start_path: Path) -> Path: + """Resolve the Git repository root, including linked worktrees.""" + cwd = start_path if start_path.is_dir() else start_path.parent + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + cwd=cwd, + capture_output=True, + text=True, + check=False, + ) + except (FileNotFoundError, OSError) as e: + raise ValueError("Git is required but was not found") from e + + if result.returncode != 0: + details = result.stderr.strip() or str(start_path) + raise ValueError(f"Not a git repository: {details}") + + return Path(result.stdout.strip()).resolve() + + def _git_path(self, path: str) -> Path: + """Resolve a Git metadata path using git rev-parse --git-path.""" + result = subprocess.run( + ["git", "rev-parse", "--git-path", path], + cwd=self.repo_root, + capture_output=True, + text=True, + check=True, + ) + git_path = Path(result.stdout.strip()) + if not git_path.is_absolute(): + git_path = self.repo_root / git_path + return git_path.resolve() + + def setup_hooks(self, dry_run: bool = False) -> bool: """Setup git hooks for gitinfo2""" try: - hooks_dir = self.repo_root / "hooks" - git_hooks_dir = self.repo_root / ".git" / "hooks" - - if not hooks_dir.exists(): - print_error(f"Hooks directory not found: {hooks_dir}") - return False - - if not git_hooks_dir.exists(): - print_error("Git hooks directory missing") - return False + git_hooks_dir = self._git_path("hooks") + post_commit_src = self.repo_root / "hooks" / "post-commit" + + if dry_run: + print_info("Dry run: no setup files were changed.") + print_info(f"Would ensure repository hook source: {post_commit_src}") + print_info(f"Would ensure Git hooks directory: {git_hooks_dir}") + for hook_name in ["post-commit", "post-checkout", "post-merge"]: + print_info(f"Would install or update managed hook: {hook_name}") + print_info( + "Would refresh gitHeadLocal.gin if git metadata is available." + ) + return True - post_commit_src = hooks_dir / "post-commit" - if not post_commit_src.exists(): - print_error(f"Source hook not found: {post_commit_src}") - return False + git_hooks_dir.mkdir(parents=True, exist_ok=True) + post_commit_src = ensure_gitinfo2_hook_source(self.repo_root) - # Copy and make executable for hook_name in ["post-commit", "post-checkout", "post-merge"]: dest = git_hooks_dir / hook_name - - with open(post_commit_src, "r") as src: - with open(dest, "w") as dst: - dst.write(src.read()) - - dest.chmod(0o755) - print_success(f"Installed hook: {hook_name}") - - # Run git checkout to trigger hooks - subprocess.run(["git", "checkout"], check=True, cwd=self.repo_root) - - # Check for gitHeadInfo.gin - git_head_info = self.repo_root / ".git" / "gitHeadInfo.gin" - if not git_head_info.exists(): - print_warning("gitHeadInfo.gin not found, skipping local copy") - else: - local_copy = self.repo_root / "gitHeadLocal.gin" - with open(git_head_info, "r") as src: - with open(local_copy, "w") as dst: - dst.write(src.read()) - - subprocess.run( - ["git", "add", "gitHeadLocal.gin"], check=True, cwd=self.repo_root + status = install_managed_gitinfo2_hook(post_commit_src, dest) + if status == "created": + print_success(f"Installed hook: {hook_name}") + elif status == "updated": + print_success(f"Updated managed hook: {hook_name}") + elif status == "merged": + print_success( + f"Preserved existing hook and added managed block: {hook_name}" + ) + else: + print_info(f"Hook already current: {hook_name}") + + if refresh_gitinfo2_metadata(self.repo_root): + print_success("Refreshed gitHeadLocal.gin") + summary = gitinfo2_metadata_summary(self.repo_root) + if summary: + print_info(f"Version metadata: {summary}") + print_info( + "gitHeadLocal.gin was not committed; commit it explicitly if your " + "paper policy tracks generated metadata." ) - subprocess.run( - [ - "git", - "commit", - "-m", - "Created gitHeadLocal.gin for initial setup", - ], - check=True, - cwd=self.repo_root, + else: + print_warning( + "Could not refresh gitHeadLocal.gin yet. This is expected before " + "the first commit." ) - print_success("Created and committed gitHeadLocal.gin") return True @@ -99,91 +123,112 @@ def setup_hooks(self) -> bool: print_error(f"Failed to setup hooks: {e}") return False - def create_release(self, version: str, auto_push: bool = False) -> bool: + def refresh_version_metadata(self, dry_run: bool = False) -> bool: + """ + Refresh and report gitinfo2 version metadata without creating tags. + + Args: + dry_run: Report planned actions without writing gitHeadLocal.gin + + Returns: + True if the current git state could be reported, False otherwise + """ + commit = self._git_output(["rev-parse", "--short", "HEAD"]) or "unknown" + describe = self._git_output( + ["describe", "--tags", "--long", "--always", "--dirty=-*"] + ) + branch = self._git_output(["branch", "--show-current"]) or "detached" + + print_info(f"Git branch: {branch}") + print_info(f"Git commit: {commit}") + if describe: + print_info(f"Git describe: {describe}") + + if dry_run: + print_info("Dry run: no version metadata files were changed.") + print_info("Would refresh gitHeadLocal.gin from gitinfo2 metadata.") + return True + + if refresh_gitinfo2_metadata(self.repo_root): + print_success("Refreshed gitHeadLocal.gin") + summary = gitinfo2_metadata_summary(self.repo_root) + if summary: + print_info(f"Version metadata: {summary}") + else: + print_warning( + "Could not refresh gitHeadLocal.gin. Run article-cli setup after " + "the first commit if gitinfo2 hooks are missing." + ) + + return True + + def create_release( + self, version: str, auto_push: bool = False, dry_run: bool = False + ) -> bool: """ - Create a new release with the given version + Create a new release with the given version. + + This legacy entrypoint is intentionally conservative. The full checked + paper release workflow lives in ReleaseService and is used by the CLI. Args: - version: Version string (e.g., 'v1.0.0') + version: Version string (e.g., 'v1' or 'v1.0.0') auto_push: Whether to automatically push the release + dry_run: Validate and report actions without creating a release Returns: True if successful, False otherwise """ - # Validate version format - if not re.match(r"^v\d+\.\d+\.\d+(-[a-z]+\.\d+)?$", version): + if not re.match( + r"^v\d+(?:\.\d+){0,2}(?:[-._]?(?:alpha|beta|rc|pre|preview)\.?\d*)?$", + version, + ): print_error(f"Invalid version format: {version}") - print_info("Expected format: vX.Y.Z or vX.Y.Z-pre.N") + print_info("Expected examples: v1, v1.0, v1.0.0, v1.0.0-rc.1") return False try: - # Check if tag exists - result = subprocess.run( - ["git", "rev-parse", version], capture_output=True, cwd=self.repo_root - ) - if result.returncode == 0: + if self.tag_exists(version): print_error(f"Tag {version} already exists") return False - # Create tag - subprocess.run( - ["git", "tag", "-a", version, "-m", f"Release {version}"], - check=True, - cwd=self.repo_root, - ) + dirty_files = self.dirty_files(ignore_gitinfo=True) + if dirty_files: + print_error("Tracked or untracked files are dirty.") + for dirty_file in dirty_files: + print_info(f" {dirty_file}") + return False + + if dry_run: + print_info("Dry run: no tags, commits, or metadata files were changed.") + print_info(f"Would create annotated tag: {version}") + print_info("Would refresh gitHeadLocal.gin before and after tagging.") + if auto_push: + print_info(f"Would push with: git push origin {version}") + else: + print_info("Would leave push to the user.") + return True + + if not refresh_gitinfo2_metadata(self.repo_root): + print_warning("Could not refresh gitHeadLocal.gin before tagging.") + + self.create_tag(version) print_success(f"Created tag: {version}") - # Trigger hooks - subprocess.run(["git", "checkout"], check=True, cwd=self.repo_root) - - # Copy gitHeadInfo - git_head_info = self.repo_root / ".git" / "gitHeadInfo.gin" - if git_head_info.exists(): - local_copy = self.repo_root / "gitHeadLocal.gin" - with open(git_head_info, "r") as src: - content = src.read() - with open(local_copy, "w") as dst: - dst.write(content) - - # Show reltag - for line in content.split("\n"): - if "reltag" in line: - print_info(f"Release tag: {line}") - - subprocess.run( - ["git", "add", "gitHeadLocal.gin"], check=True, cwd=self.repo_root - ) - subprocess.run( - [ - "git", - "commit", - "-m", - f"Updated gitHeadLocal.gin for release {version}", - ], - check=True, - cwd=self.repo_root, - ) - subprocess.run( - ["git", "tag", "-f", "-a", version, "-m", f"Release {version}"], - check=True, - cwd=self.repo_root, - ) + if refresh_gitinfo2_metadata(self.repo_root): + summary = gitinfo2_metadata_summary(self.repo_root) + if summary: + print_info(f"Version metadata: {summary}") + else: + print_warning("Could not refresh gitHeadLocal.gin after tagging.") print_success(f"Release {version} created successfully") if auto_push: - try: - subprocess.run( - ["git", "push", "origin", "--follow-tags"], - check=True, - cwd=self.repo_root, - ) - print_success("Release pushed to remote") - except subprocess.CalledProcessError as e: - print_warning(f"Failed to push release: {e}") - print_info("Push manually with: git push origin --follow-tags") + self.push_tag(version) + print_success("Release pushed to remote") else: - print_info("Push with: git push origin --follow-tags") + print_info(f"Push with: git push origin {version}") return True @@ -194,6 +239,82 @@ def create_release(self, version: str, auto_push: bool = False) -> bool: print_error(f"Failed to create release: {e}") return False + def _git_output(self, args: List[str]) -> Optional[str]: + """Run git and return stdout without the trailing newline.""" + try: + result = subprocess.run( + ["git", *args], + capture_output=True, + text=True, + check=False, + cwd=self.repo_root, + ) + except (FileNotFoundError, OSError): + return None + if result.returncode != 0: + return None + return result.stdout.strip() + + def git( + self, + args: List[str], + check: bool = False, + capture_output: bool = True, + text: bool = True, + ) -> subprocess.CompletedProcess: + """Run a git command in the managed repository.""" + return subprocess.run( + ["git", *args], + capture_output=capture_output, + text=text, + check=check, + cwd=self.repo_root, + ) + + def tag_exists(self, tag: str) -> bool: + """Return whether a tag already exists.""" + result = self.git(["rev-parse", "--verify", f"refs/tags/{tag}"]) + return result.returncode == 0 + + def create_tag(self, tag: str, force: bool = False) -> bool: + """Create or update an annotated tag.""" + args = ["tag"] + if force: + args.append("-f") + args.extend(["-a", tag, "-m", f"Release {tag}"]) + self.git(args, check=True, capture_output=False, text=True) + return True + + def delete_tag(self, tag: str) -> bool: + """Delete a local tag.""" + self.git(["tag", "-d", tag], check=True, capture_output=True, text=True) + return True + + def dirty_files(self, ignore_gitinfo: bool = True) -> List[str]: + """Return dirty tracked/untracked files, optionally ignoring gitHeadLocal.gin.""" + pathspec = ["--", "."] + if ignore_gitinfo: + pathspec.append(":(exclude)gitHeadLocal.gin") + result = self.git(["status", "--porcelain", *pathspec]) + if result.returncode != 0: + return [""] + return [line.strip() for line in result.stdout.splitlines() if line.strip()] + + def commit_paths(self, paths: List[str], message: str) -> bool: + """Commit selected paths.""" + self.git(["add", *paths], check=True, capture_output=False, text=True) + result = self.git(["diff", "--cached", "--quiet"]) + if result.returncode == 0: + print_info("No staged metadata changes to commit.") + return True + self.git(["commit", "-m", message], check=True, capture_output=False, text=True) + return True + + def push_tag(self, tag: str) -> bool: + """Push a tag to origin.""" + self.git(["push", "origin", tag], check=True, capture_output=False, text=True) + return True + def list_releases(self, count: int = 5) -> bool: """ List recent releases diff --git a/src/article_cli/latex_compiler.py b/src/article_cli/latex_compiler.py index 820d342..14915e3 100644 --- a/src/article_cli/latex_compiler.py +++ b/src/article_cli/latex_compiler.py @@ -5,27 +5,41 @@ with support for latexmk and pdflatex engines. """ +import re +import shutil import subprocess import time from pathlib import Path from typing import Dict, List, Optional +from .command_runner import DEFAULT_RUNNER, CommandRunner from .config import Config -from .zotero import print_error, print_info, print_success +from .git_hooks import gitinfo2_metadata_summary, refresh_gitinfo2_metadata +from .reporting import print_error, print_info, print_success, print_warning + +ENGINE_RUNNERS = { + "latexmk": "_run_latexmk", + "pdflatex": "_run_pdflatex", + "xelatex": "_run_xelatex", + "lualatex": "_run_lualatex", +} class LaTeXCompiler: """Handles LaTeX document compilation with various engines and options""" - def __init__(self, config: Config): + def __init__(self, config: Config, runner: CommandRunner = DEFAULT_RUNNER): """ Initialize LaTeX compiler Args: config: Configuration instance + runner: Command execution boundary """ self.config = config + self.runner = runner self.latex_config = config.get_latex_config() + self.timeout = int(self.latex_config.get("timeout", 300)) def compile( self, @@ -54,6 +68,11 @@ def compile( return False print_info(f"Compiling {tex_file} with {engine}...") + if refresh_gitinfo2_metadata(tex_path.parent): + print_info("Updated gitinfo2 metadata") + summary = gitinfo2_metadata_summary(tex_path.parent) + if summary: + print_info(f"Version metadata: {summary}") if watch: return self._compile_watch(tex_path, engine, shell_escape, output_dir) @@ -64,17 +83,11 @@ def _compile_once( self, tex_path: Path, engine: str, shell_escape: bool, output_dir: Optional[str] ) -> bool: """Compile document once""" - if engine == "latexmk": - return self._run_latexmk(tex_path, shell_escape, output_dir) - elif engine == "pdflatex": - return self._run_pdflatex(tex_path, shell_escape, output_dir) - elif engine == "xelatex": - return self._run_xelatex(tex_path, shell_escape, output_dir) - elif engine == "lualatex": - return self._run_lualatex(tex_path, shell_escape, output_dir) - else: + runner_name = ENGINE_RUNNERS.get(engine) + if runner_name is None: print_error(f"Unknown engine: {engine}") return False + return bool(getattr(self, runner_name)(tex_path, shell_escape, output_dir)) def _compile_watch( self, tex_path: Path, engine: str, shell_escape: bool, output_dir: Optional[str] @@ -96,26 +109,14 @@ def _compile_watch( tex_path, shell_escape, output_dir, continuous=True ) - process = subprocess.Popen( + process = self.runner.popen( cmd, cwd=tex_path.parent, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True, - bufsize=1, ) # Stream output in real time try: - while True: - if process.stdout is None: - break - output = process.stdout.readline() - if output == "" and process.poll() is not None: - break - if output: - print(output.strip()) - + self.runner.stream_lines(process) except KeyboardInterrupt: print_info("\nStopping watch mode...") process.terminate() @@ -136,41 +137,25 @@ def _run_latexmk( try: print_info(f"Running: {' '.join(cmd)}") - result = subprocess.run( + result = self.runner.run( cmd, cwd=tex_path.parent, - capture_output=True, - text=True, - timeout=300, # 5 minute timeout + timeout=self.timeout, ) if result.returncode == 0: - pdf_name = tex_path.with_suffix(".pdf").name - if output_dir: - pdf_path = Path(output_dir) / pdf_name - else: - pdf_path = tex_path.with_suffix(".pdf") - - if pdf_path.exists(): - print_success(f"✅ Compilation successful: {pdf_path}") - self._show_pdf_info(pdf_path) - else: - print_error("Compilation reported success but PDF not found") - return False - - return True + return self._verify_pdf(tex_path, output_dir) else: print_error("❌ Compilation failed") - if result.stdout: - print("STDOUT:") - print(result.stdout) - if result.stderr: - print("STDERR:") - print(result.stderr) + self._print_process_output(result) return False except subprocess.TimeoutExpired: - print_error("Compilation timed out after 5 minutes") + print_error(f"Compilation timed out after {self.timeout} seconds") + return False + except FileNotFoundError: + print_error(f"Required build tool not found: {cmd[0]}") + print_info("Run article-cli doctor for full toolchain diagnostics.") return False except Exception as e: print_error(f"Compilation error: {e}") @@ -181,55 +166,7 @@ def _run_pdflatex( ) -> bool: """Run pdflatex compilation (multiple passes for cross-references)""" cmd = self._build_pdflatex_command(tex_path, shell_escape, output_dir) - - try: - # Run multiple passes for cross-references, bibliography, etc. - passes = ["First pass", "Second pass", "Third pass"] - - for i, pass_name in enumerate(passes): - print_info(f"{pass_name}...") - result = subprocess.run( - cmd, - cwd=tex_path.parent, - capture_output=True, - text=True, - timeout=120, # 2 minute timeout per pass - ) - - if result.returncode != 0: - print_error(f"❌ {pass_name} failed") - if result.stdout: - print("STDOUT:") - print(result.stdout) - if result.stderr: - print("STDERR:") - print(result.stderr) - return False - - # Check if we need to run bibtex/biber - if i == 0: # After first pass - self._run_bibliography_if_needed(tex_path, result.stdout) - - pdf_name = tex_path.with_suffix(".pdf").name - if output_dir: - pdf_path = Path(output_dir) / pdf_name - else: - pdf_path = tex_path.with_suffix(".pdf") - - if pdf_path.exists(): - print_success(f"✅ Compilation successful: {pdf_path}") - self._show_pdf_info(pdf_path) - return True - else: - print_error("Compilation reported success but PDF not found") - return False - - except subprocess.TimeoutExpired: - print_error("Compilation timed out") - return False - except Exception as e: - print_error(f"Compilation error: {e}") - return False + return self._run_engine_passes(tex_path, cmd, output_dir) def _build_latexmk_command( self, @@ -309,55 +246,7 @@ def _run_xelatex( ) -> bool: """Run xelatex compilation (multiple passes for cross-references)""" cmd = self._build_xelatex_command(tex_path, shell_escape, output_dir) - - try: - # Run multiple passes for cross-references, bibliography, etc. - passes = ["First pass", "Second pass", "Third pass"] - - for i, pass_name in enumerate(passes): - print_info(f"{pass_name} (xelatex)...") - result = subprocess.run( - cmd, - cwd=tex_path.parent, - capture_output=True, - text=True, - timeout=120, # 2 minute timeout per pass - ) - - if result.returncode != 0: - print_error(f"❌ {pass_name} failed") - if result.stdout: - print("STDOUT:") - print(result.stdout) - if result.stderr: - print("STDERR:") - print(result.stderr) - return False - - # Check if we need to run bibtex/biber - if i == 0: # After first pass - self._run_bibliography_if_needed(tex_path, result.stdout) - - pdf_name = tex_path.with_suffix(".pdf").name - if output_dir: - pdf_path = Path(output_dir) / pdf_name - else: - pdf_path = tex_path.with_suffix(".pdf") - - if pdf_path.exists(): - print_success(f"✅ Compilation successful: {pdf_path}") - self._show_pdf_info(pdf_path) - return True - else: - print_error("Compilation reported success but PDF not found") - return False - - except subprocess.TimeoutExpired: - print_error("Compilation timed out") - return False - except Exception as e: - print_error(f"Compilation error: {e}") - return False + return self._run_engine_passes(tex_path, cmd, output_dir, label="xelatex") def _build_xelatex_command( self, tex_path: Path, shell_escape: bool, output_dir: Optional[str] @@ -388,55 +277,7 @@ def _run_lualatex( ) -> bool: """Run lualatex compilation (multiple passes for cross-references)""" cmd = self._build_lualatex_command(tex_path, shell_escape, output_dir) - - try: - # Run multiple passes for cross-references, bibliography, etc. - passes = ["First pass", "Second pass", "Third pass"] - - for i, pass_name in enumerate(passes): - print_info(f"{pass_name} (lualatex)...") - result = subprocess.run( - cmd, - cwd=tex_path.parent, - capture_output=True, - text=True, - timeout=120, # 2 minute timeout per pass - ) - - if result.returncode != 0: - print_error(f"❌ {pass_name} failed") - if result.stdout: - print("STDOUT:") - print(result.stdout) - if result.stderr: - print("STDERR:") - print(result.stderr) - return False - - # Check if we need to run bibtex/biber - if i == 0: # After first pass - self._run_bibliography_if_needed(tex_path, result.stdout) - - pdf_name = tex_path.with_suffix(".pdf").name - if output_dir: - pdf_path = Path(output_dir) / pdf_name - else: - pdf_path = tex_path.with_suffix(".pdf") - - if pdf_path.exists(): - print_success(f"✅ Compilation successful: {pdf_path}") - self._show_pdf_info(pdf_path) - return True - else: - print_error("Compilation reported success but PDF not found") - return False - - except subprocess.TimeoutExpired: - print_error("Compilation timed out") - return False - except Exception as e: - print_error(f"Compilation error: {e}") - return False + return self._run_engine_passes(tex_path, cmd, output_dir, label="lualatex") def _build_lualatex_command( self, tex_path: Path, shell_escape: bool, output_dir: Optional[str] @@ -478,11 +319,9 @@ def _run_bibliography_if_needed(self, tex_path: Path, latex_output: str) -> None if "\\bibdata" in aux_content: print_info("Running bibtex for bibliography...") try: - subprocess.run( + self.runner.run( ["bibtex", base_name], cwd=tex_path.parent, - capture_output=True, - text=True, timeout=60, ) except Exception as e: @@ -523,16 +362,141 @@ def check_dependencies(self) -> Dict[str, bool]: def _check_command(self, command: str) -> bool: """Check if a command is available in PATH""" try: - result = subprocess.run( + result = self.runner.run( [command, "--version"], - capture_output=True, - text=True, timeout=10, ) return result.returncode == 0 except (subprocess.TimeoutExpired, FileNotFoundError): return False + def _run_engine_passes( + self, + tex_path: Path, + cmd: List[str], + output_dir: Optional[str], + label: str = "", + ) -> bool: + """Run a LaTeX engine through the standard three-pass workflow.""" + try: + for pass_index, pass_name in enumerate( + ["First pass", "Second pass", "Third pass"] + ): + suffix = f" ({label})" if label else "" + print_info(f"{pass_name}{suffix}...") + result = self.runner.run( + cmd, + cwd=tex_path.parent, + timeout=self.timeout, + ) + + if result.returncode != 0: + print_error(f"❌ {pass_name} failed") + self._print_process_output(result) + return False + + if pass_index == 0: + self._run_bibliography_if_needed(tex_path, result.stdout) + + return self._verify_pdf(tex_path, output_dir) + + except subprocess.TimeoutExpired: + print_error("Compilation timed out") + return False + except FileNotFoundError: + print_error(f"Required build tool not found: {cmd[0]}") + print_info("Run article-cli doctor for full toolchain diagnostics.") + return False + except Exception as e: + print_error(f"Compilation error: {e}") + return False + + def _pdf_path(self, tex_path: Path, output_dir: Optional[str]) -> Path: + """Return the expected PDF path for a compiled document.""" + pdf_name = tex_path.with_suffix(".pdf").name + if output_dir: + return Path(output_dir) / pdf_name + return tex_path.with_suffix(".pdf") + + def _verify_pdf(self, tex_path: Path, output_dir: Optional[str]) -> bool: + """Verify that compilation produced the expected PDF.""" + pdf_path = self._pdf_path(tex_path, output_dir) + if pdf_path.exists(): + print_success(f"✅ Compilation successful: {pdf_path}") + self._show_pdf_info(pdf_path) + self._show_pdf_diagnostics(tex_path, pdf_path, output_dir) + return True + + print_error("Compilation reported success but PDF not found") + return False + + def _show_pdf_diagnostics( + self, tex_path: Path, pdf_path: Path, output_dir: Optional[str] + ) -> None: + """Print lightweight diagnostics for the generated PDF and LaTeX log.""" + self._show_pdf_page_count(pdf_path) + self._show_latex_log_diagnostics(tex_path, output_dir) + + def _show_pdf_page_count(self, pdf_path: Path) -> None: + """Print PDF page count when pdfinfo is available.""" + if shutil.which("pdfinfo") is None: + return + try: + result = self.runner.run(["pdfinfo", str(pdf_path)], timeout=10) + except Exception: + return + if result.returncode != 0: + return + match = re.search(r"^Pages:\s+(\d+)", result.stdout, re.MULTILINE) + if match: + print_info(f"PDF pages: {match.group(1)}") + + def _show_latex_log_diagnostics( + self, tex_path: Path, output_dir: Optional[str] + ) -> None: + """Print common LaTeX warning counts from the generated log.""" + log_path = self._log_path(tex_path, output_dir) + if not log_path.exists(): + return + + content = log_path.read_text(errors="replace") + undefined_citations = len( + re.findall(r"LaTeX Warning: Citation .* undefined", content) + ) + undefined_references = len( + re.findall( + r"LaTeX Warning: Reference .* undefined|There were undefined references", + content, + ) + ) + overfull_boxes = len(re.findall(r"Overfull \\hbox", content)) + + if undefined_citations: + print_warning(f"Undefined citations: {undefined_citations}") + if undefined_references: + print_warning(f"Undefined references: {undefined_references}") + if overfull_boxes: + print_warning(f"Overfull boxes: {overfull_boxes}") + if not (undefined_citations or undefined_references or overfull_boxes): + print_info("No undefined citations/references or overfull boxes reported.") + + def _log_path(self, tex_path: Path, output_dir: Optional[str]) -> Path: + """Return the expected LaTeX log path for a compiled document.""" + log_name = tex_path.with_suffix(".log").name + if output_dir: + return Path(output_dir) / log_name + return tex_path.with_suffix(".log") + + @staticmethod + def _print_process_output(result: subprocess.CompletedProcess) -> None: + """Print captured process output when a tool fails.""" + if result.stdout: + print("STDOUT:") + print(result.stdout) + if result.stderr: + print("STDERR:") + print(result.stderr) + def print_dependency_status(self) -> None: """Print status of LaTeX dependencies""" print_info("Checking LaTeX dependencies...") diff --git a/src/article_cli/project_context.py b/src/article_cli/project_context.py new file mode 100644 index 0000000..2320773 --- /dev/null +++ b/src/article_cli/project_context.py @@ -0,0 +1,225 @@ +""" +Shared project context resolution for article-cli commands and services. +""" + +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, Optional + +from .config import Config + +LATEX_ENGINES = {"latexmk", "pdflatex", "xelatex", "lualatex"} +TYPST_ENGINE = "typst" + + +@dataclass(frozen=True) +class ProjectContext: + """Resolved project state shared across commands.""" + + config: Config + cwd: Path + project_root: Path + project_type: str + document: Optional[Path] + engine: str + output_dir: Optional[Path] + shell_escape: bool + + @classmethod + def resolve( + cls, + config: Config, + cwd: Optional[Path] = None, + document: Optional[str] = None, + engine: Optional[str] = None, + output_dir: Optional[str] = None, + shell_escape: Optional[bool] = None, + ) -> "ProjectContext": + """Resolve document, engine, and output policy from CLI and config.""" + resolved_cwd = (cwd or Path.cwd()).resolve() + root = _resolve_project_root(resolved_cwd) + project_config = config.get_project_config() + latex_config = config.get_latex_config() + documents_config = config.get_documents_config() + + resolved_engine = engine or latex_config.get("engine") or "latexmk" + document_name = document or documents_config.get("main") or None + resolved_document = _resolve_document( + root, + document_name, + resolved_engine, + cwd=resolved_cwd, + prefer_cwd=document is not None, + ) + + if resolved_document and resolved_document.suffix == ".typ": + resolved_engine = TYPST_ENGINE + elif ( + resolved_document + and resolved_document.suffix == ".tex" + and (resolved_engine == TYPST_ENGINE) + ): + # Keep the invalid combination visible for validation. + resolved_engine = TYPST_ENGINE + + resolved_output = _resolve_output_dir( + config, + resolved_engine, + output_dir, + ) + resolved_shell_escape = ( + bool(shell_escape) + if shell_escape is not None + else bool(latex_config.get("shell_escape", False)) + ) + + return cls( + config=config, + cwd=resolved_cwd, + project_root=root, + project_type=str(project_config.get("project_type", "article")), + document=resolved_document, + engine=str(resolved_engine), + output_dir=resolved_output, + shell_escape=resolved_shell_escape, + ) + + @property + def is_typst(self) -> bool: + """Whether the resolved compilation engine is Typst.""" + return self.engine == TYPST_ENGINE + + @property + def document_name(self) -> Optional[str]: + """Return the document path as a string for compiler APIs.""" + if self.document is None: + return None + try: + return str(self.document.relative_to(self.cwd)) + except ValueError: + try: + return str(self.document.relative_to(self.project_root)) + except ValueError: + return str(self.document) + + @property + def output_dir_name(self) -> Optional[str]: + """Return the output directory as a string for compiler APIs.""" + return str(self.output_dir) if self.output_dir else None + + def expected_pdf_path(self) -> Optional[Path]: + """Return the expected PDF path for the resolved document.""" + if self.document is None: + return None + pdf_name = self.document.with_suffix(".pdf").name + if self.output_dir is not None: + return self.output_dir / pdf_name + return self.document.with_suffix(".pdf") + + def as_doctor_context(self) -> Dict[str, Any]: + """Return stable context fields for doctor JSON output.""" + return { + "project_type": self.project_type, + "main_document": str(self.document) if self.document else None, + "engine": self.engine, + "output_directory": str(self.output_dir) if self.output_dir else ".", + } + + +def _resolve_project_root(cwd: Path) -> Path: + """Resolve the git project root, falling back to the working directory.""" + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + cwd=cwd, + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0 and result.stdout.strip(): + return Path(result.stdout.strip()).resolve() + except (FileNotFoundError, OSError): + pass + return cwd.resolve() + + +def _resolve_document( + root: Path, + document: Optional[str], + engine: str, + cwd: Path, + prefer_cwd: bool = False, +) -> Optional[Path]: + """Resolve a configured, explicit, or auto-detected source document.""" + if document: + path = Path(document) + if path.is_absolute(): + return path + if prefer_cwd: + cwd_candidate = (cwd / path).resolve() + root_candidate = (root / path).resolve() + if cwd_candidate.exists() or not root_candidate.exists(): + return cwd_candidate + return root_candidate + return (root / path).resolve() + + if engine == TYPST_ENGINE: + return _auto_detect_typ_file(root) or _auto_detect_tex_file(root) + return _auto_detect_tex_file(root) or _auto_detect_typ_file(root) + + +def _auto_detect_tex_file(root: Path) -> Optional[Path]: + """Auto-detect the main LaTeX file in a project.""" + tex_files = sorted(root.glob("*.tex")) + if not tex_files: + return None + if len(tex_files) == 1: + return tex_files[0] + for pattern in ["main.tex", "article.tex", f"{root.name}.tex"]: + candidate = root / pattern + if candidate.exists(): + return candidate + return tex_files[0] + + +def _auto_detect_typ_file(root: Path) -> Optional[Path]: + """Auto-detect the main Typst file in a project.""" + typ_files = sorted(root.glob("*.typ")) + if not typ_files: + return None + if len(typ_files) == 1: + return typ_files[0] + for pattern in [ + "main.typ", + "article.typ", + "presentation.typ", + "presentation.template.typ", + f"{root.name}.typ", + ]: + candidate = root / pattern + if candidate.exists(): + return candidate + return typ_files[0] + + +def _resolve_output_dir( + config: Config, + engine: str, + output_dir: Optional[str], +) -> Optional[Path]: + """Resolve output directory from CLI and project configuration.""" + if output_dir is not None: + return Path(output_dir) + + workflow_config = config.get_workflow_config() + if engine == TYPST_ENGINE: + typst_config = config.get_typst_config() + configured = typst_config.get("build_dir") or workflow_config.get("output_dir") + else: + latex_config = config.get_latex_config() + configured = workflow_config.get("output_dir") or latex_config.get("build_dir") + + if configured and configured != ".": + return Path(str(configured)) + return None diff --git a/src/article_cli/reporting.py b/src/article_cli/reporting.py new file mode 100644 index 0000000..756dfdf --- /dev/null +++ b/src/article_cli/reporting.py @@ -0,0 +1,39 @@ +""" +Terminal reporting helpers for article-cli. +""" + +import sys + + +class Colors: + """ANSI color codes for terminal output.""" + + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKCYAN = "\033[96m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + + +def print_success(msg: str) -> None: + """Print success message in green.""" + print(f"{Colors.OKGREEN}✓ {msg}{Colors.ENDC}") + + +def print_error(msg: str) -> None: + """Print error message in red.""" + print(f"{Colors.FAIL}✗ Error: {msg}{Colors.ENDC}", file=sys.stderr) + + +def print_warning(msg: str) -> None: + """Print warning message in yellow.""" + print(f"{Colors.WARNING}⚠ Warning: {msg}{Colors.ENDC}") + + +def print_info(msg: str) -> None: + """Print info message in cyan.""" + print(f"{Colors.OKCYAN}ℹ {msg}{Colors.ENDC}") diff --git a/src/article_cli/repository_setup.py b/src/article_cli/repository_setup.py index 1a5fa3c..027094b 100644 --- a/src/article_cli/repository_setup.py +++ b/src/article_cli/repository_setup.py @@ -8,10 +8,39 @@ - Git hooks and configuration """ +import json +from dataclasses import dataclass from pathlib import Path -from typing import List, Optional +from typing import List, Optional, Sequence, Union + +from .git_hooks import ensure_gitinfo2_hook_source +from .template_renderer import TEMPLATE_VERSION, TemplateRenderer +from .reporting import print_error, print_info, print_success + +ARTICLE_CLI_MIN_VERSION = "1.5.0" +ARTICLE_TEMPLATES = { + ("article", "default"): "article/main.tex.j2", + ("article", "lncs"): "article/lncs.tex.j2", + ("article", "ieee"): "article/ieee.tex.j2", + ("typst-article", "default"): "article/main.typ.j2", + ("typst-article", "lncs"): "article/lncs.typ.j2", +} + + +@dataclass(frozen=True) +class TemplateValue: + """Pre-rendered scalar value for templates that need quoted syntax.""" + + name: str + -from .zotero import print_error, print_info, print_success +@dataclass(frozen=True) +class WorkflowDocument: + """Additional document metadata for generated workflows.""" + + name: str + base: str + pdf: str class RepositorySetup: @@ -25,6 +54,7 @@ def __init__(self, repo_path: Optional[Path] = None): repo_path: Path to repository (defaults to current directory) """ self.repo_path = repo_path or Path.cwd() + self.renderer = TemplateRenderer() def init_repository( self, @@ -40,6 +70,15 @@ def init_repository( output_dir: str = "", fonts_dir: str = "", install_fonts: bool = False, + style: str = "default", + template: str = "", + ci_bibliography: str = "off", + ci_runner_policy: str = "github", + ci_github_runner: str = "ubuntu-24.04", + ci_self_hosted_label: str = "self-texlive", + ci_self_hosted_org: str = "", + ci_release_policy: str = "github", + ci_artifact_includes: Optional[List[str]] = None, ) -> bool: """ Initialize a complete LaTeX repository (article, presentation, or poster) @@ -52,7 +91,7 @@ def init_repository( - .vscode/settings.json with LaTeX Workshop configuration - LTeX dictionary files for spell checking - hooks/post-commit for gitinfo2 integration - - main.tex if no .tex file exists (or presentation/poster template) + - main.tex or main.typ if no source file exists Args: title: Document title @@ -60,22 +99,40 @@ def init_repository( group_id: Zotero group ID force: Overwrite existing files if True main_tex_file: Main .tex filename (auto-detected if None, created if missing) - project_type: Type of project ("article", "presentation", "poster") + project_type: Type of project ("article", "typst-article", "presentation", "poster") theme: Beamer theme for presentations (e.g., "numpex", "metropolis") aspect_ratio: Aspect ratio for presentations ("169", "43", "1610") additional_documents: List of additional .tex files to compile (e.g., ["poster.tex"]) output_dir: Output directory for compiled files (e.g., "build") fonts_dir: Directory containing custom fonts for XeLaTeX install_fonts: Whether to install custom fonts in CI + style: Built-in document style for generated article templates + template: Local Jinja2 template path for custom article styles + ci_bibliography: CI bibliography policy (off, check, update, required) + ci_runner_policy: CI runner policy (github, self-hosted, self-hosted-auto) + ci_github_runner: GitHub-hosted runner label + ci_self_hosted_label: Self-hosted runner label + ci_self_hosted_org: Organization for opt-in self-hosted runner discovery + ci_release_policy: CI release policy (github or off) + ci_artifact_includes: Extra artifact path globs Returns: True if successful, False otherwise """ + ci_artifact_includes = ci_artifact_includes or [] print_info(f"Initializing {project_type} repository at: {self.repo_path}") # Detect or validate main .tex file, create if missing tex_file = self._detect_or_create_tex_file( - main_tex_file, title, authors, force, project_type, theme, aspect_ratio + main_tex_file, + title, + authors, + force, + project_type, + theme, + aspect_ratio, + style, + template, ) if not tex_file: print_error("Failed to detect or create main .tex file") @@ -95,6 +152,10 @@ def init_repository( print_info(f"Output directory: {output_dir}") if fonts_dir: print_info(f"Fonts directory: {fonts_dir}") + if style: + print_info(f"Document style: {style}") + if template: + print_info(f"Custom template: {template}") try: # Create directory structure @@ -102,14 +163,21 @@ def init_repository( # Create GitHub Actions workflows if not self._create_workflow( - project_name, - tex_file, - force, - project_type, - additional_documents, - output_dir, - fonts_dir, - install_fonts, + project_name=project_name, + tex_file=tex_file, + force=force, + project_type=project_type, + additional_documents=additional_documents, + output_dir=output_dir, + fonts_dir=fonts_dir, + install_fonts=install_fonts, + ci_bibliography=ci_bibliography, + ci_runner_policy=ci_runner_policy, + ci_github_runner=ci_github_runner, + ci_self_hosted_label=ci_self_hosted_label, + ci_self_hosted_org=ci_self_hosted_org, + ci_release_policy=ci_release_policy, + ci_artifact_includes=ci_artifact_includes, ): return False @@ -127,12 +195,31 @@ def init_repository( output_dir, fonts_dir, install_fonts, + style, + template, + tex_file, + ci_bibliography, + ci_runner_policy, + ci_github_runner, + ci_self_hosted_label, + ci_self_hosted_org, + ci_release_policy, + ci_artifact_includes, ): return False # Create README if not self._create_readme( - project_name, title, authors, tex_file, force, project_type + project_name, + title, + authors, + tex_file, + force, + project_type, + style, + ci_bibliography, + ci_runner_policy, + ci_release_policy, ): return False @@ -152,7 +239,7 @@ def init_repository( print_info(" 1. Review and edit pyproject.toml") print_info(" 2. Add ZOTERO_API_KEY secret to GitHub repository") print_info(" 3. Run: article-cli setup") - print_info(" 4. Run: article-cli update-bibtex") + print_info(" 4. Run: article-cli bib update") print_info(" 5. Commit and push changes") return True @@ -170,6 +257,8 @@ def _detect_or_create_tex_file( project_type: str = "article", theme: str = "", aspect_ratio: str = "169", + style: str = "default", + template: str = "", ) -> Optional[str]: """ Detect main .tex/.typ file in repository or create one if missing @@ -179,9 +268,11 @@ def _detect_or_create_tex_file( title: Document title (for creating new file) authors: List of author names (for creating new file) force: Overwrite existing file if True - project_type: Type of project ("article", "presentation", "poster", "typst-presentation") + project_type: Type of project ("article", "typst-article", "presentation", "poster", "typst-presentation") theme: Beamer/Typst theme for presentations aspect_ratio: Aspect ratio for presentations + style: Built-in document style for generated article templates + template: Local Jinja2 template path for custom article styles Returns: Main document filename or None on failure @@ -196,7 +287,15 @@ def _detect_or_create_tex_file( return specified # Specified file doesn't exist - create it if self._create_tex_file( - specified, title, authors, force, project_type, theme, aspect_ratio + specified, + title, + authors, + force, + project_type, + theme, + aspect_ratio, + style, + template, ): return specified return None @@ -214,7 +313,15 @@ def _detect_or_create_tex_file( default_name = f"main{file_ext}" print_info(f"No {file_ext} file found, creating {default_name}") if self._create_tex_file( - default_name, title, authors, force, project_type, theme, aspect_ratio + default_name, + title, + authors, + force, + project_type, + theme, + aspect_ratio, + style, + template, ): return default_name return None @@ -250,6 +357,8 @@ def _create_tex_file( project_type: str = "article", theme: str = "", aspect_ratio: str = "169", + style: str = "default", + template: str = "", ) -> bool: """ Create a LaTeX or Typst file based on project type @@ -259,9 +368,11 @@ def _create_tex_file( title: Document title authors: List of author names force: Overwrite if exists - project_type: Type of project ("article", "presentation", "poster", "typst-presentation") + project_type: Type of project ("article", "typst-article", "presentation", "poster", "typst-presentation") theme: Beamer/Typst theme for presentations aspect_ratio: Aspect ratio for presentations + style: Built-in document style for generated article templates + template: Local Jinja2 template path for custom article styles Returns: True if successful @@ -276,27 +387,28 @@ def _create_tex_file( is_typst = project_type.startswith("typst-") or filename.endswith(".typ") if is_typst: - # Format authors for Typst - authors_typst = ", ".join([f'"{author}"' for author in authors]) - if project_type in ( "typst-presentation", "presentation", ) and filename.endswith(".typ"): doc_content = self._get_typst_presentation_template( - title, authors_typst, theme + title, authors, theme ) elif project_type in ("typst-poster", "poster") and filename.endswith( ".typ" ): - doc_content = self._get_typst_poster_template(title, authors_typst) + doc_content = self._get_typst_poster_template(title, authors) + elif project_type == "typst-article" or filename.endswith(".typ"): + doc_content = self._get_typst_article_template( + title, authors, style, template + ) else: doc_content = self._get_typst_presentation_template( - title, authors_typst, theme + title, authors, theme ) else: # Format authors for LaTeX - authors_latex = " \\\\and ".join(authors) + authors_latex = self._coerce_latex_authors(authors) if project_type == "presentation": doc_content = self._get_presentation_template( @@ -305,432 +417,178 @@ def _create_tex_file( elif project_type == "poster": doc_content = self._get_poster_template(title, authors_latex) else: - doc_content = self._get_article_template(title, authors_latex) + doc_content = self._get_article_template( + title, authors_latex, style, template + ) - doc_path.write_text(doc_content) + doc_path.write_text(doc_content, encoding="utf-8") print_success(f"Created: {doc_path.relative_to(self.repo_path)}") return True - def _get_article_template(self, title: str, authors_latex: str) -> str: - """Get article template content""" - return f"""\\documentclass[a4paper,11pt]{{article}} - -% Essential packages -\\usepackage[utf8]{{inputenc}} -\\usepackage[T1]{{fontenc}} -\\usepackage{{lmodern}} -\\usepackage{{amsmath,amssymb,amsthm}} -\\usepackage{{graphicx}} -\\usepackage{{hyperref}} -\\usepackage[margin=1in]{{geometry}} - -% Bibliography -\\usepackage[style=numeric,sorting=none]{{biblatex}} -\\addbibresource{{references.bib}} - -% Git version information -\\usepackage{{gitinfo2}} - -% Title and authors -\\title{{{title}}} -\\author{{{authors_latex}}} -\\date{{\\today}} - -\\begin{{document}} - -\\maketitle - -\\begin{{abstract}} - Your abstract goes here. -\\end{{abstract}} - -\\section{{Introduction}} - -Your introduction goes here. - -\\section{{Methodology}} - -Your methodology goes here. - -\\section{{Results}} - -Your results go here. - -\\section{{Conclusion}} - -Your conclusion goes here. + @staticmethod + def _toml_string(value: str) -> str: + """Return a TOML-compatible quoted string.""" + return json.dumps(value) + + @staticmethod + def _coerce_latex_authors(authors: Union[str, Sequence[str]]) -> str: + """Normalize author input for LaTeX templates.""" + if isinstance(authors, str): + return authors + return " \\and ".join(authors) + + @staticmethod + def _coerce_author_list(authors: Union[str, Sequence[str]]) -> List[str]: + """Normalize author input for Typst templates.""" + if isinstance(authors, str): + authors = authors.replace("\\and", ",") + return [ + author.strip().strip('"') + for author in authors.split(",") + if author.strip() + ] + return list(authors) + + def _article_template_context( + self, title: str, authors: Union[str, Sequence[str]], style: str + ) -> dict: + """Build context shared by built-in and user article templates.""" + author_list = self._coerce_author_list(authors) + return { + "title": title, + "authors": author_list, + "authors_latex": self._coerce_latex_authors(authors), + "authors_display": ", ".join(author_list), + "authors_bibtex": " and ".join(author_list), + "style": style, + "template_version": TEMPLATE_VERSION, + } -% Print bibliography -\\printbibliography + def _render_article_style( + self, + project_type: str, + title: str, + authors: Union[str, Sequence[str]], + style: str, + template: str, + ) -> str: + """Render a built-in or user-supplied article template.""" + context = self._article_template_context(title, authors, style) + if template: + return self.renderer.render_path(Path(template), context) + + style_key = style or "default" + template_name = ARTICLE_TEMPLATES.get((project_type, style_key)) + if template_name is None: + supported = sorted( + name for kind, name in ARTICLE_TEMPLATES if kind == project_type + ) + raise ValueError( + f"Unsupported style '{style_key}' for {project_type}. " + f"Built-in styles: {', '.join(supported)}. " + "Use --template to provide a custom Jinja2 template." + ) + return self.renderer.render(template_name, context) + + @staticmethod + def _document_base(filename: str) -> str: + """Return a stable output base name for a source document.""" + return Path(filename).stem + + def _workflow_documents(self, documents: Sequence[str]) -> List[WorkflowDocument]: + """Build typed workflow metadata for additional LaTeX documents.""" + return [ + WorkflowDocument( + name=doc, + base=self._document_base(doc), + pdf=f"{self._document_base(doc)}.pdf", + ) + for doc in documents + ] -% Git information (optional - appears in footer) -\\vfill -\\hrule -\\small -\\noindent Git version: \\gitAbbrevHash{{}} (\\gitAuthorIsoDate) \\\\ -Branch: \\gitBranch + def _get_article_template( + self, + title: str, + authors_latex: Union[str, Sequence[str]], + style: str = "default", + template: str = "", + ) -> str: + """Get article template content.""" + return self._render_article_style( + "article", title, authors_latex, style, template + ) -\\end{{document}} -""" + def _get_typst_article_template( + self, + title: str, + authors_typst: Union[str, Sequence[str]], + style: str = "default", + template: str = "", + ) -> str: + """Get Typst article template content.""" + return self._render_article_style( + "typst-article", title, authors_typst, style, template + ) def _get_presentation_template( - self, title: str, authors_latex: str, theme: str, aspect_ratio: str + self, + title: str, + authors_latex: Union[str, Sequence[str]], + theme: str, + aspect_ratio: str, ) -> str: - """Get Beamer presentation template content""" + """Get Beamer presentation template content.""" theme_line = f"\\usetheme{{{theme}}}" if theme else "% \\usetheme{default}" + return self.renderer.render( + "presentation/beamer.tex.j2", + { + "title": title, + "authors_latex": self._coerce_latex_authors(authors_latex), + "theme_line": theme_line, + "aspect_ratio": aspect_ratio, + }, + ) - return f"""\\documentclass[aspectratio={aspect_ratio}]{{beamer}} - -% Theme configuration -{theme_line} - -% Essential packages -\\usepackage{{tikz}} -\\usepackage{{pgfplots}} -\\pgfplotsset{{compat=newest}} -\\usepackage{{booktabs}} -\\usepackage{{hyperref}} - -% Bibliography (optional) -% \\usepackage[style=numeric,sorting=none]{{biblatex}} -% \\addbibresource{{references.bib}} - -% Git version information -\\usepackage{{gitinfo2}} - -% Title and authors -\\title{{{title}}} -\\author{{{authors_latex}}} -\\date{{\\today}} -\\institute{{Your Institution}} - -\\begin{{document}} - -\\maketitle - -\\begin{{frame}}{{Outline}} - \\tableofcontents -\\end{{frame}} - -\\section{{Introduction}} - -\\begin{{frame}}{{Introduction}} - \\begin{{itemize}} - \\item First point - \\item Second point - \\item Third point - \\end{{itemize}} -\\end{{frame}} - -\\section{{Main Content}} - -\\begin{{frame}}{{Main Content}} - Your main content goes here. -\\end{{frame}} - -\\section{{Conclusion}} - -\\begin{{frame}}{{Conclusion}} - \\begin{{itemize}} - \\item Summary point 1 - \\item Summary point 2 - \\end{{itemize}} -\\end{{frame}} - -\\begin{{frame}}{{Questions?}} - \\centering - \\Large Thank you for your attention! - - \\vspace{{1cm}} - \\small - Git version: \\gitAbbrevHash{{}} (\\gitAuthorIsoDate) -\\end{{frame}} - -\\end{{document}} -""" - - def _get_poster_template(self, title: str, authors_latex: str) -> str: - """Get poster template content""" - return f"""\\documentclass[a0paper,portrait]{{tikzposter}} - -% Essential packages -\\usepackage{{amsmath,amssymb}} -\\usepackage{{graphicx}} -\\usepackage{{booktabs}} - -% Git version information -\\usepackage{{gitinfo2}} - -% Title and authors -\\title{{{title}}} -\\author{{{authors_latex}}} -\\institute{{Your Institution}} -\\date{{\\today}} - -% Theme -\\usetheme{{Default}} - -\\begin{{document}} - -\\maketitle - -\\begin{{columns}} - \\column{{0.5}} - - \\block{{Introduction}}{{ - Your introduction goes here. - }} - - \\block{{Methods}}{{ - Your methods description goes here. - }} - - \\column{{0.5}} - - \\block{{Results}}{{ - Your results go here. - }} - - \\block{{Conclusions}}{{ - Your conclusions go here. - }} - -\\end{{columns}} - -\\block{{References}}{{ - Your references go here. -}} - -\\note[targetoffsetx=0cm, targetoffsety=-8cm, width=0.4\\textwidth]{{ - Git version: \\gitAbbrevHash{{}} (\\gitAuthorIsoDate) -}} - -\\end{{document}} -""" + def _get_poster_template( + self, title: str, authors_latex: Union[str, Sequence[str]] + ) -> str: + """Get poster template content.""" + return self.renderer.render( + "poster/poster.tex.j2", + { + "title": title, + "authors_latex": self._coerce_latex_authors(authors_latex), + }, + ) def _get_typst_presentation_template( - self, title: str, authors_typst: str, theme: str + self, title: str, authors_typst: Union[str, Sequence[str]], theme: str ) -> str: - """Get Typst presentation template content""" - if theme: - theme_import = f'#import "{theme}.typ": *' - theme_show = f"#show: {theme}-theme.with(" - else: - theme_import = "// No theme specified - using basic Typst presentation" - theme_show = '#set page(paper: "presentation-16-9")\n#set text(size: 24pt)\n\n// Document metadata\n#let title = ' - - if theme: - return f"""{theme_import} - -{theme_show} - title: "{title}", - author: [{authors_typst}], - date: datetime.today().display("[month repr:long] [day], [year]"), - institution: "Your Institution", -) - -// Title slide is automatically generated by the theme - -#slide(title: "Outline")[ - #outline() -] - -= Introduction - -#slide(title: "Introduction")[ - - First point - - Second point - - Third point -] - -= Main Content - -#slide(title: "Main Content")[ - Your main content goes here. -] - -= Conclusion - -#slide(title: "Conclusion")[ - - Summary point 1 - - Summary point 2 -] - -#slide(title: "Questions?")[ - #align(center)[ - #text(size: 36pt)[Thank you for your attention!] - ] -] -""" - else: - return f"""// Typst Presentation -// Title: {title} -// Authors: {authors_typst} - -#set page(paper: "presentation-16-9", margin: 2cm) -#set text(font: "Helvetica Neue", size: 24pt) - -// Title slide -#page[ - #align(center + horizon)[ - #text(size: 48pt, weight: "bold")[{title}] - - #v(1cm) - - #text(size: 28pt)[{authors_typst.replace('"', '')}] - - #v(0.5cm) - - Your Institution - - #v(0.5cm) - - #datetime.today().display("[month repr:long] [day], [year]") - ] -] - -// Introduction -#page[ - #text(size: 36pt, weight: "bold")[Introduction] - - #v(1cm) - - - First point - - Second point - - Third point -] - -// Main Content -#page[ - #text(size: 36pt, weight: "bold")[Main Content] - - #v(1cm) - - Your main content goes here. -] - -// Conclusion -#page[ - #text(size: 36pt, weight: "bold")[Conclusion] - - #v(1cm) - - - Summary point 1 - - Summary point 2 -] - -// Questions -#page[ - #align(center + horizon)[ - #text(size: 48pt)[Questions?] - - #v(1cm) - - Thank you for your attention! - ] -] -""" - - def _get_typst_poster_template(self, title: str, authors_typst: str) -> str: - """Get Typst poster template content""" - return f"""// Typst Poster -// Title: {title} -// Authors: {authors_typst} - -#set page(paper: "a0", margin: 2cm) -#set text(font: "Helvetica Neue", size: 24pt) - -// Header -#align(center)[ - #text(size: 72pt, weight: "bold")[{title}] - - #v(1cm) - - #text(size: 36pt)[{authors_typst.replace('"', '')}] - - #text(size: 28pt)[Your Institution] -] - -#v(2cm) - -#columns(3, gutter: 2cm)[ - // Column 1 - #block( - fill: rgb("#f0f0f0"), - inset: 1cm, - radius: 10pt, - width: 100%, - )[ - #text(size: 36pt, weight: "bold")[Introduction] - - #v(0.5cm) - - Your introduction goes here. - ] - - #v(1cm) - - #block( - fill: rgb("#f0f0f0"), - inset: 1cm, - radius: 10pt, - width: 100%, - )[ - #text(size: 36pt, weight: "bold")[Methods] - - #v(0.5cm) - - Your methods description goes here. - ] - - #colbreak() - - // Column 2 - #block( - fill: rgb("#f0f0f0"), - inset: 1cm, - radius: 10pt, - width: 100%, - )[ - #text(size: 36pt, weight: "bold")[Results] - - #v(0.5cm) - - Your results go here. - ] - - #colbreak() - - // Column 3 - #block( - fill: rgb("#f0f0f0"), - inset: 1cm, - radius: 10pt, - width: 100%, - )[ - #text(size: 36pt, weight: "bold")[Conclusions] - - #v(0.5cm) - - Your conclusions go here. - ] - - #v(1cm) - - #block( - fill: rgb("#e8f4ea"), - inset: 1cm, - radius: 10pt, - width: 100%, - )[ - #text(size: 36pt, weight: "bold")[References] - - #v(0.5cm) + """Get Typst presentation template content.""" + authors = self._coerce_author_list(authors_typst) + return self.renderer.render( + "presentation/typst.typ.j2", + { + "title": title, + "authors": authors, + "authors_display": ", ".join(authors), + "theme": theme, + "has_theme": bool(theme), + }, + ) - Your references go here. - ] -] -""" + def _get_typst_poster_template( + self, title: str, authors_typst: Union[str, Sequence[str]] + ) -> str: + """Get Typst poster template content.""" + authors = self._coerce_author_list(authors_typst) + return self.renderer.render( + "poster/poster.typ.j2", + { + "title": title, + "authors_display": ", ".join(authors), + }, + ) def _create_directories(self) -> None: """Create necessary directory structure""" @@ -754,6 +612,16 @@ def _create_workflow( output_dir: str = "", fonts_dir: str = "", install_fonts: bool = False, + style: str = "default", + template: str = "", + main_document: str = "", + ci_bibliography: str = "off", + ci_runner_policy: str = "github", + ci_github_runner: str = "ubuntu-24.04", + ci_self_hosted_label: str = "self-texlive", + ci_self_hosted_org: str = "", + ci_release_policy: str = "github", + ci_artifact_includes: Optional[List[str]] = None, ) -> bool: """ Create GitHub Actions workflow file @@ -767,6 +635,13 @@ def _create_workflow( output_dir: Output directory for compiled files (e.g., "build") fonts_dir: Directory containing custom fonts install_fonts: Whether to install fonts in CI + ci_bibliography: CI bibliography policy + ci_runner_policy: CI runner policy + ci_github_runner: GitHub-hosted runner label + ci_self_hosted_label: Self-hosted runner label + ci_self_hosted_org: Organization for self-hosted auto-discovery + ci_release_policy: CI release policy + ci_artifact_includes: Extra artifact path globs Returns: True if successful @@ -779,585 +654,111 @@ def _create_workflow( ) return True - # Extract base name (without .tex extension) - tex_base = tex_file.replace(".tex", "") - workflow_content = self._get_workflow_template( project_name, - tex_base, + tex_file, project_type, additional_documents or [], output_dir, fonts_dir, install_fonts, + ci_bibliography, + ci_runner_policy, + ci_github_runner, + ci_self_hosted_label, + ci_self_hosted_org, + ci_release_policy, + ci_artifact_includes or [], ) - workflow_path.write_text(workflow_content) + workflow_path.write_text(workflow_content, encoding="utf-8") print_success(f"Created workflow: {workflow_path.relative_to(self.repo_path)}") return True def _get_workflow_template( self, project_name: str, - tex_base: str, + source_file: str, project_type: str = "article", additional_documents: Optional[List[str]] = None, output_dir: str = "", fonts_dir: str = "", install_fonts: bool = False, + ci_bibliography: str = "off", + ci_runner_policy: str = "github", + ci_github_runner: str = "ubuntu-24.04", + ci_self_hosted_label: str = "self-texlive", + ci_self_hosted_org: str = "", + ci_release_policy: str = "github", + ci_artifact_includes: Optional[List[str]] = None, ) -> str: """ - Get GitHub Actions workflow template + Get GitHub Actions workflow template. Args: project_name: Name of the project - tex_base: Base name of .tex file (without extension) + source_file: Main source document file project_type: Type of project ("article", "presentation", "poster") additional_documents: List of additional .tex files to compile output_dir: Output directory for compiled files fonts_dir: Directory containing custom fonts install_fonts: Whether to install fonts in CI + ci_bibliography: CI bibliography policy + ci_runner_policy: CI runner policy + ci_github_runner: GitHub-hosted runner label + ci_self_hosted_label: Self-hosted runner label + ci_self_hosted_org: Organization for self-hosted auto-discovery + ci_release_policy: CI release policy + ci_artifact_includes: Extra artifact path globs Returns: Workflow YAML content """ additional_documents = additional_documents or [] - - # Determine LaTeX engine based on project type + ci_artifact_includes = ci_artifact_includes or [] + workflow_documents = self._workflow_documents(additional_documents) + is_typst = project_type.startswith("typst-") or source_file.endswith(".typ") use_xelatex = project_type in ["presentation", "poster"] - latexmk_args = "-xelatex" if use_xelatex else "-pdf" - - # Build output directory arguments outdir_arg = f"-outdir={output_dir}" if output_dir else "" - pdf_location = f"{output_dir}/" if output_dir else "" - - # Build output directory echo line (avoid backslash issues in f-strings) - output_dir_echo = ( - f'echo "- **Output Directory**: `{output_dir}`" >> $GITHUB_STEP_SUMMARY' - if output_dir - else "" + source_base = self._document_base(source_file) + + return self.renderer.render( + "github/latex.yml.j2", + { + "project_name": project_name, + "project_type": project_type, + "source_base": source_base, + "source_file": source_file, + "tex_base": source_base, + "tex_file": source_file, + "is_typst": is_typst, + "additional_documents": workflow_documents, + "additional_documents_label": ( + ", ".join(additional_documents) if additional_documents else "none" + ), + "output_dir": output_dir, + "output_dir_label": output_dir if output_dir else "root", + "fonts_dir": fonts_dir, + "install_fonts": install_fonts, + "runner_policy": ci_runner_policy, + "github_runner": ci_github_runner, + "self_hosted_label": ci_self_hosted_label, + "self_hosted_org": ci_self_hosted_org, + "bibliography_policy": ci_bibliography, + "release_policy": ci_release_policy, + "artifact_includes": ci_artifact_includes, + "latex_engine_label": ( + "Typst" if is_typst else "XeLaTeX" if use_xelatex else "pdfLaTeX" + ), + "latexmk_args": "-xelatex" if use_xelatex else "-pdf", + "latexmk_use_xelatex": "true" if use_xelatex else "false", + "outdir_arg": outdir_arg, + "pdf_location": f"{output_dir}/" if output_dir else "", + "article_cli_min_version": ARTICLE_CLI_MIN_VERSION, + "template_version": TEMPLATE_VERSION, + }, ) - # Build font installation step (only for presentations with custom fonts) - font_install_step = "" - if install_fonts and fonts_dir: - # Build strings separately to avoid backslash issues in f-strings - backtick = "`" - triple_backtick = "```" - find_cmd = ( - "find " - + fonts_dir - + ' -type f \\( -name "*.ttf" -o -name "*.otf" -o -name "*.woff" -o -name "*.woff2" \\) -exec cp {} ~/.local/share/fonts/ \\;' - ) - font_install_step = f""" - - name: Install custom fonts - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "## 🔤 Font Installation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Create user fonts directory - mkdir -p ~/.local/share/fonts - - # Copy fonts from repository - if [ -d "{fonts_dir}" ]; then - {find_cmd} - - # Update font cache - fc-cache -f -v - - echo "✅ **Fonts Installed Successfully**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Installed fonts from {backtick}{fonts_dir}/{backtick}:" >> $GITHUB_STEP_SUMMARY - echo "{triple_backtick}" >> $GITHUB_STEP_SUMMARY - ls -la ~/.local/share/fonts/ >> $GITHUB_STEP_SUMMARY - echo "{triple_backtick}" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ **Warning**: Font directory {backtick}{fonts_dir}{backtick} not found" >> $GITHUB_STEP_SUMMARY - fi -""" - - # Build additional document compilation steps - additional_compile_steps = "" - additional_artifact_files = "" - additional_release_files = "" - - for doc in additional_documents: - doc_base = doc.replace(".tex", "") - additional_compile_steps += f""" - - name: Compile additional document ({doc}) - uses: xu-cheng/latex-action@v3 - if: ${{{{{{ needs.workflow-setup.outputs.runner == 'ubuntu-latest' }}}}}} - with: - root_file: {doc} - latexmk_shell_escape: true - {f'args: "{outdir_arg}"' if outdir_arg else ''} - - - name: Compile additional document ({doc}) - Self-hosted - if: ${{{{{{ needs.workflow-setup.outputs.runner == 'self-texlive' }}}}}} - run: | - latexmk -shell-escape {latexmk_args} {outdir_arg} -file-line-error -interaction=nonstopmode {doc} - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Additional Document: {doc}" >> $GITHUB_STEP_SUMMARY - echo "- **Compiled**: ✅" >> $GITHUB_STEP_SUMMARY -""" - # Add to artifact and release files - additional_artifact_files += f"\n ./{pdf_location}{doc_base}.pdf" - additional_release_files += f"\n artifact/{doc_base}.pdf" - - return f"""name: Compile LaTeX and Release PDF - -# This workflow uses article-cli for: -# - Git hooks setup (article-cli setup) -# - Bibliography updates from Zotero (article-cli update-bibtex) -# - LaTeX build file cleanup (article-cli clean) -# -# Project type: {project_type} -# LaTeX engine: {'XeLaTeX' if use_xelatex else 'pdfLaTeX'} -# Output directory: {output_dir if output_dir else 'root'} -# Additional documents: {', '.join(additional_documents) if additional_documents else 'none'} - -on: - push: - tags: - - 'v*' - branches: - - 'main' - pull_request: - branches: - - 'main' - -jobs: - workflow-setup: - name: Workflow Setup - runs-on: ubuntu-24.04 - outputs: - runner: ${{{{ steps.texlive_runner.outputs.runner }}}} - prefix: ${{{{ steps.doc_prefix.outputs.prefix }}}} - prefixwithref: ${{{{ steps.doc_prefix.outputs.prefixwithref }}}} - pdf: ${{{{ steps.doc_prefix.outputs.pdf }}}} - tex: ${{{{ steps.doc_prefix.outputs.tex }}}} - steps: - - name: Get TeXLive Runner - id: texlive_runner - run: | - if ! [ -z "$GH_TOKEN" ]; then - runners=$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /orgs/feelpp/actions/runners) - texlive=$(echo $runners | jq --arg label "self-texlive" '[.runners[] | any(.labels[]; .name == $label) and .status == "online"] | any') - if [ "$texlive" = "false" ]; then - echo "runner=ubuntu-latest" >> "$GITHUB_OUTPUT" - else - echo "runner=self-texlive" >> "$GITHUB_OUTPUT" - fi - else - echo "runner=ubuntu-latest" >> "$GITHUB_OUTPUT" - fi - env: - GH_TOKEN: ${{{{ secrets.TOKEN_RUNNER }}}} - - - name: Get Document Prefix - id: doc_prefix - run: | - prefix=$(echo "${{{{ github.repository }}}}" | cut -d'/' -f2) - echo "prefix=$prefix" >> "$GITHUB_OUTPUT" - - # Handle different event types for naming - if [[ "${{{{ github.event_name }}}}" == "pull_request" ]]; then - # For pull requests, use pr-NUMBER format - prefixwithref=$(echo "$prefix")-pr-${{{{ github.event.number }}}} - else - # For tags and branches, use the ref name - prefixwithref=$(echo "$prefix")-${{{{ github.ref_name }}}} - fi - - echo "prefixwithref=$prefixwithref" >> "$GITHUB_OUTPUT" - echo "pdf=$prefixwithref.pdf" >> "$GITHUB_OUTPUT" - echo "tex={tex_base}.tex" >> "$GITHUB_OUTPUT" - - - name: Show Outputs - run: | - echo "runner=${{{{ steps.texlive_runner.outputs.runner }}}}" - echo "prefix=${{{{ steps.doc_prefix.outputs.prefix }}}}" - echo "prefixwithref=${{{{ steps.doc_prefix.outputs.prefixwithref }}}}" - echo "pdf=${{{{ steps.doc_prefix.outputs.pdf }}}}" - echo "tex=${{{{ steps.doc_prefix.outputs.tex }}}}" - - - build_latex: - needs: workflow-setup - runs-on: ${{{{ needs.workflow-setup.outputs.runner }}}} - name: Build LaTeX Artifact - env: - VERSION: ${{{{ github.ref_name }}}} - steps: - - name: Set up Git repository - uses: actions/checkout@v4 - with: - clean: true - - - name: Set up Python and uv - uses: astral-sh/setup-uv@v3 - with: - version: "latest" - enable-cache: false - - - name: Set up Python - run: uv python install 3.11 - - - name: Create virtual environment and install article-cli - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "## ⚡ Fast Python Setup with UV" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Setting up isolated Python environment..." >> $GITHUB_STEP_SUMMARY - - start_time=$(date +%s) - uv venv .venv --python 3.11 - echo "VIRTUAL_ENV=${{PWD}}/.venv" >> $GITHUB_ENV - echo "${{PWD}}/.venv/bin" >> $GITHUB_PATH - uv pip install "article-cli>=1.1.0" - end_time=$(date +%s) - duration=$((end_time - start_time)) - - echo "✅ **Environment Setup Complete**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Tool**: UV (fast Python package installer)" >> $GITHUB_STEP_SUMMARY - echo "- **Python**: 3.11 (isolated virtual environment)" >> $GITHUB_STEP_SUMMARY - echo "- **Package**: article-cli>=1.1.0" >> $GITHUB_STEP_SUMMARY - echo "- **Duration**: ${{duration}}s" >> $GITHUB_STEP_SUMMARY - echo "- **Cache**: Enabled for faster subsequent runs" >> $GITHUB_STEP_SUMMARY - - - name: Install hooks and setup - run: | - article-cli setup - - # Add git status to summary - echo "" >> $GITHUB_STEP_SUMMARY - echo "## 🔧 Git Setup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Event**: ${{{{ github.event_name }}}}" >> $GITHUB_STEP_SUMMARY - echo "- **Ref**: ${{{{ github.ref }}}}" >> $GITHUB_STEP_SUMMARY - echo "- **SHA**: ${{{{ github.sha }}}}" >> $GITHUB_STEP_SUMMARY - - # For pull requests, stay on the current commit; for branches/tags, checkout the ref - if [[ "${{{{ github.event_name }}}}" == "pull_request" ]]; then - echo "- **Action**: Staying on PR merge commit" >> $GITHUB_STEP_SUMMARY - echo "- **PDF Name**: ${{{{ needs.workflow-setup.outputs.pdf }}}} (pr-${{{{ github.event.number }}}} format)" >> $GITHUB_STEP_SUMMARY - echo "Pull request detected - staying on current commit ${{{{ github.sha }}}}" - else - echo "- **Action**: Checking out ${{{{ github.ref }}}}" >> $GITHUB_STEP_SUMMARY - echo "- **PDF Name**: ${{{{ needs.workflow-setup.outputs.pdf }}}} (ref-based format)" >> $GITHUB_STEP_SUMMARY - echo "Checking out ${{{{ github.ref }}}}" - git checkout ${{{{ github.ref }}}} - fi - - - name: Show article-cli configuration - run: | - article-cli --version - article-cli config show - echo "" >> $GITHUB_STEP_SUMMARY - echo "## 🔧 Environment Configuration" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Python Environment:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "UV Version: $(uv --version)" >> $GITHUB_STEP_SUMMARY - echo "Python Version: $(python --version)" >> $GITHUB_STEP_SUMMARY - echo "Virtual Environment: $VIRTUAL_ENV" >> $GITHUB_STEP_SUMMARY - echo "Article CLI: $(article-cli --version)" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Configuration Details:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - article-cli config show >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - - name: Update bibliography from Zotero - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "## 📚 Bibliography Update" >> $GITHUB_STEP_SUMMARY - echo "Updating bibliography from Zotero group using isolated virtual environment..." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if article-cli update-bibtex; then - echo "✅ **Bibliography Updated Successfully**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Environment**: Isolated venv with uv" >> $GITHUB_STEP_SUMMARY - echo "- **Source**: Zotero Group (configured in pyproject.toml)" >> $GITHUB_STEP_SUMMARY - echo "- **Output**: references.bib" >> $GITHUB_STEP_SUMMARY - echo "- **Backup**: references.bib.backup" >> $GITHUB_STEP_SUMMARY - if [ -f references.bib ]; then - entries=$(grep -c "^@" references.bib || echo "0") - echo "- **Total entries**: $entries" >> $GITHUB_STEP_SUMMARY - fi - else - echo "❌ **Bibliography Update Failed**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Please check Zotero API key and group permissions." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - env: - ZOTERO_API_KEY: ${{{{ secrets.ZOTERO_API_KEY }}}} -{font_install_step} - - name: Create output directory - if: ${{{{ '{output_dir}' != '' }}}} - run: mkdir -p {output_dir} - - - name: Compile LaTeX document - uses: xu-cheng/latex-action@v3 - if: ${{{{ needs.workflow-setup.outputs.runner == 'ubuntu-latest' }}}} - with: - root_file: ${{{{ needs.workflow-setup.outputs.tex }}}} - latexmk_shell_escape: true - latexmk_use_xelatex: {'true' if use_xelatex else 'false'} - {f'args: "{outdir_arg}"' if outdir_arg else ''} - post_compile: "article-cli clean" - - - name: Generate compilation summary (Ubuntu) - if: ${{{{ needs.workflow-setup.outputs.runner == 'ubuntu-latest' }}}} - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "## 🔨 LaTeX Compilation (Ubuntu)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Engine**: latexmk {'with XeLaTeX' if use_xelatex else 'with pdfLaTeX'} (shell-escape enabled)" >> $GITHUB_STEP_SUMMARY - echo "- **Runner**: ubuntu-latest (xu-cheng/latex-action@v3)" >> $GITHUB_STEP_SUMMARY - echo "- **Source**: `${{{{ needs.workflow-setup.outputs.tex }}}}`" >> $GITHUB_STEP_SUMMARY - {output_dir_echo} - echo "- **Clean-up**: article-cli clean (from isolated venv)" >> $GITHUB_STEP_SUMMARY - - - name: Compile LaTeX document - if: ${{{{ needs.workflow-setup.outputs.runner == 'self-texlive' }}}} - run: | - latexmk -shell-escape {latexmk_args} {outdir_arg} -file-line-error -interaction=nonstopmode ${{{{ needs.workflow-setup.outputs.tex }}}} - article-cli clean - echo "" >> $GITHUB_STEP_SUMMARY - echo "## 🔨 LaTeX Compilation (Self-hosted)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Engine**: latexmk {'with XeLaTeX' if use_xelatex else 'with pdfLaTeX'} (shell-escape enabled)" >> $GITHUB_STEP_SUMMARY - echo "- **Runner**: self-texlive (self-hosted)" >> $GITHUB_STEP_SUMMARY - echo "- **Source**: `${{{{ needs.workflow-setup.outputs.tex }}}}`" >> $GITHUB_STEP_SUMMARY - {output_dir_echo} - echo "- **Clean-up**: article-cli clean (from isolated venv)" >> $GITHUB_STEP_SUMMARY -{additional_compile_steps} - - name: Rename PDF - run: | - mv {pdf_location}{tex_base}.pdf ${{{{ needs.workflow-setup.outputs.pdf }}}} - echo "" >> $GITHUB_STEP_SUMMARY - echo "## 📄 LaTeX Compilation Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if [ -f "${{{{ needs.workflow-setup.outputs.pdf }}}}" ]; then - file_size=$(du -h "${{{{ needs.workflow-setup.outputs.pdf }}}}" | cut -f1) - echo "✅ **PDF Generated Successfully**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **File**: \`${{{{ needs.workflow-setup.outputs.pdf }}}}\`" >> $GITHUB_STEP_SUMMARY - echo "- **Size**: $file_size" >> $GITHUB_STEP_SUMMARY - echo "- **Runner**: ${{{{ needs.workflow-setup.outputs.runner }}}}" >> $GITHUB_STEP_SUMMARY - echo "- **Source**: \`${{{{ needs.workflow-setup.outputs.tex }}}}\`" >> $GITHUB_STEP_SUMMARY - else - echo "❌ **PDF Generation Failed**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Expected file: \`${{{{ needs.workflow-setup.outputs.pdf }}}}\`" >> $GITHUB_STEP_SUMMARY - fi - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: ${{{{ needs.workflow-setup.outputs.pdf }}}} - path: ${{{{ needs.workflow-setup.outputs.pdf }}}} - - name: Upload Full Artifact - uses: actions/upload-artifact@v4 - with: - name: ${{{{ needs.workflow-setup.outputs.prefixwithref }}}} - path: | - ./*.tex - ./*.bib - ./*.sty - ./*.cls - ./*.gin - ./*.bbl - ./*.tikz - ./${{{{ needs.workflow-setup.outputs.pdf }}}}{additional_artifact_files} - ./README.md - ./fig-* - ./data/* - {f'./{fonts_dir}/*' if fonts_dir else ''} - !./.git* - !./.github* - !./.vscode* - !./.idea* - !./.DS_Store* - !./.gitignore* - - - name: Generate build summary - if: always() - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "## 📦 Artifact Upload" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Artifact Name**: \`${{{{ needs.workflow-setup.outputs.prefixwithref }}}}\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Included Files:" >> $GITHUB_STEP_SUMMARY - echo "- LaTeX source files (\*.tex, \*.sty, \*.cls)" >> $GITHUB_STEP_SUMMARY - echo "- Bibliography files (\*.bib)" >> $GITHUB_STEP_SUMMARY - echo "- Generated PDF: \`${{{{ needs.workflow-setup.outputs.pdf }}}}\`" >> $GITHUB_STEP_SUMMARY - echo "- Git info files (\*.gin)" >> $GITHUB_STEP_SUMMARY - echo "- Figures and data files" >> $GITHUB_STEP_SUMMARY - - check: - needs: [build_latex,workflow-setup] - runs-on: ${{{{ needs.workflow-setup.outputs.runner }}}} - name: Check LaTeX Artifact - steps: - - - name: Download Artifact - uses: actions/download-artifact@v4 - with: - name: ${{{{ needs.workflow-setup.outputs.prefixwithref }}}} - path: ${{{{ github.workspace }}}}/artifact - - - name: Set up Python and uv - uses: astral-sh/setup-uv@v3 - with: - version: "latest" - enable-cache: false - - - name: Set up Python - run: uv python install 3.11 - - - name: Create virtual environment and install article-cli - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "## ⚡ Check Environment Setup" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - start_time=$(date +%s) - uv venv .venv --python 3.11 - echo "VIRTUAL_ENV=${{PWD}}/.venv" >> $GITHUB_ENV - echo "${{PWD}}/.venv/bin" >> $GITHUB_PATH - uv pip install "article-cli>=1.1.0" - end_time=$(date +%s) - duration=$((end_time - start_time)) - - echo "✅ **Check Environment Ready**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Setup time**: ${{duration}}s (with uv cache)" >> $GITHUB_STEP_SUMMARY - echo "- **Environment**: Isolated from build job" >> $GITHUB_STEP_SUMMARY - echo "- **Purpose**: Artifact verification" >> $GITHUB_STEP_SUMMARY - - - name: List Artifact - run: | - ls -R ${{{{ github.workspace }}}} - echo "" >> $GITHUB_STEP_SUMMARY - echo "## 🔍 Artifact Check" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Artifact**: \`${{{{ needs.workflow-setup.outputs.prefixwithref }}}}\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Artifact Contents:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - ls -la ${{{{ github.workspace }}}}/artifact/ >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - - name: Check compilation of LaTeX document from artifact - if: ${{{{ needs.workflow-setup.outputs.runner == 'ubuntu-latest' }}}} - uses: xu-cheng/latex-action@v3 - with: - root_file: ${{{{ needs.workflow-setup.outputs.tex }}}} - latexmk_shell_escape: true - latexmk_use_xelatex: {'true' if use_xelatex else 'false'} - working_directory: ${{{{ github.workspace }}}}/artifact - - - name: Generate artifact verification summary (Ubuntu) - if: ${{{{ needs.workflow-setup.outputs.runner == 'ubuntu-latest' }}}} - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ **Artifact Verification Completed (Ubuntu)**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **LaTeX compilation from artifact**: Success" >> $GITHUB_STEP_SUMMARY - echo "- **Runner**: ${{{{ needs.workflow-setup.outputs.runner }}}}" >> $GITHUB_STEP_SUMMARY - echo "- **Clean-up**: Not needed (artifact already cleaned)" >> $GITHUB_STEP_SUMMARY - - - name: Check compilation of LaTeX document from artifact - if: ${{{{ needs.workflow-setup.outputs.runner == 'self-texlive' }}}} - run: | - latexmk -shell-escape {latexmk_args} -file-line-error -interaction=nonstopmode ${{{{ needs.workflow-setup.outputs.tex }}}} - echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ **Artifact Verification Completed**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **LaTeX compilation from artifact**: Success" >> $GITHUB_STEP_SUMMARY - echo "- **Runner**: ${{{{ needs.workflow-setup.outputs.runner }}}}" >> $GITHUB_STEP_SUMMARY - echo "- **Clean-up**: Not needed (artifact already cleaned)" >> $GITHUB_STEP_SUMMARY - working-directory: ${{{{ github.workspace }}}}/artifact - - release: - needs: [workflow-setup,build_latex, check] - runs-on: ${{{{ needs.workflow-setup.outputs.runner }}}} - name: Create Release - if: startsWith(github.ref, 'refs/tags/v') - steps: - - name: Download Artifact - uses: actions/download-artifact@v4 - with: - name: ${{{{ needs.workflow-setup.outputs.prefixwithref }}}} - path: ${{{{ github.workspace }}}}/artifact - - - name: Archive Article - run: | - temp_dir=$(mktemp -d) - tar -czvf "${{temp_dir}}/${{{{ needs.workflow-setup.outputs.prefixwithref }}}}.tar.gz" -C artifact ./ - mv "${{temp_dir}}/${{{{ needs.workflow-setup.outputs.prefixwithref }}}}.tar.gz" ./ - rm -rf "$temp_dir" - - # Generate release summary - echo "" >> $GITHUB_STEP_SUMMARY - echo "## 🚀 Release Preparation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Tag**: \`${{{{ github.ref_name }}}}\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Release Assets:" >> $GITHUB_STEP_SUMMARY - - if [ -f "artifact/${{{{ needs.workflow-setup.outputs.prefixwithref }}}}.pdf" ]; then - pdf_size=$(du -h "artifact/${{{{ needs.workflow-setup.outputs.prefixwithref }}}}.pdf" | cut -f1) - echo "- 📄 **PDF**: \`${{{{ needs.workflow-setup.outputs.prefixwithref }}}}.pdf\` ($pdf_size)" >> $GITHUB_STEP_SUMMARY - fi - - if [ -f "${{{{ needs.workflow-setup.outputs.prefixwithref }}}}.tar.gz" ]; then - archive_size=$(du -h "${{{{ needs.workflow-setup.outputs.prefixwithref }}}}.tar.gz" | cut -f1) - echo "- 📦 **Archive**: \`${{{{ needs.workflow-setup.outputs.prefixwithref }}}}.tar.gz\` ($archive_size)" >> $GITHUB_STEP_SUMMARY - fi - - - name: Create Release - id: create_release - uses: softprops/action-gh-release@v2 - with: - draft: false - prerelease: ${{{{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') || contains(github.ref, 'preview') }}}} - name: Release ${{{{ github.ref_name }}}} - generate_release_notes: true - tag_name: ${{{{ github.ref }}}} - token: ${{{{ secrets.GITHUB_TOKEN }}}} - files: | - artifact/${{{{ needs.workflow-setup.outputs.prefixwithref }}}}.pdf{additional_release_files} - ${{{{ needs.workflow-setup.outputs.prefixwithref }}}}.tar.gz - - - name: Generate release summary - if: always() - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "## 🎉 Release Created" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${{{{ steps.create_release.outcome }}}}" = "success" ]; then - echo "✅ **Release Published Successfully**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Release**: [${{{{ github.ref_name }}}}](${{{{ steps.create_release.outputs.url }}}})" >> $GITHUB_STEP_SUMMARY - echo "- **Type**: ${{{{ contains(github.ref, 'alpha') && 'Pre-release' || contains(github.ref, 'beta') && 'Pre-release' || contains(github.ref, 'rc') && 'Pre-release' || contains(github.ref, 'preview') && 'Pre-release' || 'Stable Release' }}}}" >> $GITHUB_STEP_SUMMARY - echo "- **Assets**: PDF + Source Archive" >> $GITHUB_STEP_SUMMARY - echo "- **Notes**: Auto-generated from commits" >> $GITHUB_STEP_SUMMARY - else - echo "❌ **Release Creation Failed**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Please check the workflow logs for details." >> $GITHUB_STEP_SUMMARY - fi -""" - def _create_pyproject( self, project_name: str, @@ -1372,9 +773,19 @@ def _create_pyproject( output_dir: str = "", fonts_dir: str = "", install_fonts: bool = False, + style: str = "default", + template: str = "", + main_document: str = "", + ci_bibliography: str = "off", + ci_runner_policy: str = "github", + ci_github_runner: str = "ubuntu-24.04", + ci_self_hosted_label: str = "self-texlive", + ci_self_hosted_org: str = "", + ci_release_policy: str = "github", + ci_artifact_includes: Optional[List[str]] = None, ) -> bool: """ - Create pyproject.toml file + Create pyproject.toml file. Args: project_name: Name of the project @@ -1389,6 +800,16 @@ def _create_pyproject( output_dir: Output directory for compiled files fonts_dir: Directory containing custom fonts install_fonts: Whether to install fonts in CI + style: Built-in document style + template: Local custom template path + main_document: Main document written to project config + ci_bibliography: CI bibliography policy + ci_runner_policy: CI runner policy + ci_github_runner: GitHub-hosted runner label + ci_self_hosted_label: Self-hosted runner label + ci_self_hosted_org: Organization for self-hosted auto-discovery + ci_release_policy: CI release policy + ci_artifact_includes: Extra artifact path globs Returns: True if successful @@ -1399,99 +820,60 @@ def _create_pyproject( print_info("pyproject.toml already exists (use --force to overwrite)") return True - # Format authors for TOML - authors_toml = ",\n ".join([f'{{name = "{author}"}}' for author in authors]) - - # Determine default engine based on project type - default_engine = ( - "xelatex" if project_type in ["presentation", "poster"] else "latexmk" + additional_documents = additional_documents or [] + ci_artifact_includes = ci_artifact_includes or [] + if project_type.startswith("typst-"): + default_engine = "typst" + elif project_type in ["presentation", "poster"]: + default_engine = "xelatex" + else: + default_engine = "latexmk" + content = self.renderer.render( + "project/pyproject.toml.j2", + { + "project_type_title": project_type.capitalize(), + "project_type": project_type, + "style_toml": self._toml_string(style or "default"), + "custom_template": bool(template), + "template_toml": self._toml_string(template), + "project_name_toml": self._toml_string(project_name), + "title_toml": self._toml_string(title), + "author_entries": [ + TemplateValue(self._toml_string(author)) for author in authors + ], + "article_cli_min_version": ARTICLE_CLI_MIN_VERSION, + "template_version": TEMPLATE_VERSION, + "group_id_toml": self._toml_string(group_id), + "default_engine": default_engine, + "theme_toml": self._toml_string(theme), + "aspect_ratio": aspect_ratio, + "main_document_toml": self._toml_string( + main_document or f"{self.repo_path.name}.tex" + ), + "additional_documents": additional_documents, + "additional_document_entries": [ + TemplateValue(self._toml_string(doc)) + for doc in additional_documents + ], + "output_dir": output_dir, + "output_dir_toml": self._toml_string(output_dir), + "fonts_dir": fonts_dir, + "fonts_dir_toml": self._toml_string(fonts_dir), + "install_fonts": install_fonts, + "runner_policy_toml": self._toml_string(ci_runner_policy), + "github_runner_toml": self._toml_string(ci_github_runner), + "self_hosted_label_toml": self._toml_string(ci_self_hosted_label), + "self_hosted_org_toml": self._toml_string(ci_self_hosted_org), + "bibliography_policy_toml": self._toml_string(ci_bibliography), + "release_policy_toml": self._toml_string(ci_release_policy), + "artifact_include_entries": [ + TemplateValue(self._toml_string(path)) + for path in ci_artifact_includes + ], + }, ) - # Build project type specific config - project_type_config = f""" -[tool.article-cli.project] -type = "{project_type}" -""" - - if project_type == "presentation": - project_type_config += f""" -[tool.article-cli.presentation] -theme = "{theme}" -aspect_ratio = "{aspect_ratio}" -color_theme = "" -font_theme = "" -""" - elif project_type == "poster": - project_type_config += """ -[tool.article-cli.poster] -size = "a0" -orientation = "portrait" -columns = 3 -""" - - # Build documents configuration - documents_config = "" - if additional_documents: - additional_list = ", ".join([f'"{doc}"' for doc in additional_documents]) - documents_config = f""" -[tool.article-cli.documents] -# main = "{self.repo_path.name}.tex" # Uncomment to specify main document -additional = [{additional_list}] -""" - - # Build workflow configuration - workflow_config = "" - if output_dir or fonts_dir or install_fonts: - workflow_config = """ -[tool.article-cli.workflow] -""" - if output_dir: - workflow_config += f'output_dir = "{output_dir}"\n' - if fonts_dir: - workflow_config += f'fonts_dir = "{fonts_dir}"\n' - if install_fonts: - workflow_config += f"install_fonts = true\n" - - pyproject_content = f"""# {project_type.capitalize()} Repository Dependency Management -# This file manages dependencies for the LaTeX {project_type} project - -[project] -name = "{project_name}" -version = "0.1.0" -description = "{title}" -authors = [ - {authors_toml}, -] -readme = "README.md" -requires-python = ">=3.8" -dependencies = [ - "article-cli>=1.2.0", - # Add other dependencies your project might need: - # "matplotlib>=3.5.0", - # "numpy>=1.20.0", - # "pandas>=1.3.0", -] - -# Configuration for article-cli (embedded in pyproject.toml) -[tool.article-cli.zotero] -group_id = "{group_id}" # Zotero group ID for this project -# api_key = "your_api_key_here" # Uncomment and add your API key or use ZOTERO_API_KEY env variable -output_file = "references.bib" - -[tool.article-cli.git] -auto_push = false -default_branch = "main" - -[tool.article-cli.latex] -clean_extensions = [ - ".aux", ".bbl", ".blg", ".log", ".out", ".pyg", - ".fls", ".synctex.gz", ".toc", ".fdb_latexmk", - ".idx", ".ilg", ".ind", ".lof", ".lot", ".nav", ".snm", ".vrb" -] -engine = "{default_engine}" -{project_type_config}{documents_config}{workflow_config}""" - - pyproject_path.write_text(pyproject_content) + pyproject_path.write_text(content, encoding="utf-8") print_success(f"Created: {pyproject_path.relative_to(self.repo_path)}") return True @@ -1503,9 +885,13 @@ def _create_readme( tex_file: str, force: bool, project_type: str = "article", + style: str = "default", + ci_bibliography: str = "off", + ci_runner_policy: str = "github", + ci_release_policy: str = "github", ) -> bool: """ - Create README.md file + Create README.md file. Args: project_name: Name of the project @@ -1514,6 +900,10 @@ def _create_readme( tex_file: Main .tex filename force: Overwrite if exists project_type: Type of project + style: Built-in or custom document style + ci_bibliography: CI bibliography policy + ci_runner_policy: CI runner policy + ci_release_policy: CI release policy Returns: True if successful @@ -1524,10 +914,16 @@ def _create_readme( print_info("README.md already exists (use --force to overwrite)") return True - authors_list = "\n".join([f"- {author}" for author in authors]) - - # Determine build command based on project type - if project_type == "presentation": + if project_type == "typst-article": + build_cmd = f"typst compile {tex_file}" + doc_type = "Typst article" + elif project_type == "typst-presentation": + build_cmd = f"typst compile {tex_file}" + doc_type = "Typst presentation" + elif project_type == "typst-poster": + build_cmd = f"typst compile {tex_file}" + doc_type = "Typst poster" + elif project_type == "presentation": build_cmd = f"latexmk -xelatex {tex_file}" doc_type = "presentation" elif project_type == "poster": @@ -1537,143 +933,30 @@ def _create_readme( build_cmd = f"latexmk -pdf {tex_file}" doc_type = "article" - readme_content = f"""# {title} - -## Authors - -{authors_list} - -## Overview - -This repository contains the LaTeX source for the {doc_type} "{title}". - -## Prerequisites - -- Python 3.8+ (for bibliography management) -- LaTeX distribution (TeX Live recommended) -- Git with gitinfo2 package - -## Setup - -1. **Install article-cli**: - ```bash - pip install article-cli - ``` - -2. **Setup git hooks**: - ```bash - article-cli setup - ``` - -3. **Configure Zotero** (for bibliography management): - - Add your Zotero API key as a secret in GitHub: - - Go to Repository Settings → Secrets → Actions - - Add `ZOTERO_API_KEY` with your API key - - Or set it locally: - ```bash - export ZOTERO_API_KEY="your_api_key_here" - ``` - -4. **Update bibliography**: - ```bash - article-cli update-bibtex - ``` - -## Building the Document - -### Local Build - -```bash -{build_cmd} -``` - -Or using article-cli: -```bash -article-cli compile {tex_file} -``` - -### Clean Build Files - -```bash -article-cli clean -``` - -## CI/CD - -This repository uses GitHub Actions for automated PDF compilation: - -- **On push to main**: Compiles and uploads PDF artifact -- **On pull request**: Compiles and verifies the document -- **On tag push (v*)**: Creates a GitHub release with PDF - -## Project Structure - -``` -. -├── {tex_file} # Main LaTeX document -├── references.bib # Bibliography (managed via Zotero) -├── pyproject.toml # Project configuration -├── README.md # This file -└── .github/ - └── workflows/ - └── latex.yml # CI/CD pipeline -``` - -## article-cli Commands - -```bash -# Setup repository -article-cli setup - -# Update bibliography from Zotero -article-cli update-bibtex - -# Clean LaTeX build files -article-cli clean - -# Create a release -article-cli create v1.0.0 - -# List releases -article-cli list - -# Show configuration -article-cli config show -``` - -## Development Workflow - -1. Make changes to LaTeX source files -2. Update bibliography if needed: `article-cli update-bibtex` -3. Build locally: `latexmk -pdf {tex_file}` -4. Commit and push changes -5. Create a release tag for publication: `article-cli create v1.0.0 --push` - -## License - -[Specify your license here] - -## Citation - -```bibtex -@article{{{project_name}, - title = {{{title}}}, - author = {{{', '.join(authors)}}}, - year = {{2025}}, - url = {{https://github.com/feelpp/{project_name}}} -}} -``` -""" - - readme_path.write_text(readme_content) + content = self.renderer.render( + "project/README.md.j2", + { + "project_name": project_name, + "title": title, + "authors": authors, + "tex_file": tex_file, + "build_cmd": build_cmd, + "doc_type": doc_type, + "style": style or "default", + "ci_bibliography": ci_bibliography, + "ci_runner_policy": ci_runner_policy, + "ci_release_policy": ci_release_policy, + "citation_key": project_name.replace("-", "_"), + "authors_bibtex": " and ".join(authors), + }, + ) + readme_path.write_text(content, encoding="utf-8") print_success(f"Created: {readme_path.relative_to(self.repo_path)}") return True def _create_gitignore(self, force: bool) -> bool: """ - Create or update .gitignore file with LaTeX-specific entries + Create or update .gitignore file with LaTeX-specific entries. Args: force: Overwrite if exists @@ -1682,63 +965,21 @@ def _create_gitignore(self, force: bool) -> bool: True if successful """ gitignore_path = self.repo_path / ".gitignore" - - latex_ignores = """ -# LaTeX build files -*.aux -*.bbl -*.blg -*.log -*.out -*.toc -*.fdb_latexmk -*.fls -*.synctex.gz -*.pdf -*.dvi -*.ps -*.idx -*.ilg -*.ind -*.lof -*.lot - -# Python -__pycache__/ -*.py[cod] -*$py.class -.venv/ -venv/ -.pytest_cache/ -.mypy_cache/ - -# Editor -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# article-cli -.article-cli.toml.backup -references.bib.backup -""" + latex_ignores = self.renderer.render("project/gitignore.j2", {}) if gitignore_path.exists() and not force: - # Append if not already present - existing = gitignore_path.read_text() + existing = gitignore_path.read_text(encoding="utf-8") if "LaTeX build files" not in existing: - with open(gitignore_path, "a") as f: - f.write(latex_ignores) + separator = "" if existing.endswith("\n") else "\n" + gitignore_path.write_text( + existing + separator + latex_ignores, + encoding="utf-8", + ) print_info("Updated .gitignore with LaTeX entries") else: print_info(".gitignore already contains LaTeX entries") else: - gitignore_path.write_text(latex_ignores.lstrip()) + gitignore_path.write_text(latex_ignores, encoding="utf-8") print_success(f"Created: {gitignore_path.relative_to(self.repo_path)}") return True @@ -1747,7 +988,7 @@ def _create_vscode_settings( self, force: bool, project_type: str = "article" ) -> bool: """ - Create VS Code settings for LaTeX Workshop + Create VS Code settings for LaTeX Workshop. Args: force: Overwrite if exists @@ -1764,160 +1005,18 @@ def _create_vscode_settings( ) return True - # Use XeLaTeX for presentations and posters - if project_type in ("presentation", "poster"): - settings_content = """{ - "latex-workshop.latex.recipes": [ - { - "name": "latexmk-xelatex", - "tools": [ - "latexmk-xelatex-shell-escape" - ] - }, - { - "name": "latexmk-lualatex", - "tools": [ - "latexmk-lualatex-shell-escape" - ] - }, - { - "name": "xelatex-shell-escape-recipe", - "tools": [ - "xelatex-shell-escape" - ] - } - ], - "latex-workshop.latex.tools": [ - { - "name": "latexmk-xelatex-shell-escape", - "command": "latexmk", - "args": [ - "--shell-escape", - "-xelatex", - "-interaction=nonstopmode", - "-synctex=1", - "%DOC%" - ], - "env": {} - }, - { - "name": "latexmk-lualatex-shell-escape", - "command": "latexmk", - "args": [ - "--shell-escape", - "-lualatex", - "-interaction=nonstopmode", - "-synctex=1", - "%DOC%" - ], - "env": {} - }, - { - "name": "xelatex-shell-escape", - "command": "xelatex", - "args": [ - "--shell-escape", - "-synctex=1", - "-interaction=nonstopmode", - "-file-line-error", - "%DOC%" - ] - } - ], - "latex-workshop.latex.autoBuild.run": "onSave", - "latex-workshop.latex.autoBuild.enabled": true, - "latex-workshop.latex.build.showOutput": "always", - "latex-workshop.latex.outDir": "%DIR%", - "latex-workshop.latex.clean.subfolder.enabled": true, - "latex-workshop.message.badbox.show": "none", - "workbench.editor.pinnedTabsOnSeparateRow": true, - "ltex.latex.commands": { - "\\\\author{}": "ignore", - "\\\\IfFileExists{}{}": "ignore", - "\\\\todo{}": "ignore", - "\\\\todo[]{}": "ignore", - "\\\\ts{}": "ignore", - "\\\\cp{}": "ignore", - "\\\\pgfmathprintnumber{}": "dummy", - "\\\\feelpp{}": "dummy", - "\\\\pgfplotstableread[]{}": "ignore", - "\\\\xpatchcmd{}{}{}{}{}": "ignore" - }, - "ltex.enabled": true, - "ltex.language": "en-US" -} -""" - else: - settings_content = """{ - "latex-workshop.latex.recipes": [ - { - "name": "latexmk-pdf", - "tools": [ - "latexmk-shell-escape" - ] - }, - { - "name": "pdflatex-shell-escape-recipe", - "tools": [ - "pdflatex-shell-escape" - ] - } - ], - "latex-workshop.latex.tools": [ - { - "name": "latexmk-shell-escape", - "command": "latexmk", - "args": [ - "--shell-escape", - "-pdf", - "-interaction=nonstopmode", - "-synctex=1", - "%DOC%" - ], - "env": {} - }, - { - "name": "pdflatex-shell-escape", - "command": "pdflatex", - "args": [ - "--shell-escape", - "-synctex=1", - "-interaction=nonstopmode", - "-file-line-error", - "%DOC%" - ] - } - ], - "latex-workshop.latex.autoBuild.run": "onSave", - "latex-workshop.latex.autoBuild.enabled": true, - "latex-workshop.latex.build.showOutput": "always", - "latex-workshop.latex.outDir": "%DIR%", - "latex-workshop.latex.clean.subfolder.enabled": true, - "latex-workshop.message.badbox.show": "none", - "workbench.editor.pinnedTabsOnSeparateRow": true, - "ltex.latex.commands": { - "\\\\author{}": "ignore", - "\\\\IfFileExists{}{}": "ignore", - "\\\\todo{}": "ignore", - "\\\\todo[]{}": "ignore", - "\\\\ts{}": "ignore", - "\\\\cp{}": "ignore", - "\\\\pgfmathprintnumber{}": "dummy", - "\\\\feelpp{}": "dummy", - "\\\\pgfplotstableread[]{}": "ignore", - "\\\\xpatchcmd{}{}{}{}{}": "ignore" - }, - "ltex.enabled": true, - "ltex.language": "en-US" -} -""" - - vscode_settings_path.write_text(settings_content) + vscode_settings_path.parent.mkdir(parents=True, exist_ok=True) + content = self.renderer.render( + "project/vscode-settings.json.j2", + { + "is_typst": project_type.startswith("typst-"), + "use_xelatex": project_type in ("presentation", "poster"), + }, + ) + vscode_settings_path.write_text(content, encoding="utf-8") print_success(f"Created: {vscode_settings_path.relative_to(self.repo_path)}") - # Create ltex dictionary files self._create_ltex_files(force) - return True def _create_ltex_files(self, force: bool) -> bool: @@ -1942,7 +1041,7 @@ def _create_ltex_files(self, force: bool) -> bool: functionals parametrical """ - dictionary_path.write_text(dictionary_content) + dictionary_path.write_text(dictionary_content, encoding="utf-8") print_info(f"Created: {dictionary_path.relative_to(self.repo_path)}") # Hidden false positives file (empty initially) @@ -1950,7 +1049,7 @@ def _create_ltex_files(self, force: bool) -> bool: self.repo_path / ".vscode" / "ltex.hiddenFalsePositives.en-US.txt" ) if not false_positives_path.exists() or force: - false_positives_path.write_text("") + false_positives_path.write_text("", encoding="utf-8") print_info(f"Created: {false_positives_path.relative_to(self.repo_path)}") return True @@ -1965,56 +1064,5 @@ def _create_git_hooks(self, force: bool) -> bool: Returns: True if successful """ - post_commit_path = self.repo_path / "hooks" / "post-commit" - - if post_commit_path.exists() and not force: - print_info("hooks/post-commit already exists (use --force to overwrite)") - return True - - # gitinfo2 post-commit hook - post_commit_content = """#!/bin/sh -# Copyright 2015 Brent Longborough -# Part of gitinfo2 package Version 2 -# Release 2.0.7 2015-11-22 -# Please read gitinfo2.pdf for licencing and other details -# ----------------------------------------------------- -# Post-{commit,checkout,merge} hook for the gitinfo2 package -# -# Get the first tag found in the history from the current HEAD -FIRSTTAG=$(git describe --tags --always --dirty='-*' 2>/dev/null) -# Get the first tag in history that looks like a Release -RELTAG=$(git describe --tags --long --always --dirty='-*' --match 'v[0-9]*\\.[0-9]*\\.[0-9]*' 2>/dev/null) -# Hoover up the metadata -git --no-pager log -1 --date=short --decorate=short \\ - --pretty=format:"\\usepackage[% - shash={%h}, - lhash={%H}, - authname={%an}, - authemail={%ae}, - authsdate={%ad}, - authidate={%ai}, - authudate={%at}, - commname={%cn}, - commemail={%ce}, - commsdate={%cd}, - commidate={%ci}, - commudate={%ct}, - refnames={%d}, - firsttagdescribe={$FIRSTTAG}, - reltag={$RELTAG} - ]{gitexinfo}" HEAD > .git/gitHeadInfo.gin -""" - - post_commit_path.write_text(post_commit_content) - - # Make the hook executable - import stat - - post_commit_path.chmod( - post_commit_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH - ) - - print_success(f"Created: {post_commit_path.relative_to(self.repo_path)}") - print_info("Made post-commit hook executable") - + ensure_gitinfo2_hook_source(self.repo_path, force) return True diff --git a/src/article_cli/services/__init__.py b/src/article_cli/services/__init__.py new file mode 100644 index 0000000..7000d26 --- /dev/null +++ b/src/article_cli/services/__init__.py @@ -0,0 +1,21 @@ +""" +Service layer for article-cli command handlers. +""" + +from .bibliography import BibliographyService, BibliographyUpdateOptions +from .compiler import CompileOptions, CompilerService +from .git import GitService +from .gitinfo import GitInfoService +from .release import ReleaseService +from .workflow import WorkflowService + +__all__ = [ + "BibliographyService", + "BibliographyUpdateOptions", + "CompileOptions", + "CompilerService", + "GitInfoService", + "GitService", + "ReleaseService", + "WorkflowService", +] diff --git a/src/article_cli/services/bibliography.py b/src/article_cli/services/bibliography.py new file mode 100644 index 0000000..4499127 --- /dev/null +++ b/src/article_cli/services/bibliography.py @@ -0,0 +1,109 @@ +""" +Bibliography service. +""" + +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable + +from ..config import Config +from ..reporting import print_info, print_success +from ..zotero import ZoteroBibTexUpdater + + +@dataclass(frozen=True) +class BibliographyUpdateOptions: + """User-facing bibliography update options.""" + + no_backup: bool = False + dry_run: bool = False + check: bool = False + include_local: bool = False + check_citations: bool = False + timestamp: bool = False + + +class BibliographyService: + """Service boundary for bibliography synchronization.""" + + def __init__( + self, + config: Config, + updater_cls: Callable[..., ZoteroBibTexUpdater] = ZoteroBibTexUpdater, + ) -> None: + self.config = config + self.updater_cls = updater_cls + + def update(self, args: Any, options: BibliographyUpdateOptions) -> bool: + """Validate config and update bibliography data.""" + zotero_config = self.config.validate_zotero_config(args) + + if options.dry_run: + library = ( + f"group {zotero_config['group_id']}" + if zotero_config["group_id"] + else f"user {zotero_config['user_id']}" + ) + print_info("Dry run: no bibliography files were changed.") + print_info(f"Would update bibliography from Zotero {library}.") + if zotero_config.get("collection_id"): + print_info(f"Would export collection: {zotero_config['collection_id']}") + print_info(f"Would write: {zotero_config['output_file']}") + if options.include_local: + print_info( + f"Would include local entries: {zotero_config['local_file']}" + ) + if zotero_config.get("merged_output_file"): + print_info( + f"Would write merged file: {zotero_config['merged_output_file']}" + ) + if options.check: + print_info("Would check whether bibliography files are current.") + if options.check_citations: + print_info("Would check citation key completeness.") + if not options.no_backup: + print_info("Would create a backup if the output file exists.") + print_success("Bibliography update dry run completed") + return True + + updater = self.updater_cls( + api_key=zotero_config["api_key"], + user_id=zotero_config["user_id"], + group_id=zotero_config["group_id"], + collection_id=zotero_config.get("collection_id") or None, + output_file=zotero_config["output_file"], + ) + return bool( + updater.update( + backup=not options.no_backup, + check=options.check, + include_local=options.include_local, + local_file=zotero_config.get("local_file") or None, + merged_output_file=zotero_config.get("merged_output_file") or None, + check_citations=options.check_citations, + citation_sources=_citation_sources(self.config), + timestamp=options.timestamp + or not bool(zotero_config.get("deterministic", True)), + ) + ) + + +def _citation_sources(config: Config) -> list[Path]: + """Return likely source files for citation completeness checks.""" + documents_config = config.get_documents_config() + candidates = [] + main = documents_config.get("main") + if main: + candidates.append(Path(str(main))) + candidates.append(Path(str(main)).with_suffix(".aux")) + + for document in documents_config.get("additional") or []: + candidates.append(Path(str(document))) + candidates.append(Path(str(document)).with_suffix(".aux")) + + if not candidates: + candidates.extend(sorted(Path.cwd().glob("*.tex"))) + candidates.extend(sorted(Path.cwd().glob("*.typ"))) + candidates.extend(sorted(Path.cwd().glob("*.aux"))) + + return [path for path in candidates if path.exists()] diff --git a/src/article_cli/services/compiler.py b/src/article_cli/services/compiler.py new file mode 100644 index 0000000..362138b --- /dev/null +++ b/src/article_cli/services/compiler.py @@ -0,0 +1,130 @@ +""" +Compiler service. +""" + +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional, Type + +from ..config import Config +from ..git_manager import GitManager +from ..latex_compiler import LaTeXCompiler +from ..project_context import LATEX_ENGINES, ProjectContext, TYPST_ENGINE +from ..reporting import print_error, print_info +from ..typst_compiler import TypstCompiler + + +@dataclass(frozen=True) +class CompileOptions: + """User-facing compile options.""" + + document: Optional[str] = None + engine: Optional[str] = None + shell_escape: Optional[bool] = None + output_dir: Optional[str] = None + font_paths: Optional[List[str]] = None + clean_first: bool = False + clean_after: bool = False + watch: bool = False + + +class CompilerService: + """Resolve project policy and dispatch to the right compiler.""" + + def __init__( + self, + config: Config, + cwd: Optional[Path] = None, + latex_compiler_cls: Type[LaTeXCompiler] = LaTeXCompiler, + typst_compiler_cls: Type[TypstCompiler] = TypstCompiler, + git_manager_cls: Type[GitManager] = GitManager, + ) -> None: + self.config = config + self.cwd = cwd + self.latex_compiler_cls = latex_compiler_cls + self.typst_compiler_cls = typst_compiler_cls + self.git_manager_cls = git_manager_cls + + def compile(self, options: CompileOptions) -> bool: + """Compile the resolved document.""" + context = ProjectContext.resolve( + self.config, + cwd=self.cwd, + document=options.document, + engine=options.engine, + output_dir=options.output_dir, + shell_escape=options.shell_escape, + ) + + if context.document is None: + print_error( + "No .tex or .typ file specified and none found in current directory" + ) + return False + if not context.document.exists(): + print_error(f"Document file not found: {context.document}") + return False + if context.document.suffix == ".tex" and context.engine == TYPST_ENGINE: + print_error(f"Cannot use Typst engine with .tex file: {context.document}") + return False + if context.document.suffix == ".typ" and context.engine != TYPST_ENGINE: + print_info("Detected Typst file, switching engine to typst") + + print_info(f"Resolved document: {context.document}") + print_info(f"Selected engine: {context.engine}") + print_info(f"Output directory: {context.output_dir_name or '.'}") + if context.engine != TYPST_ENGINE: + shell_escape = "enabled" if context.shell_escape else "disabled" + print_info(f"Shell escape: {shell_escape}") + + if context.engine == TYPST_ENGINE: + return self._compile_typst(context, options) + + if context.engine not in LATEX_ENGINES: + print_error(f"Unknown engine: {context.engine}") + return False + return self._compile_latex(context, options) + + def _compile_typst( + self, + context: ProjectContext, + options: CompileOptions, + ) -> bool: + """Compile a Typst document.""" + compiler = self.typst_compiler_cls(self.config) + return bool( + compiler.compile( + typ_file=context.document_name or str(context.document), + output_dir=context.output_dir_name, + font_paths=options.font_paths, + watch=options.watch, + ) + ) + + def _compile_latex( + self, + context: ProjectContext, + options: CompileOptions, + ) -> bool: + """Compile a LaTeX document.""" + latex_config = self.config.get_latex_config() + if options.clean_first: + print_info("Cleaning build files before compilation...") + self.git_manager_cls().clean_latex_files(latex_config["clean_extensions"]) + + compiler = self.latex_compiler_cls(self.config) + success = bool( + compiler.compile( + tex_file=context.document_name or str(context.document), + engine=context.engine, + shell_escape=context.shell_escape, + output_dir=context.output_dir_name, + watch=options.watch, + ) + ) + + if options.clean_after and success: + print_info("Cleaning build files after compilation...") + self.git_manager_cls().clean_latex_files(latex_config["clean_extensions"]) + + return success diff --git a/src/article_cli/services/git.py b/src/article_cli/services/git.py new file mode 100644 index 0000000..72cef0a --- /dev/null +++ b/src/article_cli/services/git.py @@ -0,0 +1,29 @@ +""" +Git service wrappers. +""" + +from pathlib import Path +from typing import List, Optional, Type + +from ..git_manager import GitManager + + +class GitService: + """Service boundary around repository git operations.""" + + def __init__( + self, + repo_root: Optional[Path] = None, + manager_cls: Type[GitManager] = GitManager, + ) -> None: + self.manager = ( + manager_cls(repo_root) if repo_root is not None else manager_cls() + ) + + def setup_hooks(self, dry_run: bool = False) -> bool: + """Install or preview article-cli managed hooks.""" + return self.manager.setup_hooks(dry_run=dry_run) + + def clean_latex_files(self, extensions: Optional[List[str]] = None) -> bool: + """Clean LaTeX build files.""" + return self.manager.clean_latex_files(extensions) diff --git a/src/article_cli/services/gitinfo.py b/src/article_cli/services/gitinfo.py new file mode 100644 index 0000000..44df00e --- /dev/null +++ b/src/article_cli/services/gitinfo.py @@ -0,0 +1,125 @@ +""" +gitinfo2 metadata service. +""" + +from dataclasses import dataclass +from pathlib import Path +from typing import Optional, Type, Union + +from ..command_runner import DEFAULT_RUNNER, CommandRunner +from ..config import Config +from ..git_manager import GitManager +from ..project_context import ProjectContext +from ..reporting import print_error, print_info, print_success +from .compiler import CompileOptions, CompilerService +from .release import _absolute_pdf_path, pdf_contains_text, write_sha256 + + +@dataclass(frozen=True) +class VersionOptions: + """User-facing gitinfo2 version workflow options.""" + + dry_run: bool = False + compile_pdf: bool = False + check_pdf: bool = False + checksum: bool = False + tag: Optional[str] = None + document: Optional[str] = None + engine: Optional[str] = None + output_dir: Optional[str] = None + shell_escape: Optional[bool] = None + + +class GitInfoService: + """Service boundary for gitinfo2 metadata refresh/reporting.""" + + def __init__( + self, + config: Optional[Config] = None, + repo_root: Optional[Path] = None, + manager_cls: Type[GitManager] = GitManager, + compiler_service_cls: Type[CompilerService] = CompilerService, + runner: CommandRunner = DEFAULT_RUNNER, + ) -> None: + self.config = config or Config(quiet=True) + self.manager = ( + manager_cls(repo_root) if repo_root is not None else manager_cls() + ) + self.compiler_service_cls = compiler_service_cls + self.runner = runner + + def refresh(self, options: Union[bool, VersionOptions] = False) -> bool: + """Refresh or preview local gitinfo2 metadata.""" + if isinstance(options, bool): + return self.manager.refresh_version_metadata(options) + + if not self.manager.refresh_version_metadata(options.dry_run): + return False + + if options.dry_run: + self._print_dry_run(options) + return True + + if options.compile_pdf: + compiler = self.compiler_service_cls( + self.config, cwd=self.manager.repo_root + ) + if not compiler.compile( + CompileOptions( + document=options.document, + engine=options.engine, + output_dir=options.output_dir, + shell_escape=options.shell_escape, + ) + ): + return False + + if options.check_pdf: + expected_text = options.tag or self._current_description() + if not expected_text: + print_error("Could not determine a version string for PDF checking.") + return False + pdf_path = self._expected_pdf_path(options) + if not pdf_contains_text(pdf_path, expected_text, self.runner): + return False + + if options.checksum: + pdf_path = self._expected_pdf_path(options) + checksum_path = write_sha256(pdf_path) + print_success(f"Wrote checksum: {checksum_path}") + + return True + + def _print_dry_run(self, options: VersionOptions) -> None: + """Print additional dry-run actions for the version workflow.""" + if options.compile_pdf: + print_info("Would compile the document after refreshing metadata.") + if options.check_pdf: + expected = options.tag or self._current_description() or "" + print_info(f"Would verify PDF text contains: {expected}") + if options.checksum: + print_info("Would write a PDF sha256 checksum sidecar.") + + def _expected_pdf_path(self, options: VersionOptions) -> Path: + """Resolve the expected PDF path for version checks.""" + context = ProjectContext.resolve( + self.config, + cwd=self.manager.repo_root, + document=options.document, + engine=options.engine, + output_dir=options.output_dir, + shell_escape=options.shell_escape, + ) + pdf_path = _absolute_pdf_path(context) + if pdf_path is None: + raise ValueError("No PDF path could be resolved for version checking.") + return pdf_path + + def _current_description(self) -> Optional[str]: + """Return the current git describe string.""" + result = self.manager.git( + ["describe", "--tags", "--long", "--always", "--dirty=-*"] + ) + if result.returncode != 0: + return None + return result.stdout.strip() or None diff --git a/src/article_cli/services/release.py b/src/article_cli/services/release.py new file mode 100644 index 0000000..43c14a2 --- /dev/null +++ b/src/article_cli/services/release.py @@ -0,0 +1,564 @@ +""" +Transactional release service. +""" + +from __future__ import annotations + +import hashlib +import shutil +from dataclasses import dataclass +from pathlib import Path +from types import SimpleNamespace +from typing import Optional, Sequence, Type + +from ..command_runner import DEFAULT_RUNNER, CommandRunner +from ..config import Config +from ..git_hooks import gitinfo2_metadata_summary, refresh_gitinfo2_metadata +from ..git_manager import GitManager +from ..project_context import ProjectContext +from ..reporting import print_error, print_info, print_success, print_warning +from .bibliography import BibliographyService, BibliographyUpdateOptions +from .compiler import CompileOptions, CompilerService + + +@dataclass(frozen=True) +class ReleaseOptions: + """User-facing release workflow options.""" + + tag: str + auto_push: bool = False + dry_run: bool = False + force: bool = False + commit: bool = False + compile_pdf: Optional[bool] = None + check_pdf: Optional[bool] = None + bibliography: Optional[str] = None + github_release: Optional[bool] = None + checksum: Optional[bool] = None + allow_dirty: Optional[bool] = None + tag_policy: Optional[str] = None + document: Optional[str] = None + engine: Optional[str] = None + output_dir: Optional[str] = None + shell_escape: Optional[bool] = None + + +@dataclass(frozen=True) +class ResolvedReleaseOptions: + """Release options after config defaults are applied.""" + + tag: str + auto_push: bool + dry_run: bool + force: bool + commit: bool + compile_pdf: bool + check_pdf: bool + bibliography: str + github_release: bool + checksum: bool + allow_dirty: bool + tag_policy: str + document: Optional[str] + engine: Optional[str] + output_dir: Optional[str] + shell_escape: Optional[bool] + + +class ReleaseService: + """Service boundary for tag-based paper releases.""" + + def __init__( + self, + config: Optional[Config] = None, + repo_root: Optional[Path] = None, + manager_cls: Type[GitManager] = GitManager, + compiler_service_cls: Type[CompilerService] = CompilerService, + bibliography_service_cls: Type[BibliographyService] = BibliographyService, + runner: CommandRunner = DEFAULT_RUNNER, + ) -> None: + self.config = config or Config(quiet=True) + self.manager = ( + manager_cls(repo_root) if repo_root is not None else manager_cls() + ) + self.compiler_service_cls = compiler_service_cls + self.bibliography_service_cls = bibliography_service_cls + self.runner = runner + + def create( + self, + version: str, + auto_push: bool = False, + dry_run: bool = False, + **kwargs: object, + ) -> bool: + """Backward-compatible release entrypoint.""" + return self.release( + ReleaseOptions( + tag=version, + auto_push=auto_push, + dry_run=dry_run, + force=bool(kwargs.get("force", False)), + commit=bool(kwargs.get("commit", False)), + compile_pdf=_optional_bool(kwargs.get("compile_pdf")), + check_pdf=_optional_bool(kwargs.get("check_pdf")), + bibliography=_optional_str(kwargs.get("bibliography")), + github_release=_optional_bool(kwargs.get("github_release")), + checksum=_optional_bool(kwargs.get("checksum")), + allow_dirty=_optional_bool(kwargs.get("allow_dirty")), + tag_policy=_optional_str(kwargs.get("tag_policy")), + document=_optional_str(kwargs.get("document")), + engine=_optional_str(kwargs.get("engine")), + output_dir=_optional_str(kwargs.get("output_dir")), + shell_escape=_optional_bool(kwargs.get("shell_escape")), + ) + ) + + def release(self, options: ReleaseOptions) -> bool: + """Create a checked paper release.""" + resolved = self._resolve_options(options) + if not self._validate_tag(resolved.tag, resolved.tag_policy): + return False + if not self._preflight(resolved): + return False + + context = ProjectContext.resolve( + self.config, + cwd=self.manager.repo_root, + document=resolved.document, + engine=resolved.engine, + output_dir=resolved.output_dir, + shell_escape=resolved.shell_escape, + ) + pdf_path = _absolute_pdf_path(context) + + if resolved.dry_run: + self._print_dry_run(resolved, pdf_path) + return True + + if resolved.bibliography in {"check", "update"}: + if not self._run_bibliography(resolved.bibliography): + return False + + if not self._refresh_metadata(): + return False + + if resolved.commit and not self._commit_metadata(resolved.tag): + return False + + if resolved.compile_pdf and not self._compile(resolved): + return False + + if resolved.compile_pdf and (pdf_path is None or not pdf_path.exists()): + print_error("Release PDF was not produced before tagging.") + return False + + created_tag = False + try: + self.manager.create_tag(resolved.tag, force=resolved.force) + created_tag = True + print_success(f"Created release tag: {resolved.tag}") + + if not self._refresh_metadata(): + self._print_rollback(resolved.tag) + return False + + if resolved.compile_pdf and not self._compile(resolved): + self._print_rollback(resolved.tag) + return False + + if resolved.check_pdf and not self._check_pdf(pdf_path, resolved.tag): + self._print_rollback(resolved.tag) + return False + + checksum_path = None + if resolved.checksum and pdf_path is not None: + checksum_path = write_sha256(pdf_path) + print_success(f"Wrote checksum: {checksum_path}") + + if resolved.github_release: + assets = [ + path for path in [pdf_path, checksum_path] if path is not None + ] + if not self._create_github_release(resolved.tag, assets): + self._print_rollback(resolved.tag) + return False + + if resolved.auto_push: + self.manager.push_tag(resolved.tag) + print_success(f"Pushed release tag: {resolved.tag}") + else: + print_info(f"Push with: git push origin {resolved.tag}") + + self._print_summary(resolved, pdf_path, checksum_path) + return True + + except Exception as e: + print_error(f"Release failed: {e}") + if created_tag: + self._print_rollback(resolved.tag) + return False + + def list(self, count: int = 5) -> bool: + """List release tags.""" + return self.manager.list_releases(count) + + def delete(self, version: str, remote: bool = False) -> bool: + """Delete a release tag.""" + return self.manager.delete_release(version, remote) + + def _resolve_options(self, options: ReleaseOptions) -> ResolvedReleaseOptions: + """Apply config defaults to release options.""" + release_config = self.config.get_release_config() + return ResolvedReleaseOptions( + tag=options.tag, + auto_push=options.auto_push, + dry_run=options.dry_run, + force=options.force, + commit=options.commit, + compile_pdf=_config_bool(options.compile_pdf, release_config["compile"]), + check_pdf=_config_bool(options.check_pdf, release_config["check_pdf"]), + bibliography=str(options.bibliography or release_config["bibliography"]), + github_release=_config_bool( + options.github_release, + release_config["github_release"], + ), + checksum=_config_bool(options.checksum, release_config["checksum"]), + allow_dirty=_config_bool( + options.allow_dirty, release_config["allow_dirty"] + ), + tag_policy=str(options.tag_policy or release_config["tag_policy"]), + document=options.document, + engine=options.engine, + output_dir=options.output_dir, + shell_escape=options.shell_escape, + ) + + def _validate_tag(self, tag: str, policy: str) -> bool: + """Validate the release tag according to configured policy.""" + if validate_tag(tag, policy): + return True + print_error(f"Invalid release tag for {policy!r} policy: {tag}") + if policy == "paper": + print_info("Expected examples: v1, v1.0, v1.0.0, v1.0.0-rc.1") + elif policy == "semver": + print_info("Expected format: vX.Y.Z with optional prerelease/build suffix") + return False + + def _preflight(self, options: ResolvedReleaseOptions) -> bool: + """Run checks that must pass before creating a tag.""" + if options.bibliography not in {"off", "check", "update"}: + print_error("Bibliography policy must be one of: off, check, update") + return False + + if self.manager.tag_exists(options.tag) and not options.force: + print_error(f"Tag already exists: {options.tag}") + print_info("Use --force to move an existing local tag explicitly.") + return False + + dirty_files = self.manager.dirty_files(ignore_gitinfo=True) + if dirty_files and not options.allow_dirty: + print_error("Tracked or untracked files are dirty.") + for dirty_file in dirty_files: + print_info(f" {dirty_file}") + print_info("Use --allow-dirty only for an intentional non-clean release.") + return False + + return True + + def _run_bibliography(self, policy: str) -> bool: + """Run bibliography check/update according to release policy.""" + args = SimpleNamespace( + api_key=None, + user_id=None, + group_id=None, + collection=None, + output=None, + local_file=None, + merged_output=None, + ) + options = BibliographyUpdateOptions( + no_backup=policy == "check", + check=policy == "check", + ) + return self.bibliography_service_cls(self.config).update(args, options) + + def _refresh_metadata(self) -> bool: + """Refresh gitinfo2 metadata and print the current summary.""" + if refresh_gitinfo2_metadata(self.manager.repo_root): + summary = gitinfo2_metadata_summary(self.manager.repo_root) + if summary: + print_info(f"Version metadata: {summary}") + return True + print_error("Could not refresh gitinfo2 metadata.") + return False + + def _commit_metadata(self, tag: str) -> bool: + """Commit gitHeadLocal.gin only when explicitly requested.""" + metadata_path = self.manager.repo_root / "gitHeadLocal.gin" + if not metadata_path.exists(): + print_warning("gitHeadLocal.gin does not exist; nothing to commit.") + return True + self.manager.commit_paths( + ["gitHeadLocal.gin"], + f"Update gitinfo2 metadata before release {tag}", + ) + return True + + def _compile(self, options: ResolvedReleaseOptions) -> bool: + """Compile the configured document.""" + compiler = self.compiler_service_cls(self.config, cwd=self.manager.repo_root) + return compiler.compile( + CompileOptions( + document=options.document, + engine=options.engine, + output_dir=options.output_dir, + shell_escape=options.shell_escape, + ) + ) + + def _check_pdf(self, pdf_path: Optional[Path], tag: str) -> bool: + """Check that the generated PDF text contains the requested tag.""" + if pdf_path is None: + print_error("No PDF path could be resolved for release verification.") + return False + return pdf_contains_text(pdf_path, tag, self.runner) + + def _create_github_release(self, tag: str, assets: Sequence[Path]) -> bool: + """Create a GitHub release through gh when explicitly requested.""" + if shutil.which("gh") is None: + print_error("gh is required for --github-release but was not found.") + return False + command = ["gh", "release", "create", tag, "--generate-notes"] + command.extend(str(path) for path in assets if path.exists()) + result = self.runner.run(command, cwd=self.manager.repo_root) + if result.returncode == 0: + print_success(f"Created GitHub release: {tag}") + return True + print_error(result.stderr.strip() or "GitHub release creation failed") + return False + + def _print_dry_run( + self, options: ResolvedReleaseOptions, pdf_path: Optional[Path] + ) -> None: + """Print the release plan without modifying files.""" + print_info("Dry run: no tags, commits, builds, or release files were changed.") + print_info(f"Would create tag: {options.tag}") + self._print_git_diagnostics(label="Current") + if options.force: + print_info("Would move existing local tag because --force is set.") + if options.bibliography != "off": + print_info(f"Would run bibliography policy: {options.bibliography}") + if options.compile_pdf: + print_info("Would compile before and after tagging.") + if options.check_pdf: + print_info(f"Would verify PDF text contains: {options.tag}") + if options.checksum and pdf_path is not None: + print_info(f"Would write checksum for: {pdf_path}") + if options.github_release: + print_info("Would create a GitHub release with gh.") + if options.auto_push: + print_info(f"Would push tag: {options.tag}") + + def _print_summary( + self, + options: ResolvedReleaseOptions, + pdf_path: Optional[Path], + checksum_path: Optional[Path], + ) -> None: + """Print auditable release summary.""" + print_success(f"Release {options.tag} is ready.") + print_info(f"Repository: {self.manager.repo_root}") + self._print_git_diagnostics(label="Release") + self._print_release_assets(pdf_path, checksum_path) + + def _print_rollback(self, tag: str) -> None: + """Print rollback guidance for a local tag created by this command.""" + print_info(f"Rollback local tag with: git tag -d {tag}") + + def _print_git_diagnostics(self, label: str) -> None: + """Print git state and dirty state without mutating the repository.""" + branch = self._git_output(["branch", "--show-current"]) + commit = self._git_output(["rev-parse", "--short", "HEAD"]) + describe = self._git_output( + ["describe", "--tags", "--long", "--always", "--dirty=-*"] + ) + exact_tag = self._git_output(["describe", "--tags", "--exact-match"]) + + if branch or commit or describe: + parts = [] + if branch: + parts.append(f"branch {branch}") + if commit: + parts.append(f"commit {commit}") + if describe: + parts.append(f"describe {describe}") + print_info(f"{label} git state: {'; '.join(parts)}") + if exact_tag: + print_info(f"{label} tag: {exact_tag}") + + dirty_files = self._dirty_files() + if not dirty_files: + print_info(f"{label} dirty files: none") + return + + print_warning(f"{label} dirty files: {len(dirty_files)}") + for dirty_file in dirty_files[:5]: + print_info(f" {dirty_file}") + if len(dirty_files) > 5: + print_info(f" ... and {len(dirty_files) - 5} more") + + def _print_release_assets( + self, + pdf_path: Optional[Path], + checksum_path: Optional[Path], + ) -> None: + """Print release asset paths and verifiable metadata.""" + assets = [path for path in [pdf_path, checksum_path] if path is not None] + if not assets: + print_info("Release assets: none") + return + + print_info("Release assets:") + if pdf_path is not None: + print_info(f" PDF: {pdf_path}") + self._print_pdf_metadata(pdf_path) + if checksum_path is not None: + print_info(f" Checksum: {checksum_path}") + digest = self._checksum_digest(checksum_path) + if digest: + print_info(f" SHA256: {digest}") + + def _print_pdf_metadata(self, pdf_path: Path) -> None: + """Print basic PDF metadata useful for release checks.""" + if not pdf_path.exists(): + print_warning(f" PDF missing: {pdf_path}") + return + + size_mb = pdf_path.stat().st_size / (1024 * 1024) + print_info(f" PDF size: {size_mb:.2f} MB") + pages = self._pdf_page_count(pdf_path) + if pages is not None: + print_info(f" PDF pages: {pages}") + + def _pdf_page_count(self, pdf_path: Path) -> Optional[str]: + """Return PDF page count through pdfinfo when available.""" + if shutil.which("pdfinfo") is None: + return None + try: + result = self.runner.run(["pdfinfo", str(pdf_path)], timeout=10) + except Exception: + return None + if result.returncode != 0: + return None + stdout = str(result.stdout or "") + for line in stdout.splitlines(): + if line.startswith("Pages:"): + page_count = line.split(":", 1)[1].strip() + return page_count or None + return None + + def _checksum_digest(self, checksum_path: Path) -> Optional[str]: + """Return the digest recorded in a sha256 sidecar file.""" + if not checksum_path.exists(): + return None + first_line = checksum_path.read_text(errors="replace").splitlines()[0:1] + if not first_line: + return None + digest = first_line[0].split()[0] + return digest or None + + def _git_output(self, args: Sequence[str]) -> Optional[str]: + """Run a git command through the manager when supported.""" + git = getattr(self.manager, "git", None) + if git is None: + return None + try: + result = git(list(args)) + except Exception: + return None + if result.returncode != 0: + return None + output = str(result.stdout or "").strip() + return output or None + + def _dirty_files(self) -> Sequence[str]: + """Return dirty files through the manager when supported.""" + dirty_files = getattr(self.manager, "dirty_files", None) + if dirty_files is None: + return [] + try: + return [str(path) for path in dirty_files(ignore_gitinfo=True)] + except Exception: + return [""] + + +def validate_tag(tag: str, policy: str = "paper") -> bool: + """Validate tag names for paper/software release policies.""" + import re + + patterns = { + "paper": r"^v\d+(?:\.\d+){0,2}(?:[-._]?(?:alpha|beta|rc|pre|preview)\.?\d*)?$", + "semver": ( + r"^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)" + r"(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$" + ), + "loose": r"^\S+$", + } + pattern = patterns.get(policy) + return bool(pattern and re.match(pattern, tag)) + + +def pdf_contains_text(pdf_path: Path, text: str, runner: CommandRunner) -> bool: + """Return whether extracted PDF text contains a requested string.""" + if not pdf_path.exists(): + print_error(f"Expected PDF is missing: {pdf_path}") + return False + if shutil.which("pdftotext") is None: + print_error("pdftotext is required for PDF version verification.") + return False + + result = runner.run(["pdftotext", str(pdf_path), "-"]) + if result.returncode != 0: + print_error(result.stderr.strip() or "Could not extract PDF text") + return False + if text not in result.stdout: + print_error(f"PDF text does not contain release tag {text}.") + print_info("Rebuild the PDF after tagging, or disable with --no-pdf-check.") + return False + + print_success(f"PDF text contains release tag {text}") + return True + + +def write_sha256(path: Path) -> Path: + """Write a sha256 sidecar file and return its path.""" + digest = hashlib.sha256(path.read_bytes()).hexdigest() + checksum_path = path.with_name(path.name + ".sha256") + checksum_path.write_text(f"{digest} {path.name}\n", encoding="utf-8") + return checksum_path + + +def _absolute_pdf_path(context: ProjectContext) -> Optional[Path]: + """Return expected PDF path as an absolute path.""" + pdf_path = context.expected_pdf_path() + if pdf_path is None: + return None + if pdf_path.is_absolute(): + return pdf_path + return (context.project_root / pdf_path).resolve() + + +def _config_bool(value: Optional[bool], default: object) -> bool: + """Resolve optional boolean CLI value against config default.""" + return bool(default) if value is None else bool(value) + + +def _optional_bool(value: object) -> Optional[bool]: + """Coerce a compatibility keyword into an optional boolean.""" + return None if value is None else bool(value) + + +def _optional_str(value: object) -> Optional[str]: + """Coerce a compatibility keyword into an optional string.""" + return None if value is None else str(value) diff --git a/src/article_cli/services/workflow.py b/src/article_cli/services/workflow.py new file mode 100644 index 0000000..54720cf --- /dev/null +++ b/src/article_cli/services/workflow.py @@ -0,0 +1,68 @@ +""" +Repository workflow setup service. +""" + +from pathlib import Path +from typing import List, Optional, Type + +from ..repository_setup import RepositorySetup + + +class WorkflowService: + """Service boundary for generated repository workflow/config setup.""" + + def __init__( + self, + repo_path: Optional[Path] = None, + setup_cls: Type[RepositorySetup] = RepositorySetup, + ) -> None: + self.setup = setup_cls(repo_path) if repo_path is not None else setup_cls() + + def initialize_repository( + self, + title: str, + authors: List[str], + group_id: str = "4678293", + force: bool = False, + main_tex_file: Optional[str] = None, + project_type: str = "article", + theme: str = "", + aspect_ratio: str = "169", + style: str = "default", + template: str = "", + additional_documents: Optional[List[str]] = None, + output_dir: str = "", + fonts_dir: str = "", + install_fonts: bool = False, + ci_bibliography: str = "off", + ci_runner_policy: str = "github", + ci_github_runner: str = "ubuntu-24.04", + ci_self_hosted_label: str = "self-texlive", + ci_self_hosted_org: str = "", + ci_release_policy: str = "github", + ci_artifact_includes: Optional[List[str]] = None, + ) -> bool: + """Initialize a new article repository.""" + return self.setup.init_repository( + title=title, + authors=authors, + group_id=group_id, + force=force, + main_tex_file=main_tex_file, + project_type=project_type, + theme=theme, + aspect_ratio=aspect_ratio, + style=style, + template=template, + additional_documents=additional_documents, + output_dir=output_dir, + fonts_dir=fonts_dir, + install_fonts=install_fonts, + ci_bibliography=ci_bibliography, + ci_runner_policy=ci_runner_policy, + ci_github_runner=ci_github_runner, + ci_self_hosted_label=ci_self_hosted_label, + ci_self_hosted_org=ci_self_hosted_org, + ci_release_policy=ci_release_policy, + ci_artifact_includes=ci_artifact_includes, + ) diff --git a/src/article_cli/template_renderer.py b/src/article_cli/template_renderer.py new file mode 100644 index 0000000..9f9da8d --- /dev/null +++ b/src/article_cli/template_renderer.py @@ -0,0 +1,70 @@ +""" +Template rendering helpers for generated article repositories. +""" + +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Mapping + +from jinja2 import Environment, FileSystemLoader, PackageLoader, StrictUndefined + +TEMPLATE_VERSION = "1" + + +@dataclass(frozen=True) +class TemplateWriteResult: + """Result of rendering a template to disk.""" + + path: Path + status: str + + +class TemplateRenderer: + """Render package templates with strict, explicit context.""" + + def __init__(self) -> None: + self.env = Environment( + loader=PackageLoader("article_cli", "templates"), + undefined=StrictUndefined, + autoescape=False, + keep_trailing_newline=True, + trim_blocks=True, + lstrip_blocks=True, + variable_start_string="[[", + variable_end_string="]]", + block_start_string="[%", + block_end_string="%]", + comment_start_string="[#", + comment_end_string="#]", + ) + + def render(self, template_name: str, context: Mapping[str, Any]) -> str: + """Render a template from package resources.""" + template = self.env.get_template(template_name) + return template.render(**context) + + def render_path(self, template_path: Path, context: Mapping[str, Any]) -> str: + """Render a user-supplied template from the filesystem.""" + template_path = template_path.expanduser().resolve() + if not template_path.exists(): + raise FileNotFoundError(f"Template not found: {template_path}") + + env = self.env.overlay(loader=FileSystemLoader(str(template_path.parent))) + template = env.get_template(template_path.name) + return template.render(**context) + + def write( + self, + template_name: str, + destination: Path, + context: Mapping[str, Any], + force: bool = False, + ) -> TemplateWriteResult: + """Render a template to a file using a simple overwrite policy.""" + if destination.exists() and not force: + return TemplateWriteResult(destination, "skipped") + + existed = destination.exists() + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(self.render(template_name, context), encoding="utf-8") + return TemplateWriteResult(destination, "overwritten" if existed else "created") diff --git a/src/article_cli/templates/__init__.py b/src/article_cli/templates/__init__.py new file mode 100644 index 0000000..68e0f39 --- /dev/null +++ b/src/article_cli/templates/__init__.py @@ -0,0 +1 @@ +"""Package resources for article-cli templates.""" diff --git a/src/article_cli/templates/article/ieee.tex.j2 b/src/article_cli/templates/article/ieee.tex.j2 new file mode 100644 index 0000000..4787afe --- /dev/null +++ b/src/article_cli/templates/article/ieee.tex.j2 @@ -0,0 +1,54 @@ +\documentclass[conference]{IEEEtran} + +\usepackage[T1]{fontenc} +\usepackage{cite} +\usepackage{amsmath,amssymb} +\usepackage{graphicx} +\usepackage{hyperref} +\usepackage{gitinfo2} + +\title{[[ title ]]} +\author{[[ authors_latex ]]} + +\begin{document} + +\maketitle + +\begin{abstract} +Your abstract goes here. +\end{abstract} + +\begin{IEEEkeywords} +Keyword one, keyword two, keyword three +\end{IEEEkeywords} + +\section{Introduction} + +Your introduction goes here. + +\section{Methodology} + +Your methodology goes here. + +\section{Results} + +Your results go here. + +\section{Conclusion} + +Your conclusion goes here. + +\section*{Acknowledgements} + +Add acknowledgements here. + +\bibliographystyle{IEEEtran} +\bibliography{references} + +\vfill +\hrule +\small +\noindent Git version: \gitAbbrevHash{} (\gitAuthorIsoDate) \\ +Branch: \gitBranch + +\end{document} diff --git a/src/article_cli/templates/article/lncs.tex.j2 b/src/article_cli/templates/article/lncs.tex.j2 new file mode 100644 index 0000000..eca01fd --- /dev/null +++ b/src/article_cli/templates/article/lncs.tex.j2 @@ -0,0 +1,51 @@ +\documentclass[runningheads]{llncs} + +\usepackage[T1]{fontenc} +\usepackage{graphicx} +\usepackage{amsmath,amssymb} +\usepackage{hyperref} +\usepackage{gitinfo2} + +\title{[[ title ]]} +\author{[[ authors_latex ]]} +\institute{Your Institution} + +\begin{document} + +\maketitle + +\begin{abstract} +Your abstract goes here. +\keywords{Keyword one \and Keyword two \and Keyword three} +\end{abstract} + +\section{Introduction} + +Your introduction goes here. + +\section{Methodology} + +Your methodology goes here. + +\section{Results} + +Your results go here. + +\section{Conclusion} + +Your conclusion goes here. + +\subsubsection*{Acknowledgements} + +Add acknowledgements here. + +\bibliographystyle{splncs04} +\bibliography{references} + +\vfill +\hrule +\small +\noindent Git version: \gitAbbrevHash{} (\gitAuthorIsoDate) \\ +Branch: \gitBranch + +\end{document} diff --git a/src/article_cli/templates/article/lncs.typ.j2 b/src/article_cli/templates/article/lncs.typ.j2 new file mode 100644 index 0000000..4ca2ece --- /dev/null +++ b/src/article_cli/templates/article/lncs.typ.j2 @@ -0,0 +1,69 @@ +// Typst Article, LNCS-oriented skeleton +// Title: [[ title ]] +// Authors: [[ authors_display ]] +// +// Replace this skeleton with the exact Typst package or house template required +// by the target venue when one is mandated. + +#set document( + title: "[[ title ]]", + author: ( +[% for author in authors %] + "[[ author ]]", +[% endfor %] + ), +) + +#set page( + paper: "a4", + margin: (x: 3.0cm, y: 3.0cm), + numbering: "1", +) + +#set text(font: "Libertinus Serif", size: 10pt) +#set heading(numbering: "1.") +#show heading.where(level: 1): it => block(above: 1.0em, below: 0.4em, strong(it)) + +#align(center)[ + #text(size: 16pt, weight: "bold")[ [[ title ]] ] + + #v(0.5cm) + + [[ authors_display ]] + + #v(0.4cm) + + Your Institution +] + +#v(0.7cm) + +#heading(numbering: none)[Abstract] + +Your abstract goes here. + +#heading(numbering: none)[Keywords] + +Keyword one; keyword two; keyword three. + += Introduction + +Your introduction goes here. + += Methodology + +Your methodology goes here. + += Results + +Your results go here. + += Conclusion + +Your conclusion goes here. + +#heading(numbering: none)[Acknowledgements] + +Add acknowledgements here. + +#bibliography("references.bib", style: "springer-basic") diff --git a/src/article_cli/templates/article/main.tex.j2 b/src/article_cli/templates/article/main.tex.j2 new file mode 100644 index 0000000..80c9631 --- /dev/null +++ b/src/article_cli/templates/article/main.tex.j2 @@ -0,0 +1,58 @@ +\documentclass[a4paper,11pt]{article} + +% Essential packages +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage{lmodern} +\usepackage{amsmath,amssymb,amsthm} +\usepackage{graphicx} +\usepackage{hyperref} +\usepackage[margin=1in]{geometry} + +% Bibliography +\usepackage[style=numeric,sorting=none]{biblatex} +\addbibresource{references.bib} + +% Git version information +\usepackage{gitinfo2} + +% Title and authors +\title{[[ title ]]} +\author{[[ authors_latex ]]} +\date{\today} + +\begin{document} + +\maketitle + +\begin{abstract} + Your abstract goes here. +\end{abstract} + +\section{Introduction} + +Your introduction goes here. + +\section{Methodology} + +Your methodology goes here. + +\section{Results} + +Your results go here. + +\section{Conclusion} + +Your conclusion goes here. + +% Print bibliography +\printbibliography + +% Git information (optional - appears in footer) +\vfill +\hrule +\small +\noindent Git version: \gitAbbrevHash{} (\gitAuthorIsoDate) \\ +Branch: \gitBranch + +\end{document} diff --git a/src/article_cli/templates/article/main.typ.j2 b/src/article_cli/templates/article/main.typ.j2 new file mode 100644 index 0000000..38aa61b --- /dev/null +++ b/src/article_cli/templates/article/main.typ.j2 @@ -0,0 +1,57 @@ +// Typst Article +// Title: [[ title ]] +// Authors: [[ authors_display ]] + +#set document( + title: "[[ title ]]", + author: ( +[% for author in authors %] + "[[ author ]]", +[% endfor %] + ), +) + +#set page( + paper: "a4", + margin: (x: 2.5cm, y: 2.5cm), + numbering: "1", +) + +#set text(font: "Libertinus Serif", size: 11pt) +#set heading(numbering: "1.") + +#align(center)[ + #text(size: 18pt, weight: "bold")[ [[ title ]] ] + + #v(0.6cm) + + [[ authors_display ]] + + #v(0.6cm) + + #datetime.today().display("[month repr:long] [day], [year]") +] + +#v(0.8cm) + +#heading(numbering: none)[Abstract] + +Your abstract goes here. + += Introduction + +Your introduction goes here. + += Methodology + +Your methodology goes here. + += Results + +Your results go here. + += Conclusion + +Your conclusion goes here. + +#bibliography("references.bib", style: "ieee") diff --git a/src/article_cli/templates/github/latex.yml.j2 b/src/article_cli/templates/github/latex.yml.j2 new file mode 100644 index 0000000..f1f3d1c --- /dev/null +++ b/src/article_cli/templates/github/latex.yml.j2 @@ -0,0 +1,407 @@ +name: Compile Document and Release PDF + +# Generated by article-cli template version [[ template_version ]]. +# Project type: [[ project_type ]] +# Engine: [[ latex_engine_label ]] +# Output directory: [[ output_dir_label ]] +# Additional documents: [[ additional_documents_label ]] +# Runner policy: [[ runner_policy ]] +# Bibliography policy: [[ bibliography_policy ]] +# Release policy: [[ release_policy ]] + +on: + push: + tags: + - 'v*' + branches: + - 'main' + pull_request: + branches: + - 'main' + +jobs: + workflow-setup: + name: Workflow Setup + runs-on: [[ github_runner ]] + outputs: + runner: ${{ steps.texlive_runner.outputs.runner }} + prefix: ${{ steps.doc_prefix.outputs.prefix }} + prefixwithref: ${{ steps.doc_prefix.outputs.prefixwithref }} + pdf: ${{ steps.doc_prefix.outputs.pdf }} + tex: ${{ steps.doc_prefix.outputs.tex }} + steps: + - name: Resolve CI runner + id: texlive_runner + run: | +[% if runner_policy == "self-hosted-auto" and not is_typst %] + if [ -n "${GH_TOKEN:-}" ] && [ -n "[[ self_hosted_org ]]" ]; then + runners=$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /orgs/[[ self_hosted_org ]]/actions/runners) + texlive=$(echo "$runners" | jq --arg label "[[ self_hosted_label ]]" '[.runners[] | any(.labels[]; .name == $label) and .status == "online"] | any') + if [ "$texlive" = "true" ]; then + echo "runner=[[ self_hosted_label ]]" >> "$GITHUB_OUTPUT" + else + echo "runner=[[ github_runner ]]" >> "$GITHUB_OUTPUT" + fi + else + echo "runner=[[ github_runner ]]" >> "$GITHUB_OUTPUT" + fi +[% elif runner_policy == "self-hosted" and not is_typst %] + echo "runner=[[ self_hosted_label ]]" >> "$GITHUB_OUTPUT" +[% else %] + echo "runner=[[ github_runner ]]" >> "$GITHUB_OUTPUT" +[% endif %] +[% if runner_policy == "self-hosted-auto" and not is_typst %] + env: + GH_TOKEN: ${{ secrets.TOKEN_RUNNER }} + +[% endif %] + - name: Get Document Prefix + id: doc_prefix + run: | + prefix=$(echo "${{ github.repository }}" | cut -d'/' -f2) + echo "prefix=$prefix" >> "$GITHUB_OUTPUT" + + if [ "${{ github.event_name }}" = "pull_request" ]; then + prefixwithref="${prefix}-pr-${{ github.event.number }}" + else + prefixwithref="${prefix}-${{ github.ref_name }}" + fi + + echo "prefixwithref=$prefixwithref" >> "$GITHUB_OUTPUT" + echo "pdf=$prefixwithref.pdf" >> "$GITHUB_OUTPUT" + echo "tex=[[ tex_file ]]" >> "$GITHUB_OUTPUT" + + - name: Show Outputs + run: | + echo "runner=${{ steps.texlive_runner.outputs.runner }}" + echo "prefix=${{ steps.doc_prefix.outputs.prefix }}" + echo "prefixwithref=${{ steps.doc_prefix.outputs.prefixwithref }}" + echo "pdf=${{ steps.doc_prefix.outputs.pdf }}" + echo "tex=${{ steps.doc_prefix.outputs.tex }}" + + build_document: + needs: workflow-setup + runs-on: ${{ needs.workflow-setup.outputs.runner }} + name: Build Document Artifact + env: + VERSION: ${{ github.ref_name }} + steps: + - name: Set up Git repository + uses: actions/checkout@v4 + with: + clean: true + + - name: Set up Python and uv + uses: astral-sh/setup-uv@v8.1.0 + with: + version: "latest" + enable-cache: false + + - name: Set up Python + run: uv python install 3.11 + + - name: Create virtual environment and install article-cli + run: | + uv venv .venv --python 3.11 + echo "VIRTUAL_ENV=${PWD}/.venv" >> "$GITHUB_ENV" + echo "${PWD}/.venv/bin" >> "$GITHUB_PATH" + uv pip install "article-cli>=[[ article_cli_min_version ]]" + + - name: Install hooks and setup + run: | + article-cli setup + if [ "${{ github.event_name }}" != "pull_request" ]; then + git checkout "${{ github.ref }}" + fi + + - name: Refresh gitinfo2 metadata + run: article-cli version + + - name: Show article-cli configuration + run: | + article-cli --version + article-cli config show + + - name: Run article-cli doctor diagnostics + continue-on-error: true + run: | + set +e + article-cli doctor --json > article-cli-doctor.json + doctor_status=$? + set -e + python -m json.tool article-cli-doctor.json + echo "doctor_status=$doctor_status" >> "$GITHUB_STEP_SUMMARY" + + - name: Upload Doctor Diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: article-cli-doctor-${{ needs.workflow-setup.outputs.prefixwithref }} + path: article-cli-doctor.json + if-no-files-found: ignore + +[% if bibliography_policy == "check" %] + - name: Check bibliography from Zotero + env: + ZOTERO_API_KEY: ${{ secrets.ZOTERO_API_KEY }} + run: | + if [ -n "${ZOTERO_API_KEY:-}" ]; then + article-cli bib update --check + else + echo "ZOTERO_API_KEY is not set; skipping optional bibliography check." + fi + +[% elif bibliography_policy == "update" %] + - name: Update bibliography from Zotero + env: + ZOTERO_API_KEY: ${{ secrets.ZOTERO_API_KEY }} + run: | + if [ -n "${ZOTERO_API_KEY:-}" ]; then + article-cli bib update + else + echo "ZOTERO_API_KEY is not set; using the checked-in bibliography." + fi + +[% elif bibliography_policy == "required" %] + - name: Update required bibliography from Zotero + env: + ZOTERO_API_KEY: ${{ secrets.ZOTERO_API_KEY }} + run: | + if [ -z "${ZOTERO_API_KEY:-}" ]; then + echo "ZOTERO_API_KEY is required by the generated CI bibliography policy." + exit 1 + fi + article-cli bib update + +[% endif %] +[% if install_fonts and fonts_dir %] + - name: Install custom fonts + run: | + mkdir -p ~/.local/share/fonts + if [ -d "[[ fonts_dir ]]" ]; then + find [[ fonts_dir ]] -type f \( -name "*.ttf" -o -name "*.otf" -o -name "*.woff" -o -name "*.woff2" \) -exec cp {} ~/.local/share/fonts/ \; + fc-cache -f -v + else + echo "Font directory [[ fonts_dir ]] not found" + exit 1 + fi + +[% endif %] +[% if output_dir %] + - name: Create output directory + run: mkdir -p [[ output_dir ]] + +[% endif %] +[% if is_typst %] + - name: Set up Typst + uses: typst-community/setup-typst@v4 + + - name: Compile Typst document + run: article-cli compile "${{ needs.workflow-setup.outputs.tex }}" + +[% else %] +[% if runner_policy != "self-hosted" %] + - name: Compile LaTeX document + uses: xu-cheng/latex-action@v3 + if: ${{ needs.workflow-setup.outputs.runner == '[[ github_runner ]]' }} + with: + root_file: ${{ needs.workflow-setup.outputs.tex }} + latexmk_shell_escape: true + latexmk_use_xelatex: [[ latexmk_use_xelatex ]] +[% if outdir_arg %] + args: "[[ outdir_arg ]]" +[% endif %] + post_compile: "article-cli clean" + +[% endif %] +[% if runner_policy != "github" %] + - name: Compile LaTeX document + if: ${{ needs.workflow-setup.outputs.runner == '[[ self_hosted_label ]]' }} + run: | + latexmk -shell-escape [[ latexmk_args ]] [[ outdir_arg ]] -file-line-error -interaction=nonstopmode "${{ needs.workflow-setup.outputs.tex }}" + article-cli clean + +[% endif %] +[% for doc in additional_documents %] +[% if runner_policy != "self-hosted" %] + - name: Compile additional document ([[ doc.name ]]) + uses: xu-cheng/latex-action@v3 + if: ${{ needs.workflow-setup.outputs.runner == '[[ github_runner ]]' }} + with: + root_file: [[ doc.name ]] + latexmk_shell_escape: true + latexmk_use_xelatex: [[ latexmk_use_xelatex ]] +[% if outdir_arg %] + args: "[[ outdir_arg ]]" +[% endif %] + +[% endif %] +[% if runner_policy != "github" %] + - name: Compile additional document ([[ doc.name ]]) + if: ${{ needs.workflow-setup.outputs.runner == '[[ self_hosted_label ]]' }} + run: | + latexmk -shell-escape [[ latexmk_args ]] [[ outdir_arg ]] -file-line-error -interaction=nonstopmode "[[ doc.name ]]" + +[% endif %] + - name: Stage additional PDF ([[ doc.pdf ]]) + run: cp "[[ pdf_location ]][[ doc.base ]].pdf" "[[ doc.pdf ]]" + +[% endfor %] +[% endif %] + - name: Rename PDF + run: mv "[[ pdf_location ]][[ source_base ]].pdf" "${{ needs.workflow-setup.outputs.pdf }}" + + - name: Upload PDF Artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ needs.workflow-setup.outputs.pdf }} + path: ${{ needs.workflow-setup.outputs.pdf }} + + - name: Upload Full Artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ needs.workflow-setup.outputs.prefixwithref }} + path: | + ./*.tex + ./*.typ + ./*.bib + ./*.sty + ./*.cls + ./*.gin + ./*.bbl + ./*.tikz + ./article-cli-doctor.json + ./${{ needs.workflow-setup.outputs.pdf }} +[% for doc in additional_documents %] + ./[[ doc.pdf ]] +[% endfor %] +[% for path in artifact_includes %] + [[ path ]] +[% endfor %] + ./README.md + ./fig-* + ./data/* +[% if fonts_dir %] + ./[[ fonts_dir ]]/* +[% endif %] + !./.git* + !./.github* + !./.vscode* + !./.idea* + !./.DS_Store* + !./.gitignore* + +[% if release_policy == "github" %] + check: + needs: [build_document, workflow-setup] + runs-on: ${{ needs.workflow-setup.outputs.runner }} + name: Check Document Artifact + steps: + - name: Download Artifact + uses: actions/download-artifact@v4 + with: + name: ${{ needs.workflow-setup.outputs.prefixwithref }} + path: ${{ github.workspace }}/artifact + +[% if is_typst %] + - name: Set up Typst + uses: typst-community/setup-typst@v4 + + - name: Check compilation of Typst document from artifact + run: typst compile "${{ needs.workflow-setup.outputs.tex }}" + working-directory: ${{ github.workspace }}/artifact + +[% else %] +[% if runner_policy != "self-hosted" %] + - name: Check compilation of LaTeX document from artifact + if: ${{ needs.workflow-setup.outputs.runner == '[[ github_runner ]]' }} + uses: xu-cheng/latex-action@v3 + with: + root_file: ${{ needs.workflow-setup.outputs.tex }} + latexmk_shell_escape: true + latexmk_use_xelatex: [[ latexmk_use_xelatex ]] + working_directory: ${{ github.workspace }}/artifact + +[% endif %] +[% if runner_policy != "github" %] + - name: Check compilation of LaTeX document from artifact + if: ${{ needs.workflow-setup.outputs.runner == '[[ self_hosted_label ]]' }} + run: latexmk -shell-escape [[ latexmk_args ]] -file-line-error -interaction=nonstopmode "${{ needs.workflow-setup.outputs.tex }}" + working-directory: ${{ github.workspace }}/artifact +[% endif %] +[% endif %] + + release: + needs: [workflow-setup, build_document, check] + runs-on: ${{ needs.workflow-setup.outputs.runner }} + name: Create Release + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: Download Artifact + uses: actions/download-artifact@v4 + with: + name: ${{ needs.workflow-setup.outputs.prefixwithref }} + path: ${{ github.workspace }}/artifact + + - name: Archive Article + run: | + temp_dir=$(mktemp -d) + tar -czf "${temp_dir}/${{ needs.workflow-setup.outputs.prefixwithref }}.tar.gz" -C artifact ./ + mv "${temp_dir}/${{ needs.workflow-setup.outputs.prefixwithref }}.tar.gz" ./ + rm -rf "$temp_dir" + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + draft: false + prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') || contains(github.ref, 'preview') }} + name: Release ${{ github.ref_name }} + generate_release_notes: true + tag_name: ${{ github.ref }} + token: ${{ secrets.GITHUB_TOKEN }} + files: | + artifact/${{ needs.workflow-setup.outputs.prefixwithref }}.pdf +[% for doc in additional_documents %] + artifact/[[ doc.pdf ]] +[% endfor %] + ${{ needs.workflow-setup.outputs.prefixwithref }}.tar.gz +[% else %] + check: + needs: [build_document, workflow-setup] + runs-on: ${{ needs.workflow-setup.outputs.runner }} + name: Check Document Artifact + steps: + - name: Download Artifact + uses: actions/download-artifact@v4 + with: + name: ${{ needs.workflow-setup.outputs.prefixwithref }} + path: ${{ github.workspace }}/artifact + +[% if is_typst %] + - name: Set up Typst + uses: typst-community/setup-typst@v4 + + - name: Check compilation of Typst document from artifact + run: typst compile "${{ needs.workflow-setup.outputs.tex }}" + working-directory: ${{ github.workspace }}/artifact + +[% else %] +[% if runner_policy != "self-hosted" %] + - name: Check compilation of LaTeX document from artifact + if: ${{ needs.workflow-setup.outputs.runner == '[[ github_runner ]]' }} + uses: xu-cheng/latex-action@v3 + with: + root_file: ${{ needs.workflow-setup.outputs.tex }} + latexmk_shell_escape: true + latexmk_use_xelatex: [[ latexmk_use_xelatex ]] + working_directory: ${{ github.workspace }}/artifact + +[% endif %] +[% if runner_policy != "github" %] + - name: Check compilation of LaTeX document from artifact + if: ${{ needs.workflow-setup.outputs.runner == '[[ self_hosted_label ]]' }} + run: latexmk -shell-escape [[ latexmk_args ]] -file-line-error -interaction=nonstopmode "${{ needs.workflow-setup.outputs.tex }}" + working-directory: ${{ github.workspace }}/artifact +[% endif %] +[% endif %] +[% endif %] diff --git a/src/article_cli/templates/poster/poster.tex.j2 b/src/article_cli/templates/poster/poster.tex.j2 new file mode 100644 index 0000000..5d6a54d --- /dev/null +++ b/src/article_cli/templates/poster/poster.tex.j2 @@ -0,0 +1,55 @@ +\documentclass[a0paper,portrait]{tikzposter} + +% Essential packages +\usepackage{amsmath,amssymb} +\usepackage{graphicx} +\usepackage{booktabs} + +% Git version information +\usepackage{gitinfo2} + +% Title and authors +\title{[[ title ]]} +\author{[[ authors_latex ]]} +\institute{Your Institution} +\date{\today} + +% Theme +\usetheme{Default} + +\begin{document} + +\maketitle + +\begin{columns} + \column{0.5} + + \block{Introduction}{ + Your introduction goes here. + } + + \block{Methods}{ + Your methods description goes here. + } + + \column{0.5} + + \block{Results}{ + Your results go here. + } + + \block{Conclusions}{ + Your conclusions go here. + } + +\end{columns} + +\block{References}{ + Your references go here. +} + +\note[targetoffsetx=0cm, targetoffsety=-8cm, width=0.4\textwidth]{ + Git version: \gitAbbrevHash{} (\gitAuthorIsoDate) +} + +\end{document} diff --git a/src/article_cli/templates/poster/poster.typ.j2 b/src/article_cli/templates/poster/poster.typ.j2 new file mode 100644 index 0000000..775243c --- /dev/null +++ b/src/article_cli/templates/poster/poster.typ.j2 @@ -0,0 +1,97 @@ +// Typst Poster +// Title: [[ title ]] +// Authors: [[ authors_display ]] + +#set page(paper: "a0", margin: 2cm) +#set text(font: "Helvetica Neue", size: 24pt) + +// Header +#align(center)[ + #text(size: 72pt, weight: "bold")[ [[ title ]] ] + + #v(1cm) + + #text(size: 36pt)[ [[ authors_display ]] ] + + #text(size: 28pt)[Your Institution] +] + +#v(2cm) + +#columns(3, gutter: 2cm)[ + // Column 1 + #block( + fill: rgb("#f0f0f0"), + inset: 1cm, + radius: 10pt, + width: 100%, + )[ + #text(size: 36pt, weight: "bold")[Introduction] + + #v(0.5cm) + + Your introduction goes here. + ] + + #v(1cm) + + #block( + fill: rgb("#f0f0f0"), + inset: 1cm, + radius: 10pt, + width: 100%, + )[ + #text(size: 36pt, weight: "bold")[Methods] + + #v(0.5cm) + + Your methods description goes here. + ] + + #colbreak() + + // Column 2 + #block( + fill: rgb("#f0f0f0"), + inset: 1cm, + radius: 10pt, + width: 100%, + )[ + #text(size: 36pt, weight: "bold")[Results] + + #v(0.5cm) + + Your results go here. + ] + + #colbreak() + + // Column 3 + #block( + fill: rgb("#f0f0f0"), + inset: 1cm, + radius: 10pt, + width: 100%, + )[ + #text(size: 36pt, weight: "bold")[Conclusions] + + #v(0.5cm) + + Your conclusions go here. + ] + + #v(1cm) + + #block( + fill: rgb("#e8f4ea"), + inset: 1cm, + radius: 10pt, + width: 100%, + )[ + #text(size: 36pt, weight: "bold")[References] + + #v(0.5cm) + + Your references go here. + ] +] diff --git a/src/article_cli/templates/presentation/beamer.tex.j2 b/src/article_cli/templates/presentation/beamer.tex.j2 new file mode 100644 index 0000000..4494488 --- /dev/null +++ b/src/article_cli/templates/presentation/beamer.tex.j2 @@ -0,0 +1,68 @@ +\documentclass[aspectratio=[[ aspect_ratio ]]]{beamer} + +% Theme configuration +[[ theme_line ]] + +% Essential packages +\usepackage{tikz} +\usepackage{pgfplots} +\pgfplotsset{compat=newest} +\usepackage{booktabs} +\usepackage{hyperref} + +% Bibliography (optional) +% \usepackage[style=numeric,sorting=none]{biblatex} +% \addbibresource{references.bib} + +% Git version information +\usepackage{gitinfo2} + +% Title and authors +\title{[[ title ]]} +\author{[[ authors_latex ]]} +\date{\today} +\institute{Your Institution} + +\begin{document} + +\maketitle + +\begin{frame}{Outline} + \tableofcontents +\end{frame} + +\section{Introduction} + +\begin{frame}{Introduction} + \begin{itemize} + \item First point + \item Second point + \item Third point + \end{itemize} +\end{frame} + +\section{Main Content} + +\begin{frame}{Main Content} + Your main content goes here. +\end{frame} + +\section{Conclusion} + +\begin{frame}{Conclusion} + \begin{itemize} + \item Summary point 1 + \item Summary point 2 + \end{itemize} +\end{frame} + +\begin{frame}{Questions?} + \centering + \Large Thank you for your attention! + + \vspace{1cm} + \small + Git version: \gitAbbrevHash{} (\gitAuthorIsoDate) +\end{frame} + +\end{document} diff --git a/src/article_cli/templates/presentation/typst.typ.j2 b/src/article_cli/templates/presentation/typst.typ.j2 new file mode 100644 index 0000000..3187a56 --- /dev/null +++ b/src/article_cli/templates/presentation/typst.typ.j2 @@ -0,0 +1,114 @@ +[% if has_theme %] +#import "[[ theme ]].typ": * + +#show: [[ theme ]]-theme.with( + title: "[[ title ]]", + author: [ +[% for author in authors %] + "[[ author ]]", +[% endfor %] + ], + date: datetime.today().display("[month repr:long] [day], [year]"), + institution: "Your Institution", +) + +// Title slide is automatically generated by the theme. + +#slide(title: "Outline")[ + #outline() +] + += Introduction + +#slide(title: "Introduction")[ + - First point + - Second point + - Third point +] + += Main Content + +#slide(title: "Main Content")[ + Your main content goes here. +] + += Conclusion + +#slide(title: "Conclusion")[ + - Summary point 1 + - Summary point 2 +] + +#slide(title: "Questions?")[ + #align(center)[ + #text(size: 36pt)[Thank you for your attention!] + ] +] +[% else %] +// Typst Presentation +// Title: [[ title ]] +// Authors: [[ authors_display ]] + +#set page(paper: "presentation-16-9", margin: 2cm) +#set text(font: "Helvetica Neue", size: 24pt) + +// Title slide +#page[ + #align(center + horizon)[ + #text(size: 48pt, weight: "bold")[ [[ title ]] ] + + #v(1cm) + + #text(size: 28pt)[ [[ authors_display ]] ] + + #v(0.5cm) + + Your Institution + + #v(0.5cm) + + #datetime.today().display("[month repr:long] [day], [year]") + ] +] + +// Introduction +#page[ + #text(size: 36pt, weight: "bold")[Introduction] + + #v(1cm) + + - First point + - Second point + - Third point +] + +// Main Content +#page[ + #text(size: 36pt, weight: "bold")[Main Content] + + #v(1cm) + + Your main content goes here. +] + +// Conclusion +#page[ + #text(size: 36pt, weight: "bold")[Conclusion] + + #v(1cm) + + - Summary point 1 + - Summary point 2 +] + +// Questions +#page[ + #align(center + horizon)[ + #text(size: 48pt)[Questions?] + + #v(1cm) + + Thank you for your attention! + ] +] +[% endif %] diff --git a/src/article_cli/templates/project/README.md.j2 b/src/article_cli/templates/project/README.md.j2 new file mode 100644 index 0000000..35a1a42 --- /dev/null +++ b/src/article_cli/templates/project/README.md.j2 @@ -0,0 +1,164 @@ +# [[ title ]] + +## Authors + +[% for author in authors %] +- [[ author ]] +[% endfor %] + +## Overview + +This repository contains the source for the [[ doc_type ]] "[[ title ]]". + +Document style: `[[ style ]]` + +## Prerequisites + +- Python 3.9+ for bibliography management +- LaTeX distribution, with TeX Live recommended +- Git with the gitinfo2 package + +## Setup + +1. Install article-cli: + + ```bash + uv tool install article-cli + ``` + +2. Set up git hooks: + + ```bash + article-cli setup + ``` + +3. Configure Zotero for bibliography management: + + Add `ZOTERO_API_KEY` as a GitHub Actions secret or set it locally: + + ```bash + export ZOTERO_API_KEY="your_api_key_here" + ``` + +4. Update the bibliography: + + ```bash + article-cli bib update + ``` + + CI can check the checked-in bibliography without modifying it: + + ```bash + article-cli bib update --check + ``` + +## Building the Document + +### Local Build + +```bash +[[ build_cmd ]] +``` + +Or using article-cli: + +```bash +article-cli compile [[ tex_file ]] +``` + +### Clean Build Files + +```bash +article-cli clean +``` + +## CI/CD + +This repository uses GitHub Actions for automated PDF compilation: + +- On push to `main`: compiles and uploads a PDF artifact. +- On pull request: compiles and verifies the document. +[% if ci_release_policy == "github" %] +- On tag push (`v*`): creates a GitHub release with the PDF. +[% else %] +- On tag push (`v*`): builds and uploads artifacts; GitHub release creation is disabled. +[% endif %] +- Runner policy: `[[ ci_runner_policy ]]`. +- Bibliography policy: `[[ ci_bibliography ]]`. + +## Project Structure + +```text +. +|-- [[ tex_file ]] # Main source document +|-- references.bib # Bibliography, managed via Zotero +|-- local_references.bib # Optional manual BibTeX entries +|-- pyproject.toml # Project configuration +|-- README.md # This file +`-- .github/ + `-- workflows/ + `-- latex.yml # CI/CD pipeline +``` + +## article-cli Commands + +```bash +# Setup repository +article-cli setup --dry-run +article-cli setup + +# Diagnose repository readiness +article-cli doctor + +# Update bibliography from Zotero +article-cli bib update +article-cli bib update --check +article-cli bib update --include-local --merged-output references.all.bib + +# Build the configured document +article-cli compile + +# Refresh gitinfo2 version metadata +article-cli version + +# Refresh metadata, compile, and check the PDF version text +article-cli version --compile --check-pdf + +# Preview a checked paper release +article-cli release v1 --dry-run + +# Create a checked local release tag +article-cli release v1 + +# List releases +article-cli list + +# Clean LaTeX build files +article-cli clean + +# Show configuration +article-cli config show +``` + +## Development Workflow + +1. Make changes to LaTeX source files. +2. Update bibliography if needed: `article-cli bib update`. +3. Build locally: `article-cli compile`. +4. Commit and push changes. +5. Create a release tag for publication: `article-cli release v1 --bib check --push`. + +## License + +[Specify your license here] + +## Citation + +```bibtex +@article{[[ citation_key ]], + title = {[[ title ]]}, + author = {[[ authors_bibtex ]]}, + year = {2025}, + url = {https://github.com/feelpp/[[ project_name ]]} +} +``` diff --git a/src/article_cli/templates/project/gitignore.j2 b/src/article_cli/templates/project/gitignore.j2 new file mode 100644 index 0000000..82ac784 --- /dev/null +++ b/src/article_cli/templates/project/gitignore.j2 @@ -0,0 +1,42 @@ +# LaTeX build files +*.aux +*.bbl +*.blg +*.log +*.out +*.toc +*.fdb_latexmk +*.fls +*.synctex.gz +*.pdf +*.dvi +*.ps +*.idx +*.ilg +*.ind +*.lof +*.lot + +# Python +__pycache__/ +*.py[cod] +*$py.class +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ + +# Editor +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# article-cli +.article-cli.toml.backup +references.bib.backup diff --git a/src/article_cli/templates/project/pyproject.toml.j2 b/src/article_cli/templates/project/pyproject.toml.j2 new file mode 100644 index 0000000..73e80f0 --- /dev/null +++ b/src/article_cli/templates/project/pyproject.toml.j2 @@ -0,0 +1,116 @@ +# [[ project_type_title ]] Repository Dependency Management +# This file manages dependencies for the LaTeX [[ project_type ]] project. + +[project] +name = [[ project_name_toml ]] +version = "0.1.0" +description = [[ title_toml ]] +authors = [ +[% for author in author_entries %] + {name = [[ author.name ]]}, +[% endfor %] +] +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ + "article-cli>=[[ article_cli_min_version ]]", + # Add other dependencies your project might need: + # "matplotlib>=3.5.0", + # "numpy>=1.20.0", + # "pandas>=1.3.0", +] + +# Configuration for article-cli, embedded in pyproject.toml. +[tool.article-cli.template] +version = "[[ template_version ]]" + +[tool.article-cli.zotero] +group_id = [[ group_id_toml ]] # Zotero group ID for this project +# api_key = "your_api_key_here" # Uncomment and add your API key or use ZOTERO_API_KEY env variable +# collection_id = "" # Optional Zotero collection or subcollection key +output_file = "references.bib" +local_file = "local_references.bib" +# merged_output_file = "references.all.bib" +deterministic = true + +[tool.article-cli.git] +auto_push = false +default_branch = "main" + +[tool.article-cli.release] +tag_policy = "paper" +allow_dirty = false +compile = true +check_pdf = true +checksum = true +bibliography = "off" +github_release = false + +[tool.article-cli.latex] +clean_extensions = [ + ".aux", ".bbl", ".blg", ".log", ".out", ".pyg", + ".fls", ".synctex.gz", ".toc", ".fdb_latexmk", + ".idx", ".ilg", ".ind", ".lof", ".lot", ".nav", ".snm", ".vrb" +] +engine = "[[ default_engine ]]" + +[tool.article-cli.project] +type = "[[ project_type ]]" +style = [[ style_toml ]] +[% if custom_template %] +template = [[ template_toml ]] +[% endif %] + +[% if project_type == "presentation" or project_type == "typst-presentation" %] +[tool.article-cli.presentation] +theme = [[ theme_toml ]] +aspect_ratio = "[[ aspect_ratio ]]" +color_theme = "" +font_theme = "" + +[% elif project_type == "poster" or project_type == "typst-poster" %] +[tool.article-cli.poster] +size = "a0" +orientation = "portrait" +columns = 3 + +[% endif %] +[% if project_type.startswith("typst-") %] +[tool.article-cli.typst] +build_dir = "" +font_paths = [] + +[% endif %] +[tool.article-cli.documents] +main = [[ main_document_toml ]] +[% if additional_documents %] +additional = [ +[% for doc in additional_document_entries %] + [[ doc.name ]], +[% endfor %] +] +[% else %] +# additional = ["poster.tex"] +[% endif %] + +[tool.article-cli.workflow] +runner_policy = [[ runner_policy_toml ]] +github_runner = [[ github_runner_toml ]] +self_hosted_label = [[ self_hosted_label_toml ]] +self_hosted_org = [[ self_hosted_org_toml ]] +bibliography = [[ bibliography_policy_toml ]] +release = [[ release_policy_toml ]] +artifact_includes = [ +[% for path in artifact_include_entries %] + [[ path.name ]], +[% endfor %] +] +[% if output_dir %] +output_dir = [[ output_dir_toml ]] +[% endif %] +[% if fonts_dir %] +fonts_dir = [[ fonts_dir_toml ]] +[% endif %] +[% if install_fonts %] +install_fonts = true +[% endif %] diff --git a/src/article_cli/templates/project/vscode-settings.json.j2 b/src/article_cli/templates/project/vscode-settings.json.j2 new file mode 100644 index 0000000..2576800 --- /dev/null +++ b/src/article_cli/templates/project/vscode-settings.json.j2 @@ -0,0 +1,128 @@ +{ +[% if is_typst %] + "typst-lsp.exportPdf": "onSave", + "typst-preview.preview.background.enabled": false, + "ltex.enabled": true, + "ltex.language": "en-US" +[% else %] + "latex-workshop.latex.recipes": [ +[% if use_xelatex %] + { + "name": "latexmk-xelatex", + "tools": [ + "latexmk-xelatex-shell-escape" + ] + }, + { + "name": "latexmk-lualatex", + "tools": [ + "latexmk-lualatex-shell-escape" + ] + }, + { + "name": "xelatex-shell-escape-recipe", + "tools": [ + "xelatex-shell-escape" + ] + } +[% else %] + { + "name": "latexmk-pdf", + "tools": [ + "latexmk-shell-escape" + ] + }, + { + "name": "pdflatex-shell-escape-recipe", + "tools": [ + "pdflatex-shell-escape" + ] + } +[% endif %] + ], + "latex-workshop.latex.tools": [ +[% if use_xelatex %] + { + "name": "latexmk-xelatex-shell-escape", + "command": "latexmk", + "args": [ + "--shell-escape", + "-xelatex", + "-interaction=nonstopmode", + "-synctex=1", + "%DOC%" + ], + "env": {} + }, + { + "name": "latexmk-lualatex-shell-escape", + "command": "latexmk", + "args": [ + "--shell-escape", + "-lualatex", + "-interaction=nonstopmode", + "-synctex=1", + "%DOC%" + ], + "env": {} + }, + { + "name": "xelatex-shell-escape", + "command": "xelatex", + "args": [ + "--shell-escape", + "-synctex=1", + "-interaction=nonstopmode", + "-file-line-error", + "%DOC%" + ] + } +[% else %] + { + "name": "latexmk-shell-escape", + "command": "latexmk", + "args": [ + "--shell-escape", + "-pdf", + "-interaction=nonstopmode", + "-synctex=1", + "%DOC%" + ], + "env": {} + }, + { + "name": "pdflatex-shell-escape", + "command": "pdflatex", + "args": [ + "--shell-escape", + "-synctex=1", + "-interaction=nonstopmode", + "-file-line-error", + "%DOC%" + ] + } +[% endif %] + ], + "latex-workshop.latex.autoBuild.run": "onSave", + "latex-workshop.latex.autoBuild.enabled": true, + "latex-workshop.latex.build.showOutput": "always", + "latex-workshop.latex.outDir": "%DIR%", + "latex-workshop.latex.clean.subfolder.enabled": true, + "latex-workshop.message.badbox.show": "none", + "workbench.editor.pinnedTabsOnSeparateRow": true, + "ltex.latex.commands": { + "\\\\author{}": "ignore", + "\\\\IfFileExists{}{}": "ignore", + "\\\\todo{}": "ignore", + "\\\\todo[]{}": "ignore", + "\\\\ts{}": "ignore", + "\\\\cp{}": "ignore", + "\\\\pgfmathprintnumber{}": "dummy", + "\\\\feelpp{}": "dummy", + "\\\\pgfplotstableread[]{}": "ignore", + "\\\\xpatchcmd{}{}{}{}{}": "ignore" + }, + "ltex.enabled": true, + "ltex.language": "en-US" +[% endif %] +} diff --git a/src/article_cli/themes.py b/src/article_cli/themes.py index b0eed2c..d0b00e0 100644 --- a/src/article_cli/themes.py +++ b/src/article_cli/themes.py @@ -11,7 +11,7 @@ from urllib.request import urlopen, Request from urllib.error import URLError, HTTPError -from .zotero import print_error, print_info, print_success, print_warning +from .reporting import print_error, print_info, print_success, print_warning # Default theme sources DEFAULT_THEME_SOURCES: Dict[str, Dict[str, Any]] = { diff --git a/src/article_cli/typst_compiler.py b/src/article_cli/typst_compiler.py index 924c7c1..e76d7d5 100644 --- a/src/article_cli/typst_compiler.py +++ b/src/article_cli/typst_compiler.py @@ -5,13 +5,15 @@ watch mode and custom font paths. """ +import re +import shutil import subprocess import time from pathlib import Path from typing import Dict, List, Optional from .config import Config -from .zotero import print_error, print_info, print_success +from .reporting import print_error, print_info, print_success class TypstCompiler: @@ -92,6 +94,7 @@ def _compile_once( if pdf_path.exists(): print_success(f"✅ Compilation successful: {pdf_path}") self._show_pdf_info(pdf_path) + self._show_pdf_page_count(pdf_path) else: print_error("Compilation reported success but PDF not found") return False @@ -244,6 +247,26 @@ def _show_pdf_info(self, pdf_path: Path) -> None: except Exception: pass # Silently ignore errors getting file info + def _show_pdf_page_count(self, pdf_path: Path) -> None: + """Print PDF page count when pdfinfo is available.""" + if shutil.which("pdfinfo") is None: + return + try: + result = subprocess.run( + ["pdfinfo", str(pdf_path)], + capture_output=True, + text=True, + timeout=10, + check=False, + ) + except Exception: + return + if result.returncode != 0: + return + match = re.search(r"^Pages:\s+(\d+)", result.stdout, re.MULTILINE) + if match: + print_info(f"PDF pages: {match.group(1)}") + def check_dependencies(self) -> Dict[str, bool]: """ Check if Typst is available diff --git a/src/article_cli/zotero.py b/src/article_cli/zotero.py index 243c7cd..128db24 100644 --- a/src/article_cli/zotero.py +++ b/src/article_cli/zotero.py @@ -5,50 +5,96 @@ error handling, and rate limiting. """ -import os -import sys -from typing import Optional, Tuple, List, Dict, Any +import re +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence, Set, Tuple import time +from .reporting import print_error, print_info, print_success, print_warning + try: import requests except ImportError: - print("Error: 'requests' library is required. Install with: pip install requests") - sys.exit(1) - + requests = None # type: ignore[assignment] + + +BIBTEX_ENTRY_RE = re.compile(r"@\w+\s*\{\s*([^,\s]+)\s*,", re.IGNORECASE) +LATEX_CITE_RE = re.compile( + r"\\(?:no)?(?:cite|parencite|textcite|autocite|citep|citet|citeauthor|citeyear)" + r"(?:\s*\[[^\]]*\])*\s*\{([^{}]+)\}" +) +LATEX_AUX_CITE_RE = re.compile(r"\\citation\{([^{}]+)\}") +BIBLATEX_AUX_CITE_RE = re.compile(r"\\abx@aux@cite\{[^{}]*\}\{([^{}]+)\}") +TYPST_CITE_RE = re.compile(r"(? List[str]: + """Split BibTeX content into balanced entries.""" + entries: List[str] = [] + index = 0 + while True: + start = content.find("@", index) + if start == -1: + break + + brace = content.find("{", start) + if brace == -1: + break + + depth = 0 + end = brace + while end < len(content): + char = content[end] + if char == "{": + depth += 1 + elif char == "}": + depth -= 1 + if depth == 0: + end += 1 + break + end += 1 + + if depth == 0: + entry = content[start:end].strip() + if entry: + entries.append(entry) + index = end + else: + break -class Colors: - """ANSI color codes for terminal output""" + return entries - HEADER = "\033[95m" - OKBLUE = "\033[94m" - OKCYAN = "\033[96m" - OKGREEN = "\033[92m" - WARNING = "\033[93m" - FAIL = "\033[91m" - ENDC = "\033[0m" - BOLD = "\033[1m" - UNDERLINE = "\033[4m" +def extract_bibtex_keys(content: str) -> Set[str]: + """Extract citation keys from BibTeX content.""" + return {match.group(1).strip() for match in BIBTEX_ENTRY_RE.finditer(content)} -def print_success(msg: str) -> None: - """Print success message in green""" - print(f"{Colors.OKGREEN}✓ {msg}{Colors.ENDC}") +def extract_citation_keys(path: Path) -> Set[str]: + """Extract citation keys from LaTeX, Typst, and aux files.""" + if not path.exists() or not path.is_file(): + return set() -def print_error(msg: str) -> None: - """Print error message in red""" - print(f"{Colors.FAIL}✗ Error: {msg}{Colors.ENDC}", file=sys.stderr) + text = path.read_text(encoding="utf-8", errors="replace") + keys: Set[str] = set() + if path.suffix == ".aux": + for pattern in [LATEX_AUX_CITE_RE, BIBLATEX_AUX_CITE_RE]: + for match in pattern.finditer(text): + keys.update(_split_key_list(match.group(1))) + elif path.suffix == ".typ": + keys.update(match.group(1).strip() for match in TYPST_CITE_RE.finditer(text)) + else: + for match in LATEX_CITE_RE.finditer(text): + keys.update(_split_key_list(match.group(1))) -def print_warning(msg: str) -> None: - """Print warning message in yellow""" - print(f"{Colors.WARNING}⚠ Warning: {msg}{Colors.ENDC}") + keys.discard("*") + return keys -def print_info(msg: str) -> None: - """Print info message in cyan""" - print(f"{Colors.OKCYAN}ℹ {msg}{Colors.ENDC}") +def _split_key_list(keys: str) -> Set[str]: + """Split comma-separated citation keys.""" + return {key.strip() for key in keys.split(",") if key.strip()} class ZoteroBibTexUpdater: @@ -59,6 +105,7 @@ def __init__( api_key: Optional[str], user_id: Optional[str] = None, group_id: Optional[str] = None, + collection_id: Optional[str] = None, output_file: Optional[str] = None, ): # Validate required parameters @@ -66,13 +113,21 @@ def __init__( raise ValueError("API key is required") if not user_id and not group_id: raise ValueError("Either user_id or group_id is required") + if requests is None: + raise RuntimeError( + "'requests' library is required for Zotero synchronization. " + "Install development dependencies with: uv sync --all-extras --dev" + ) self.api_key = api_key self.user_id = user_id self.group_id = group_id + self.collection_id = collection_id self.output_file = output_file or "references.bib" self.base_url = "https://api.zotero.org" self.limit = 100 + self.retry_count: int = 3 + self.retry_backoff: float = 1.0 self._group_name: Optional[str] = None # Create session with persistent headers @@ -88,12 +143,16 @@ def __init__( def _build_url(self) -> str: """Build the Zotero API URL based on user or group ID""" if self.group_id: - return f"{self.base_url}/groups/{self.group_id}/items" + library_url = f"{self.base_url}/groups/{self.group_id}" elif self.user_id: - return f"{self.base_url}/users/{self.user_id}/items" + library_url = f"{self.base_url}/users/{self.user_id}" else: raise ValueError("Either user_id or group_id must be provided") + if self.collection_id: + return f"{library_url}/collections/{self.collection_id}/items" + return f"{library_url}/items" + def _get_group_info(self) -> Optional[Dict[str, Any]]: """Get group information including name""" if not self.group_id: @@ -141,8 +200,7 @@ def _fetch_page(self, url: str, start: int = 0) -> Tuple[str, dict]: } try: - response = self.session.get(url, params=params, timeout=30) - response.raise_for_status() + response = self._request_get(url, params=params, timeout=30) # Check rate limiting rate_limit_remaining = response.headers.get("X-RateLimit-Remaining") @@ -175,18 +233,58 @@ def _fetch_page(self, url: str, start: int = 0) -> Tuple[str, dict]: print_error(f"Network error: {str(e)}") raise + def _request_get( + self, url: str, params: Optional[Dict[str, str]] = None, timeout: int = 30 + ) -> Any: + """Run a GET request with bounded retries for transient failures.""" + last_error: Optional[Exception] = None + response: Any = None + for attempt in range(1, self.retry_count + 1): + try: + response = self.session.get(url, params=params, timeout=timeout) + if response.status_code in {429, 500, 502, 503, 504}: + if attempt < self.retry_count: + wait_time = self._retry_wait(response, attempt) + print_warning( + f"Zotero request returned HTTP {response.status_code}; " + f"retrying in {wait_time:.1f}s " + f"({attempt}/{self.retry_count})" + ) + time.sleep(wait_time) + continue + response.raise_for_status() + return response + except requests.exceptions.RequestException as e: + last_error = e + if attempt >= self.retry_count: + break + wait_time = self.retry_backoff * (2 ** (attempt - 1)) + print_warning( + f"Zotero request failed: {e}; retrying in {wait_time:.1f}s " + f"({attempt}/{self.retry_count})" + ) + time.sleep(wait_time) + + if last_error is not None: + raise last_error + if response is None: + raise RuntimeError("Zotero request failed before receiving a response") + response.raise_for_status() + return response + + def _retry_wait(self, response: Any, attempt: int) -> float: + """Return retry delay from Zotero headers or exponential backoff.""" + retry_after = response.headers.get("Retry-After") + if retry_after: + try: + return float(retry_after) + except ValueError: + pass + return float(self.retry_backoff * (2 ** (attempt - 1))) + def _count_bibtex_entries(self, content: str) -> int: """Count the number of BibTeX entries in the content""" - return ( - content.count("@article") - + content.count("@book") - + content.count("@inproceedings") - + content.count("@incollection") - + content.count("@misc") - + content.count("@phdthesis") - + content.count("@techreport") - + content.count("@mastersthesis") - ) + return len(split_bibtex_entries(content)) def _filter_empty_entries(self, content: str) -> Tuple[str, int]: """ @@ -219,151 +317,442 @@ def _filter_empty_entries(self, content: str) -> Tuple[str, int]: return filtered_content, len(empty_entries) - def _backup_existing_file(self) -> None: + def _normalize_bibtex_content(self, content: str) -> str: + """Return BibTeX entries sorted by citation key for stable output.""" + entries = split_bibtex_entries(content) + keyed_entries = [] + unkeyed_entries = [] + + for entry in entries: + match = BIBTEX_ENTRY_RE.search(entry) + if match: + keyed_entries.append((match.group(1).lower(), entry.strip())) + else: + unkeyed_entries.append(entry.strip()) + + sorted_entries = [entry for _, entry in sorted(keyed_entries)] + sorted_entries.extend(unkeyed_entries) + return "\n\n".join(sorted_entries).strip() + + def _merge_bibtex_content(self, zotero_content: str, local_content: str) -> str: + """Merge Zotero and local entries, keeping Zotero entries on key conflicts.""" + entries_by_key: Dict[str, str] = {} + order: List[str] = [] + + for entry in split_bibtex_entries(zotero_content): + match = BIBTEX_ENTRY_RE.search(entry) + if not match: + continue + key = match.group(1) + entries_by_key[key] = entry.strip() + order.append(key) + + duplicate_local_keys = [] + for entry in split_bibtex_entries(local_content): + match = BIBTEX_ENTRY_RE.search(entry) + if not match: + continue + key = match.group(1) + if key in entries_by_key: + duplicate_local_keys.append(key) + continue + entries_by_key[key] = entry.strip() + order.append(key) + + if duplicate_local_keys: + print_warning( + "Skipped local entries already present in Zotero: " + + ", ".join(sorted(duplicate_local_keys)) + ) + + return "\n\n".join(entries_by_key[key] for key in sorted(order, key=str.lower)) + + def _build_document( + self, + content: str, + total_entries: int, + num_removed: int, + local_file: Optional[Path] = None, + timestamp: bool = False, + ) -> str: + """Build deterministic BibTeX file content with a stable header.""" + header = [ + "% BibTeX entries from Zotero", + f"% Total entries: {total_entries}", + ] + if self.group_id: + header.append(f"% Zotero group: {self.group_id}") + elif self.user_id: + header.append(f"% Zotero user: {self.user_id}") + if self.collection_id: + header.append(f"% Zotero collection: {self.collection_id}") + if local_file is not None: + header.append(f"% Local entries: {local_file}") + if num_removed > 0: + header.append(f"% Filtered out: {num_removed} empty entries") + if timestamp: + header.append(f"% Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}") + + body = content.strip() + return "\n".join(header) + "\n\n" + body + "\n" + + def _write_if_changed( + self, + path: Path, + content: str, + backup: bool, + check: bool, + ) -> bool: + """Write content only when changed, or report stale content in check mode.""" + current = path.read_text(encoding="utf-8") if path.exists() else None + if current == content: + print_success(f"Bibliography is already up to date: {path}") + return True + + if check: + print_error(f"Bibliography is not up to date: {path}") + return False + + if backup and current is not None: + self._backup_existing_file(path) + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + print_success(f"Updated bibliography: {path}") + return True + + def _backup_existing_file(self, path: Optional[Path] = None) -> None: """Create a backup of the existing BibTeX file""" - if os.path.exists(self.output_file): - backup_file = f"{self.output_file}.backup" + output_path = path or Path(self.output_file) + if output_path.exists(): + backup_file = output_path.with_name(output_path.name + ".backup") try: - with open(self.output_file, "r", encoding="utf-8") as src: + with open(output_path, "r", encoding="utf-8") as src: with open(backup_file, "w", encoding="utf-8") as dst: dst.write(src.read()) print_info(f"Created backup: {backup_file}") except Exception as e: print_warning(f"Could not create backup: {e}") - def update(self, backup: bool = True) -> bool: + def update( + self, + backup: bool = True, + check: bool = False, + include_local: bool = False, + local_file: Optional[str] = None, + merged_output_file: Optional[str] = None, + check_citations: bool = False, + citation_sources: Optional[Sequence[Path]] = None, + timestamp: bool = False, + ) -> bool: """ Fetch and update BibTeX file from Zotero with improved pagination Args: backup: Whether to create a backup of existing file + check: Whether to check freshness without writing files + include_local: Whether to merge local/manual BibTeX entries + local_file: Local/manual BibTeX file path + merged_output_file: Optional output path for merged bibliography + check_citations: Whether to check citation key completeness + citation_sources: Source files to scan for citation keys + timestamp: Whether to include a generated timestamp in the header Returns: True if successful, False otherwise """ try: - # Validate inputs - if not self.api_key: - print_error("Zotero API key is required") + if not self._validate_inputs(): return False - if not self.user_id and not self.group_id: - print_error("Either user_id or group_id must be provided") + fetched = self._fetch_zotero_bibtex() + if fetched is None: return False + filtered_content, total_entries, num_removed = fetched + + output_path = Path(self.output_file) + output_content = self._build_document( + filtered_content, + total_entries, + num_removed, + timestamp=timestamp, + ) + results, bibliography_for_citation_check = self._write_bibliography_outputs( + output_path=output_path, + output_content=output_content, + filtered_content=filtered_content, + num_removed=num_removed, + backup=backup, + check=check, + include_local=include_local, + local_file=local_file, + merged_output_file=merged_output_file, + timestamp=timestamp, + ) - # Create backup if requested - if backup: - self._backup_existing_file() - - # Build URL - base_url = self._build_url() - - # Show what we're connecting to - if self.group_id: - group_name = self.get_group_name() - if group_name: - print_info( - f"Connecting to Zotero group: {group_name} (ID: {self.group_id})" + if check_citations: + results.append( + self._check_citation_completeness( + bibliography_for_citation_check, + citation_sources or [], ) - else: - print_info(f"Connecting to Zotero group ID: {self.group_id}") - elif self.user_id: - print_info(f"Connecting to Zotero user library (ID: {self.user_id})") + ) - # First request to get total count - print_info("Fetching BibTeX entries from Zotero...") + return self._finish_update(results, total_entries) - try: - initial_content, initial_headers = self._fetch_page(base_url, start=0) - except Exception as e: - print_error(f"Failed to connect to Zotero: {e}") - return False + except Exception as e: + print_error(f"Unexpected error during update: {e}") + return False - # Get total number of items from header - total_results = int(initial_headers.get("Total-Results", 0)) + def _validate_inputs(self) -> bool: + """Validate required Zotero identifiers.""" + if not self.api_key: + print_error("Zotero API key is required") + return False + if not self.user_id and not self.group_id: + print_error("Either user_id or group_id must be provided") + return False + return True - if total_results == 0: - print_warning("No items found in Zotero library") - return False + def _describe_connection(self) -> None: + """Report the Zotero library selected for export.""" + if self.group_id: + group_name = self.get_group_name() + if group_name: + print_info( + f"Connecting to Zotero group: {group_name} (ID: {self.group_id})" + ) + else: + print_info(f"Connecting to Zotero group ID: {self.group_id}") + elif self.user_id: + print_info(f"Connecting to Zotero user library (ID: {self.user_id})") - print_info(f"Found {total_results} total items in library") + if self.collection_id: + print_info(f"Exporting Zotero collection: {self.collection_id}") - # Collect all BibTeX content - all_bibtex_content: List[str] = [] + def _fetch_zotero_bibtex(self) -> Optional[Tuple[str, int, int]]: + """Fetch, filter, normalize, and count Zotero BibTeX content.""" + base_url = self._build_url() + self._describe_connection() + print_info("Fetching BibTeX entries from Zotero...") - # Add initial page - if initial_content.strip(): - all_bibtex_content.append(initial_content) + first_page = self._fetch_initial_page(base_url) + if first_page is None: + return None - # Calculate pages needed - pages_needed = (total_results + self.limit - 1) // self.limit - print_info( - f"Fetching {pages_needed} pages ({self.limit} items per page)..." + initial_content, total_results = first_page + all_content = self._fetch_remaining_pages( + base_url, + initial_content, + total_results, + ) + if all_content is None: + return None + + filtered_content, num_removed = self._filter_empty_entries( + "\n\n".join(all_content) + ) + filtered_content = self._normalize_bibtex_content(filtered_content) + if num_removed > 0: + print_warning( + f"Filtered out {num_removed} empty/incomplete entries from Zotero" ) - # Fetch remaining pages - for page in range(1, pages_needed): - start = page * self.limit - progress = ((page + 1) / pages_needed) * 100 + total_entries = self._count_bibtex_entries(filtered_content) + if total_entries == 0: + print_warning("No valid BibTeX entries found after filtering") + return None - print( - f" Progress: {progress:.0f}% ({start + self.limit if start + self.limit < total_results else total_results}/{total_results} items)", - end="\r", - ) + return filtered_content, total_entries, num_removed - try: - content, _ = self._fetch_page(base_url, start=start) - if content.strip(): - all_bibtex_content.append(content) - except Exception as e: - print_error(f"\nFailed to fetch page {page + 1}: {e}") - return False + def _fetch_initial_page(self, base_url: str) -> Optional[Tuple[str, int]]: + """Fetch the first Zotero page and return content plus total count.""" + try: + initial_content, initial_headers = self._fetch_page(base_url, start=0) + except Exception as e: + print_error(f"Failed to connect to Zotero: {e}") + return None - print() # New line after progress + total_results = int(initial_headers.get("Total-Results", 0)) + if total_results == 0: + print_warning("No items found in Zotero library") + return None - # Combine all content - combined_content = "\n\n".join(all_bibtex_content) + print_info(f"Found {total_results} total items in library") + return initial_content, total_results - # Filter out empty entries - filtered_content, num_removed = self._filter_empty_entries(combined_content) - if num_removed > 0: - print_warning( - f"Filtered out {num_removed} empty/incomplete entries from Zotero" - ) + def _fetch_remaining_pages( + self, + base_url: str, + initial_content: str, + total_results: int, + ) -> Optional[List[str]]: + """Fetch all remaining Zotero BibTeX pages.""" + all_content: List[str] = [] + if initial_content.strip(): + all_content.append(initial_content) + + pages_needed = (total_results + self.limit - 1) // self.limit + print_info(f"Fetching {pages_needed} pages ({self.limit} items per page)...") + + for page in range(1, pages_needed): + start = page * self.limit + self._print_fetch_progress(page, pages_needed, start, total_results) + try: + content, _ = self._fetch_page(base_url, start=start) + except Exception as e: + print_error(f"\nFailed to fetch page {page + 1}: {e}") + return None + if content.strip(): + all_content.append(content) - # Count total valid entries - total_entries = self._count_bibtex_entries(filtered_content) + print() + return all_content - if total_entries == 0: - print_warning("No valid BibTeX entries found after filtering") - return False + def _print_fetch_progress( + self, + page: int, + pages_needed: int, + start: int, + total_results: int, + ) -> None: + """Print single-line Zotero pagination progress.""" + progress = ((page + 1) / pages_needed) * 100 + current = min(start + self.limit, total_results) + print( + f" Progress: {progress:.0f}% ({current}/{total_results} items)", + end="\r", + ) - # Write to file - print_info( - f"Writing {total_entries} valid entries to {self.output_file}..." + def _write_bibliography_outputs( + self, + output_path: Path, + output_content: str, + filtered_content: str, + num_removed: int, + backup: bool, + check: bool, + include_local: bool, + local_file: Optional[str], + merged_output_file: Optional[str], + timestamp: bool, + ) -> Tuple[List[bool], str]: + """Write primary and optional merged bibliography outputs.""" + if not include_local: + return ( + [ + self._write_if_changed( + output_path, + output_content, + backup=backup, + check=check, + ) + ], + output_content, ) - try: - with open(self.output_file, "w", encoding="utf-8") as f: - # Add header comment - f.write(f"% BibTeX entries from Zotero\n") - f.write(f"% Total entries: {total_entries}\n") - if num_removed > 0: - f.write(f"% Filtered out: {num_removed} empty entries\n") - f.write(f"% Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n") - f.write(filtered_content) - if not filtered_content.endswith("\n"): - f.write("\n") - - print_success( - f"Successfully updated {self.output_file} with {total_entries} entries" - ) - return True + local_path = Path(local_file) if local_file else Path("local_references.bib") + if not local_path.exists(): + print_warning(f"Local BibTeX file not found: {local_path}") + return ( + [ + self._write_if_changed( + output_path, + output_content, + backup=backup, + check=check, + ) + ], + output_content, + ) - except IOError as e: - print_error(f"Failed to write to {self.output_file}: {e}") - return False + merged_document = self._build_merged_document( + filtered_content, + local_path, + num_removed, + timestamp, + ) + merged_path = Path(merged_output_file) if merged_output_file else None + if merged_path is None: + return ( + [ + self._write_if_changed( + output_path, + merged_document, + backup=backup, + check=check, + ) + ], + merged_document, + ) - except Exception as e: - print_error(f"Unexpected error during update: {e}") + return ( + [ + self._write_if_changed( + output_path, + output_content, + backup=backup, + check=check, + ), + self._write_if_changed( + merged_path, + merged_document, + backup=backup, + check=check, + ), + ], + merged_document, + ) + + def _build_merged_document( + self, + filtered_content: str, + local_path: Path, + num_removed: int, + timestamp: bool, + ) -> str: + """Build a merged Zotero + local BibTeX document.""" + local_content = local_path.read_text(encoding="utf-8", errors="replace") + merged_content = self._merge_bibtex_content(filtered_content, local_content) + merged_entries = self._count_bibtex_entries(merged_content) + return self._build_document( + merged_content, + merged_entries, + num_removed, + local_file=local_path, + timestamp=timestamp, + ) + + def _finish_update(self, results: Sequence[bool], total_entries: int) -> bool: + """Return final update status and print a short summary.""" + if all(results): + print_success(f"Bibliography contains {total_entries} Zotero entries") + return True + return False + + def _check_citation_completeness( + self, bibliography_content: str, citation_sources: Sequence[Path] + ) -> bool: + """Check whether cited keys are present in bibliography content.""" + if not citation_sources: + print_info("No citation source files found for citation check.") + return True + + bibliography_keys = extract_bibtex_keys(bibliography_content) + cited_keys: Set[str] = set() + for source in citation_sources: + cited_keys.update(extract_citation_keys(source)) + + if not cited_keys: + print_info("No citation keys found in source files.") + return True + + missing = sorted(cited_keys - bibliography_keys) + if missing: + print_error( + "Missing bibliography entries for citations: " + ", ".join(missing) + ) return False + + print_success(f"All {len(cited_keys)} cited keys are present in bibliography") + return True diff --git a/tests/golden/__init__.py b/tests/golden/__init__.py new file mode 100644 index 0000000..bb258ee --- /dev/null +++ b/tests/golden/__init__.py @@ -0,0 +1 @@ +"""Golden files for rendered article-cli templates.""" diff --git a/tests/golden/article_main.tex b/tests/golden/article_main.tex new file mode 100644 index 0000000..a91c824 --- /dev/null +++ b/tests/golden/article_main.tex @@ -0,0 +1,58 @@ +\documentclass[a4paper,11pt]{article} + +% Essential packages +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage{lmodern} +\usepackage{amsmath,amssymb,amsthm} +\usepackage{graphicx} +\usepackage{hyperref} +\usepackage[margin=1in]{geometry} + +% Bibliography +\usepackage[style=numeric,sorting=none]{biblatex} +\addbibresource{references.bib} + +% Git version information +\usepackage{gitinfo2} + +% Title and authors +\title{Golden Article} +\author{Alice \and Bob} +\date{\today} + +\begin{document} + +\maketitle + +\begin{abstract} + Your abstract goes here. +\end{abstract} + +\section{Introduction} + +Your introduction goes here. + +\section{Methodology} + +Your methodology goes here. + +\section{Results} + +Your results go here. + +\section{Conclusion} + +Your conclusion goes here. + +% Print bibliography +\printbibliography + +% Git information (optional - appears in footer) +\vfill +\hrule +\small +\noindent Git version: \gitAbbrevHash{} (\gitAuthorIsoDate) \\ +Branch: \gitBranch + +\end{document} diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..928e39f --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,323 @@ +""" +Tests for article-cli command handlers. +""" + +from types import SimpleNamespace +from unittest.mock import patch + +import pytest + +import article_cli.zotero as zotero_module +from article_cli.cli import ( + create_parser, + handle_compile_command, + handle_update_bibtex_command, +) +from article_cli.commands import release as release_command +from article_cli.commands import setup as setup_command +from article_cli.commands import version as version_command +from article_cli.config import Config +from article_cli.zotero import ZoteroBibTexUpdater + +ZOTERO_ENV_VARS = ("ZOTERO_API_KEY", "ZOTERO_USER_ID", "ZOTERO_GROUP_ID") + + +def clear_zotero_env(monkeypatch): + """Keep CLI tests independent from developer Zotero credentials.""" + for var in ZOTERO_ENV_VARS: + monkeypatch.delenv(var, raising=False) + + +def test_compile_uses_project_config_when_cli_args_absent(tmp_path, monkeypatch): + """Compile should honor configured document, engine, shell escape, and output.""" + monkeypatch.chdir(tmp_path) + (tmp_path / "paper.tex").write_text("\\documentclass{article}\n") + (tmp_path / "pyproject.toml").write_text( + """ +[tool.article-cli.documents] +main = "paper.tex" + +[tool.article-cli.latex] +engine = "xelatex" +shell_escape = true + +[tool.article-cli.workflow] +output_dir = "build" +""" + ) + args = SimpleNamespace( + tex_file=None, + engine=None, + shell_escape=None, + output_dir=None, + clean_first=False, + clean_after=False, + watch=False, + font_paths=None, + ) + config = Config() + + with patch( + "article_cli.latex_compiler.LaTeXCompiler.compile", return_value=True + ) as compile_mock: + result = handle_compile_command(args, config) + + assert result == 0 + compile_mock.assert_called_once_with( + tex_file="paper.tex", + engine="xelatex", + shell_escape=True, + output_dir="build", + watch=False, + ) + + +def test_compile_cli_args_override_project_config(tmp_path, monkeypatch): + """Explicit CLI compile arguments should override project configuration.""" + monkeypatch.chdir(tmp_path) + (tmp_path / "paper.tex").write_text("\\documentclass{article}\n") + (tmp_path / "pyproject.toml").write_text( + """ +[tool.article-cli.documents] +main = "paper.tex" + +[tool.article-cli.latex] +engine = "xelatex" +shell_escape = true + +[tool.article-cli.workflow] +output_dir = "build" +""" + ) + args = SimpleNamespace( + tex_file="paper.tex", + engine="pdflatex", + shell_escape=False, + output_dir="out", + clean_first=False, + clean_after=False, + watch=False, + font_paths=None, + ) + config = Config() + + with patch( + "article_cli.latex_compiler.LaTeXCompiler.compile", return_value=True + ) as compile_mock: + result = handle_compile_command(args, config) + + assert result == 0 + compile_mock.assert_called_once_with( + tex_file="paper.tex", + engine="pdflatex", + shell_escape=False, + output_dir="out", + watch=False, + ) + + +def test_update_bibtex_uses_configured_output_file(tmp_path, monkeypatch): + """update-bibtex should not mask configured output_file with parser defaults.""" + clear_zotero_env(monkeypatch) + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text( + """ +[tool.article-cli.zotero] +api_key = "test-key" +group_id = "4709047" +output_file = "zotero/references.bib" +""" + ) + args = SimpleNamespace( + api_key=None, + user_id=None, + group_id=None, + collection=None, + output=None, + local_file=None, + merged_output=None, + include_local=False, + no_backup=True, + check=False, + check_citations=False, + timestamp=False, + ) + config = Config() + + with patch("article_cli.commands.bibtex.ZoteroBibTexUpdater") as updater_cls: + updater_cls.return_value.update.return_value = True + result = handle_update_bibtex_command(args, config) + + assert result == 0 + updater_cls.assert_called_once_with( + api_key="test-key", + user_id=None, + group_id="4709047", + collection_id=None, + output_file="zotero/references.bib", + ) + updater_cls.return_value.update.assert_called_once_with( + backup=False, + check=False, + include_local=False, + local_file="local_references.bib", + merged_output_file=None, + check_citations=False, + citation_sources=[], + timestamp=False, + ) + + +def test_update_bibtex_cli_output_overrides_config(tmp_path, monkeypatch): + """Explicit --output should override configured output_file.""" + clear_zotero_env(monkeypatch) + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text( + """ +[tool.article-cli.zotero] +api_key = "test-key" +group_id = "4709047" +output_file = "zotero/references.bib" +""" + ) + args = SimpleNamespace( + api_key=None, + user_id=None, + group_id=None, + collection=None, + output="manual.bib", + local_file=None, + merged_output=None, + include_local=False, + no_backup=True, + check=False, + check_citations=False, + timestamp=False, + ) + config = Config() + + with patch("article_cli.commands.bibtex.ZoteroBibTexUpdater") as updater_cls: + updater_cls.return_value.update.return_value = True + result = handle_update_bibtex_command(args, config) + + assert result == 0 + assert updater_cls.call_args.kwargs["output_file"] == "manual.bib" + + +def test_bib_update_dry_run_does_not_contact_zotero(tmp_path, monkeypatch): + """bib update --dry-run should validate config without writing references.""" + clear_zotero_env(monkeypatch) + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text( + """ +[tool.article-cli.zotero] +api_key = "test-key" +group_id = "4709047" +output_file = "references.bib" +""" + ) + args = SimpleNamespace( + api_key=None, + user_id=None, + group_id=None, + collection=None, + output=None, + local_file=None, + merged_output=None, + include_local=False, + no_backup=False, + dry_run=True, + check=False, + check_citations=False, + timestamp=False, + ) + config = Config(quiet=True) + + with patch("article_cli.commands.bibtex.ZoteroBibTexUpdater") as updater_cls: + result = handle_update_bibtex_command(args, config) + + assert result == 0 + updater_cls.assert_not_called() + assert not (tmp_path / "references.bib").exists() + + +def test_parser_config_backed_defaults_are_none(): + """Parser defaults must leave project config visible to command handlers.""" + parser = create_parser() + + compile_args = parser.parse_args(["compile"]) + bib_args = parser.parse_args(["update-bibtex"]) + canonical_bib_args = parser.parse_args(["bib", "update"]) + doctor_args = parser.parse_args(["doctor"]) + setup_args = parser.parse_args(["setup", "--dry-run"]) + version_args = parser.parse_args(["version", "--dry-run"]) + release_args = parser.parse_args(["release", "v1.0.0", "--dry-run"]) + create_args = parser.parse_args(["create", "v1.0.0", "--dry-run"]) + + assert compile_args.engine is None + assert compile_args.shell_escape is None + assert compile_args.output_dir is None + assert bib_args.output is None + assert canonical_bib_args.output is None + assert doctor_args.engine is None + assert doctor_args.output_dir is None + assert doctor_args.fix is False + assert setup_args.dry_run is True + assert version_args.dry_run is True + assert release_args.dry_run is True + assert create_args.dry_run is True + + +def test_setup_dry_run_is_forwarded_to_git_manager(): + """setup --dry-run should call the setup service in dry-run mode.""" + args = SimpleNamespace(dry_run=True) + + with patch("article_cli.commands.setup.GitManager") as manager_cls: + manager_cls.return_value.setup_hooks.return_value = True + result = setup_command.run(args, Config(quiet=True)) + + assert result == 0 + manager_cls.return_value.setup_hooks.assert_called_once_with(dry_run=True) + + +def test_version_dry_run_is_forwarded_to_git_manager(): + """version --dry-run should not refresh files.""" + args = SimpleNamespace(dry_run=True) + + with patch("article_cli.commands.version.GitManager") as manager_cls: + manager_cls.return_value.refresh_version_metadata.return_value = True + result = version_command.run(args, Config(quiet=True)) + + assert result == 0 + manager_cls.return_value.refresh_version_metadata.assert_called_once_with(True) + + +def test_release_dry_run_is_forwarded_to_release_service(tmp_path): + """release --dry-run should validate without creating a tag.""" + args = SimpleNamespace(version="v1.0.0", push=False, dry_run=True) + config_file = tmp_path / "pyproject.toml" + config_file.write_text( + """ +[tool.article-cli.git] +auto_push = true +""" + ) + + with patch("article_cli.commands.release.ReleaseService") as service_cls: + service_cls.return_value.release.return_value = True + result = release_command.run_release(args, Config(config_file, quiet=True)) + + assert result == 0 + service_cls.return_value.release.assert_called_once() + options = service_cls.return_value.release.call_args.args[0] + assert options.tag == "v1.0.0" + assert options.auto_push is False + assert options.dry_run is True + + +def test_zotero_missing_requests_fails_at_command_time(monkeypatch): + """Missing optional HTTP support should not terminate the process on import.""" + monkeypatch.setattr(zotero_module, "requests", None) + + with pytest.raises(RuntimeError, match="requests"): + ZoteroBibTexUpdater(api_key="test-key", group_id="4709047") diff --git a/tests/test_config.py b/tests/test_config.py index 183c911..53819aa 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -10,6 +10,15 @@ from article_cli.config import Config +ZOTERO_ENV_VARS = ("ZOTERO_API_KEY", "ZOTERO_USER_ID", "ZOTERO_GROUP_ID") + + +@pytest.fixture(autouse=True) +def isolate_zotero_environment(monkeypatch): + """Keep config tests independent from developer or CI Zotero credentials.""" + for var in ZOTERO_ENV_VARS: + monkeypatch.delenv(var, raising=False) + class TestConfig: """Test cases for Config class""" diff --git a/tests/test_doctor.py b/tests/test_doctor.py new file mode 100644 index 0000000..3f13ae1 --- /dev/null +++ b/tests/test_doctor.py @@ -0,0 +1,242 @@ +""" +Tests for article-cli doctor diagnostics. +""" + +import json +import subprocess +from pathlib import Path + +from article_cli.cli import main +from article_cli.config import Config +from article_cli.doctor import DoctorService +from article_cli.git_manager import GitManager + + +def init_git_repository(path: Path) -> None: + """Create a real git repository for doctor tests.""" + subprocess.run(["git", "init"], cwd=path, check=True, capture_output=True) + subprocess.run( + ["git", "config", "user.email", "test@example.com"], + cwd=path, + check=True, + ) + subprocess.run( + ["git", "config", "user.name", "Test User"], + cwd=path, + check=True, + ) + + +def write_ready_paper(path: Path) -> None: + """Create a minimal configured paper repository.""" + (path / "main.tex").write_text("\\documentclass{article}\n") + (path / "references.bib").write_text("@misc{test,title={Test}}\n") + (path / "pyproject.toml").write_text( + """ +[tool.article-cli.project] +type = "article" + +[tool.article-cli.documents] +main = "main.tex" + +[tool.article-cli.latex] +engine = "pdflatex" +build_dir = "." + +[tool.article-cli.zotero] +api_key = "test-key" +group_id = "4709047" +output_file = "references.bib" +""" + ) + workflow_dir = path / ".github" / "workflows" + workflow_dir.mkdir(parents=True) + (workflow_dir / "latex.yml").write_text( + """ +name: build +on: + push: +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: astral-sh/setup-uv@v8.1.0 + - run: article-cli compile main.tex +""" + ) + + +def test_doctor_reports_missing_git_repository_without_modifying_files( + tmp_path, monkeypatch +): + """Doctor should diagnose a missing Git repository without creating files.""" + monkeypatch.chdir(tmp_path) + (tmp_path / "main.tex").write_text("\\documentclass{article}\n") + before = sorted(path.relative_to(tmp_path) for path in tmp_path.rglob("*")) + + report = DoctorService(Config(quiet=True), cwd=tmp_path).run() + + after = sorted(path.relative_to(tmp_path) for path in tmp_path.rglob("*")) + assert report.ok is False + assert report.error_count >= 1 + assert any( + check.category == "git" + and check.name == "repository" + and check.status == "error" + for check in report.checks + ) + assert before == after + assert not (tmp_path / "hooks").exists() + assert not (tmp_path / "gitHeadLocal.gin").exists() + + +def test_doctor_ready_repository_has_no_blocking_errors(tmp_path, monkeypatch): + """A configured repository with hooks and tools should pass doctor.""" + monkeypatch.chdir(tmp_path) + init_git_repository(tmp_path) + write_ready_paper(tmp_path) + subprocess.run(["git", "add", "."], cwd=tmp_path, check=True) + subprocess.run( + ["git", "commit", "-m", "initial"], + cwd=tmp_path, + check=True, + capture_output=True, + ) + assert GitManager(tmp_path).setup_hooks() is True + + def fake_which(command): + return f"/usr/bin/{command}" + + monkeypatch.setattr("article_cli.doctor.shutil.which", fake_which) + report = DoctorService(Config(quiet=True), cwd=tmp_path).run() + + assert report.ok is True + assert report.error_count == 0 + assert report.context["main_document"].endswith("main.tex") + assert report.context["engine"] == "pdflatex" + assert any( + check.category == "workflow" + and check.name == "yaml-parse" + and check.status == "ok" + for check in report.checks + ) + + +def test_doctor_json_output_is_machine_readable(tmp_path, monkeypatch, capsys): + """doctor --json should not be polluted by config loading messages.""" + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text( + """ +[tool.article-cli.project] +type = "article" +""" + ) + + exit_code = main(["doctor", "--json"]) + + captured = capsys.readouterr() + payload = json.loads(captured.out) + assert exit_code == 1 + assert payload["ok"] is False + assert payload["summary"]["errors"] >= 1 + assert "Loaded configuration" not in captured.out + + +def test_doctor_reports_existing_release_tag(tmp_path, monkeypatch): + """A requested release tag that already exists is a blocking issue.""" + monkeypatch.chdir(tmp_path) + init_git_repository(tmp_path) + write_ready_paper(tmp_path) + subprocess.run(["git", "add", "."], cwd=tmp_path, check=True) + subprocess.run( + ["git", "commit", "-m", "initial"], + cwd=tmp_path, + check=True, + capture_output=True, + ) + subprocess.run(["git", "tag", "v1.0.0"], cwd=tmp_path, check=True) + + def fake_which(command): + return f"/usr/bin/{command}" + + monkeypatch.setattr("article_cli.doctor.shutil.which", fake_which) + report = DoctorService(Config(quiet=True), cwd=tmp_path).run(tag="v1.0.0") + + assert report.ok is False + assert any( + check.category == "release" and check.name == "tag" and check.status == "error" + for check in report.checks + ) + + +def test_doctor_fix_repairs_safe_repository_state(tmp_path, monkeypatch): + """doctor --fix creates only safe setup files and does not create commits.""" + monkeypatch.chdir(tmp_path) + init_git_repository(tmp_path) + (tmp_path / "main.tex").write_text("\\documentclass{article}\n") + (tmp_path / "references.bib").write_text("@misc{test,title={Test}}\n") + (tmp_path / "pyproject.toml").write_text( + """ +[tool.article-cli.project] +type = "article" + +[tool.article-cli.documents] +main = "main.tex" + +[tool.article-cli.latex] +engine = "pdflatex" + +[tool.article-cli.workflow] +output_dir = "build" + +[tool.article-cli.zotero] +api_key = "test-key" +group_id = "4709047" +output_file = "references.bib" +""" + ) + subprocess.run(["git", "add", "."], cwd=tmp_path, check=True) + subprocess.run( + ["git", "commit", "-m", "initial"], + cwd=tmp_path, + check=True, + capture_output=True, + ) + head_before = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=tmp_path, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + def fake_which(command): + return f"/usr/bin/{command}" + + monkeypatch.setattr("article_cli.doctor.shutil.which", fake_which) + report = DoctorService( + Config(tmp_path / "pyproject.toml", quiet=True), cwd=tmp_path + ).run(fix=True) + + head_after = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=tmp_path, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + assert head_after == head_before + assert (tmp_path / "build").is_dir() + assert (tmp_path / "hooks" / "post-commit").exists() + assert (tmp_path / ".git" / "hooks" / "post-commit").exists() + assert (tmp_path / "gitHeadLocal.gin").exists() + assert any( + check.category == "fix" + and check.name == "output-directory" + and check.status == "ok" + for check in report.checks + ) + assert any( + check.category == "fix" and check.name == "git-hooks" and check.status == "ok" + for check in report.checks + ) diff --git a/tests/test_latex_compiler.py b/tests/test_latex_compiler.py index cec6539..8649429 100644 --- a/tests/test_latex_compiler.py +++ b/tests/test_latex_compiler.py @@ -192,6 +192,21 @@ def test_compile_once_unknown_engine_returns_false(self, compiler, mock_tex_path assert result is False + def test_compile_refreshes_gitinfo2_metadata(self, compiler, mock_tex_path): + """Test compile refreshes gitinfo2 metadata before building""" + with ( + patch( + "article_cli.latex_compiler.refresh_gitinfo2_metadata", + return_value=True, + ) as refresh_mock, + patch.object(compiler, "_compile_once", return_value=True) as compile_mock, + ): + result = compiler.compile(str(mock_tex_path), engine="latexmk") + + refresh_mock.assert_called_once_with(mock_tex_path.parent) + compile_mock.assert_called_once_with(mock_tex_path, "latexmk", False, None) + assert result is True + # --- Test _compile_watch restrictions --- def test_compile_watch_rejects_pdflatex(self, compiler, mock_tex_path): @@ -270,8 +285,8 @@ def test_run_xelatex_success(self, mock_run, compiler, mock_tex_path, tmp_path): mock_tex_path, shell_escape=False, output_dir=None ) - # Should have been called 3 times (3 passes) - assert mock_run.call_count == 3 + # Should have run at least the 3 LaTeX passes; optional pdfinfo may add one. + assert mock_run.call_count >= 3 assert result is True @patch("subprocess.run") @@ -288,8 +303,8 @@ def test_run_lualatex_success(self, mock_run, compiler, mock_tex_path, tmp_path) mock_tex_path, shell_escape=False, output_dir=None ) - # Should have been called 3 times (3 passes) - assert mock_run.call_count == 3 + # Should have run at least the 3 LaTeX passes; optional pdfinfo may add one. + assert mock_run.call_count >= 3 assert result is True @patch("subprocess.run") @@ -337,3 +352,43 @@ def test_run_lualatex_timeout(self, mock_run, compiler, mock_tex_path): ) assert result is False + + def test_verify_pdf_reports_page_count_and_latex_log_warnings( + self, tmp_path, monkeypatch, capsys + ): + """Successful compilation should summarize page count and common warnings.""" + + class PdfInfoRunner: + def run(self, command, **kwargs): + return subprocess.CompletedProcess( + command, + 0, + stdout="Title: test\nPages: 4\n", + stderr="", + ) + + tex_path = tmp_path / "paper.tex" + tex_path.write_text("\\documentclass{article}\n") + tex_path.with_suffix(".pdf").write_bytes(b"%PDF-1.4") + tex_path.with_suffix(".log").write_text( + "\n".join( + [ + "LaTeX Warning: Citation `missing' on page 1 undefined", + "LaTeX Warning: Reference `fig:test' on page 1 undefined", + "Overfull \\hbox (1.0pt too wide) in paragraph", + ] + ) + ) + monkeypatch.setattr( + "article_cli.latex_compiler.shutil.which", + lambda command: "/usr/bin/pdfinfo" if command == "pdfinfo" else None, + ) + compiler = LaTeXCompiler(Config(quiet=True), runner=PdfInfoRunner()) + + assert compiler._verify_pdf(tex_path, output_dir=None) is True + + output = capsys.readouterr().out + assert "PDF pages: 4" in output + assert "Undefined citations: 1" in output + assert "Undefined references: 1" in output + assert "Overfull boxes: 1" in output diff --git a/tests/test_project_context.py b/tests/test_project_context.py new file mode 100644 index 0000000..cf136d7 --- /dev/null +++ b/tests/test_project_context.py @@ -0,0 +1,65 @@ +""" +Tests for shared project context resolution. +""" + +import subprocess + +from article_cli.config import Config +from article_cli.project_context import ProjectContext + + +def test_project_context_resolves_configured_document_policy(tmp_path): + """Configured document, engine, shell escape, and output are resolved once.""" + (tmp_path / "paper.tex").write_text("\\documentclass{article}\n") + config_file = tmp_path / "pyproject.toml" + config_file.write_text( + """ +[tool.article-cli.documents] +main = "paper.tex" + +[tool.article-cli.latex] +engine = "xelatex" +shell_escape = true + +[tool.article-cli.workflow] +output_dir = "build" +""" + ) + + context = ProjectContext.resolve(Config(config_file, quiet=True), cwd=tmp_path) + + assert context.document == (tmp_path / "paper.tex").resolve() + assert context.document_name == "paper.tex" + assert context.engine == "xelatex" + assert context.output_dir_name == "build" + assert context.shell_escape is True + + +def test_project_context_detects_typst_documents(tmp_path): + """A detected Typst source selects the Typst engine.""" + (tmp_path / "main.typ").write_text("= Title\n") + + context = ProjectContext.resolve(Config(quiet=True), cwd=tmp_path) + + assert context.document == (tmp_path / "main.typ").resolve() + assert context.document_name == "main.typ" + assert context.engine == "typst" + + +def test_project_context_resolves_cli_document_relative_to_cwd(tmp_path): + """Explicit CLI document paths are relative to the user's current directory.""" + subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True) + subdir = tmp_path / "sections" + subdir.mkdir() + (tmp_path / "paper.tex").write_text("\\documentclass{article}\n") + (subdir / "paper.tex").write_text("\\documentclass{article}\n") + + context = ProjectContext.resolve( + Config(quiet=True), + cwd=subdir, + document="paper.tex", + ) + + assert context.project_root == tmp_path.resolve() + assert context.document == (subdir / "paper.tex").resolve() + assert context.document_name == "paper.tex" diff --git a/tests/test_release_service.py b/tests/test_release_service.py new file mode 100644 index 0000000..97e81f2 --- /dev/null +++ b/tests/test_release_service.py @@ -0,0 +1,193 @@ +""" +Tests for transactional release helpers. +""" + +import subprocess +from pathlib import Path + +from article_cli.config import Config +from article_cli.services.release import ( + ReleaseOptions, + ReleaseService, + validate_tag, + write_sha256, +) + + +class FakeGitManager: + """Capture release git operations without touching git.""" + + instances = [] + + def __init__(self, repo_root=None): + self.repo_root = Path(repo_root or ".").resolve() + self.calls = [] + self.existing_tags = set() + self.dirty = [] + self.instances.append(self) + + def tag_exists(self, tag): + self.calls.append(("tag_exists", tag)) + return tag in self.existing_tags + + def dirty_files(self, ignore_gitinfo=True): + self.calls.append(("dirty_files", ignore_gitinfo)) + return list(self.dirty) + + def create_tag(self, tag, force=False): + self.calls.append(("create_tag", tag, force)) + return True + + def list_releases(self, count=5): + self.calls.append(("list_releases", count)) + return True + + def delete_release(self, version, remote=False): + self.calls.append(("delete_release", version, remote)) + return True + + +def test_paper_tag_policy_accepts_short_paper_tags(): + """Paper releases may use v1, v1.0, or full SemVer tags.""" + assert validate_tag("v1", "paper") + assert validate_tag("v1.0", "paper") + assert validate_tag("v1.0.0", "paper") + assert validate_tag("v1.0.0-rc.1", "paper") + + +def test_semver_tag_policy_remains_strict(): + """Strict SemVer still rejects short paper tags.""" + assert not validate_tag("v1", "semver") + assert validate_tag("v1.0.0", "semver") + + +def test_release_dry_run_does_not_create_tag(tmp_path): + """A release dry run performs preflight but does not mutate tags.""" + service = ReleaseService( + Config(quiet=True), + repo_root=tmp_path, + manager_cls=FakeGitManager, + ) + + assert service.release(ReleaseOptions(tag="v1", dry_run=True)) is True + + assert FakeGitManager.instances[-1].calls == [ + ("tag_exists", "v1"), + ("dirty_files", True), + ("dirty_files", True), + ] + + +def test_release_blocks_dirty_tree_without_explicit_override(tmp_path): + """Dirty files stop a release unless allow_dirty is explicitly enabled.""" + service = ReleaseService( + Config(quiet=True), + repo_root=tmp_path, + manager_cls=FakeGitManager, + ) + FakeGitManager.instances[-1].dirty = ["M paper.tex"] + + assert service.release(ReleaseOptions(tag="v1")) is False + + assert ("create_tag", "v1", False) not in FakeGitManager.instances[-1].calls + + +def test_release_prints_rollback_guidance_after_post_tag_failure( + tmp_path, monkeypatch, capsys +): + """A failure after tag creation should tell the user how to roll it back.""" + (tmp_path / "paper.tex").write_text("\\documentclass{article}\n") + monkeypatch.setattr( + "article_cli.services.release.refresh_gitinfo2_metadata", + lambda repo_root: True, + ) + monkeypatch.setattr( + "article_cli.services.release.gitinfo2_metadata_summary", + lambda repo_root: "release v1", + ) + service = ReleaseService( + Config(quiet=True), + repo_root=tmp_path, + manager_cls=FakeGitManager, + ) + + assert ( + service.release( + ReleaseOptions( + tag="v1", + compile_pdf=False, + check_pdf=True, + checksum=False, + ) + ) + is False + ) + + assert ("create_tag", "v1", False) in FakeGitManager.instances[-1].calls + assert "Rollback local tag with: git tag -d v1" in capsys.readouterr().out + + +def test_release_summary_prints_assets_git_state_and_checksums( + tmp_path, monkeypatch, capsys +): + """Release summary should expose enough diagnostics to audit artifacts.""" + + class DiagnosticGitManager(FakeGitManager): + instances = [] + + def git(self, args): + command = tuple(args) + self.calls.append(("git", command)) + outputs = { + ("branch", "--show-current"): "main\n", + ("rev-parse", "--short", "HEAD"): "abc1234\n", + ( + "describe", + "--tags", + "--long", + "--always", + "--dirty=-*", + ): "v1-0-gabc1234\n", + ("describe", "--tags", "--exact-match"): "v1\n", + } + if command in outputs: + return subprocess.CompletedProcess(args, 0, outputs[command], "") + return subprocess.CompletedProcess(args, 1, "", "") + + class PdfInfoRunner: + def run(self, command, **kwargs): + return subprocess.CompletedProcess( + command, + 0, + stdout="Title: paper\nPages: 4\n", + stderr="", + ) + + pdf_path = tmp_path / "paper.pdf" + pdf_path.write_bytes(b"%PDF-1.4") + checksum_path = write_sha256(pdf_path) + monkeypatch.setattr( + "article_cli.services.release.shutil.which", + lambda command: "/usr/bin/pdfinfo" if command == "pdfinfo" else None, + ) + service = ReleaseService( + Config(quiet=True), + repo_root=tmp_path, + manager_cls=DiagnosticGitManager, + runner=PdfInfoRunner(), + ) + options = service._resolve_options( + ReleaseOptions(tag="v1", compile_pdf=False, check_pdf=False) + ) + + service._print_summary(options, pdf_path, checksum_path) + + output = capsys.readouterr().out + assert "Release git state:" in output + assert "Release tag: v1" in output + assert "Release dirty files: none" in output + assert "Release assets:" in output + assert f"PDF: {pdf_path}" in output + assert "PDF pages: 4" in output + assert f"Checksum: {checksum_path}" in output + assert "SHA256:" in output diff --git a/tests/test_repository_setup.py b/tests/test_repository_setup.py index a16db82..202626e 100644 --- a/tests/test_repository_setup.py +++ b/tests/test_repository_setup.py @@ -4,10 +4,55 @@ Tests project type support including article, presentation, and poster templates. """ +import subprocess +import json +import shutil import tempfile from pathlib import Path +import yaml + +from article_cli.git_hooks import refresh_gitinfo2_metadata +from article_cli.git_manager import GitManager from article_cli.repository_setup import RepositorySetup +from article_cli.template_renderer import TEMPLATE_VERSION + +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - exercised on Python < 3.11 + import tomli as tomllib # type: ignore[no-redef] + + +def init_git_repository(path: Path) -> None: + """Create a real git repository for hook tests.""" + subprocess.run(["git", "init"], cwd=path, check=True, capture_output=True) + subprocess.run( + ["git", "config", "user.email", "test@example.com"], + cwd=path, + check=True, + ) + subprocess.run( + ["git", "config", "user.name", "Test User"], + cwd=path, + check=True, + ) + + +def assert_generated_project_files_parse(root: Path) -> None: + """Assert generated structured project files are syntactically valid.""" + pyproject = tomllib.loads((root / "pyproject.toml").read_text()) + settings = json.loads((root / ".vscode" / "settings.json").read_text()) + workflow_text = (root / ".github" / "workflows" / "latex.yml").read_text() + workflow = yaml.safe_load(workflow_text) + + assert pyproject["tool"]["article-cli"]["template"]["version"] == TEMPLATE_VERSION + if "latex-workshop.latex.recipes" in settings: + assert settings["latex-workshop.latex.recipes"] + else: + assert settings["typst-lsp.exportPdf"] == "onSave" + assert workflow["jobs"]["build_document"] + assert "${{ github.ref_name }}" in workflow_text + assert "article-cli>=1.5.0" in workflow_text class TestRepositorySetupProjectTypes: @@ -35,6 +80,23 @@ def test_detect_or_create_tex_file_article_default(self): assert result == "main.tex" assert (Path(temp_dir) / "main.tex").exists() + def test_detect_or_create_typst_article_default(self): + """Test default typ filename for Typst article type""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup._detect_or_create_tex_file( + None, + "Test Title", + ["Author One"], + False, + "typst-article", + "", + "169", + ) + + assert result == "main.typ" + assert (Path(temp_dir) / "main.typ").exists() + def test_detect_or_create_tex_file_presentation_default(self): """Test default tex filename for presentation type""" with tempfile.TemporaryDirectory() as temp_dir: @@ -98,6 +160,51 @@ def test_get_article_template_multiple_authors(self): # Template includes authors list assert "Alice Smith" in template or "Multi-Author Article" in template + def test_get_article_template_lncs_style(self): + """Test article template with LNCS style""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + template = setup._get_article_template( + "LNCS Article", ["Alice Smith"], "lncs" + ) + + assert "\\documentclass[runningheads]{llncs}" in template + assert "\\bibliographystyle{splncs04}" in template + + def test_get_article_template_ieee_style(self): + """Test article template with IEEE style""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + template = setup._get_article_template( + "IEEE Article", ["Alice Smith"], "ieee" + ) + + assert "\\documentclass[conference]{IEEEtran}" in template + assert "\\bibliographystyle{IEEEtran}" in template + + def test_get_typst_article_template(self): + """Test Typst article template generation""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + template = setup._get_typst_article_template( + "Typst Article", ["Alice Smith"] + ) + + assert "// Typst Article" in template + assert '#bibliography("references.bib"' in template + + def test_get_custom_article_template(self, tmp_path): + """Test rendering a user-provided article template""" + custom_template = tmp_path / "custom.tex.j2" + custom_template.write_text("Title=[[ title ]]; Authors=[[ authors_display ]]") + setup = RepositorySetup(tmp_path) + + template = setup._get_article_template( + "Custom Article", ["Alice Smith"], template=str(custom_template) + ) + + assert template == "Title=Custom Article; Authors=Alice Smith" + class TestPresentationTemplate: """Test cases for presentation (Beamer) template generation""" @@ -206,8 +313,12 @@ def test_create_pyproject_article_type(self): assert pyproject_path.exists() content = pyproject_path.read_text() + parsed = tomllib.loads(content) assert "[tool.article-cli.project]" in content assert 'type = "article"' in content + assert ( + parsed["tool"]["article-cli"]["template"]["version"] == TEMPLATE_VERSION + ) def test_create_pyproject_presentation_type(self): """Test pyproject.toml generation for presentation type""" @@ -227,6 +338,7 @@ def test_create_pyproject_presentation_type(self): assert result is True pyproject_path = Path(temp_dir) / "pyproject.toml" content = pyproject_path.read_text() + parsed = tomllib.loads(content) assert "[tool.article-cli.project]" in content assert 'type = "presentation"' in content @@ -235,6 +347,7 @@ def test_create_pyproject_presentation_type(self): assert 'aspect_ratio = "169"' in content # Presentations should default to xelatex assert 'engine = "xelatex"' in content + assert parsed["tool"]["article-cli"]["latex"]["engine"] == "xelatex" def test_create_pyproject_poster_type(self): """Test pyproject.toml generation for poster type""" @@ -254,10 +367,39 @@ def test_create_pyproject_poster_type(self): assert result is True pyproject_path = Path(temp_dir) / "pyproject.toml" content = pyproject_path.read_text() + parsed = tomllib.loads(content) assert "[tool.article-cli.project]" in content assert 'type = "poster"' in content assert "[tool.article-cli.poster]" in content + assert parsed["tool"]["article-cli"]["project"]["type"] == "poster" + + def test_create_pyproject_typst_article_type(self): + """Test pyproject.toml generation for Typst article type""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup._create_pyproject( + "test-typst-article", + "Test Typst Article", + ["Author"], + "12345", + False, + "typst-article", + "", + "169", + style="lncs", + main_document="main.typ", + ) + + assert result is True + content = (Path(temp_dir) / "pyproject.toml").read_text() + parsed = tomllib.loads(content) + + assert 'type = "typst-article"' in content + assert "[tool.article-cli.typst]" in content + assert parsed["tool"]["article-cli"]["latex"]["engine"] == "typst" + assert parsed["tool"]["article-cli"]["project"]["style"] == "lncs" + assert parsed["tool"]["article-cli"]["documents"]["main"] == "main.typ" class TestReadmeGeneration: @@ -322,6 +464,26 @@ def test_create_readme_poster_type(self): assert "latexmk -xelatex poster.tex" in content assert "poster" in content.lower() + def test_create_readme_typst_article_type(self): + """Test README.md generation for Typst article type""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup._create_readme( + "test-typst", + "Test Typst", + ["Author"], + "main.typ", + False, + "typst-article", + "lncs", + ) + + assert result is True + content = (Path(temp_dir) / "README.md").read_text() + + assert "typst compile main.typ" in content + assert "Document style: `lncs`" in content + class TestVSCodeSettingsGeneration: """Test cases for VS Code settings generation""" @@ -340,8 +502,10 @@ def test_create_vscode_settings_article(self): assert settings_path.exists() content = settings_path.read_text() + settings = json.loads(content) assert "latexmk-pdf" in content assert '"-pdf"' in content + assert settings["latex-workshop.latex.recipes"][0]["name"] == "latexmk-pdf" def test_create_vscode_settings_presentation(self): """Test VS Code settings for presentation type (xelatex)""" @@ -355,9 +519,13 @@ def test_create_vscode_settings_presentation(self): assert result is True settings_path = vscode_dir / "settings.json" content = settings_path.read_text() + settings = json.loads(content) assert "latexmk-xelatex" in content assert '"-xelatex"' in content + assert ( + settings["latex-workshop.latex.recipes"][0]["name"] == "latexmk-xelatex" + ) def test_create_vscode_settings_poster(self): """Test VS Code settings for poster type (xelatex)""" @@ -371,9 +539,188 @@ def test_create_vscode_settings_poster(self): assert result is True settings_path = vscode_dir / "settings.json" content = settings_path.read_text() + settings = json.loads(content) assert "latexmk-xelatex" in content assert '"-xelatex"' in content + assert ( + settings["latex-workshop.latex.recipes"][0]["name"] == "latexmk-xelatex" + ) + + def test_create_vscode_settings_typst_article(self): + """Test VS Code settings for Typst article type""" + with tempfile.TemporaryDirectory() as temp_dir: + vscode_dir = Path(temp_dir) / ".vscode" + vscode_dir.mkdir() + + setup = RepositorySetup(Path(temp_dir)) + result = setup._create_vscode_settings(False, "typst-article") + + assert result is True + settings = json.loads((vscode_dir / "settings.json").read_text()) + + assert settings["typst-lsp.exportPdf"] == "onSave" + + +class TestGitHookSetup: + """Test cases for gitinfo2 hook generation and installation""" + + def test_create_git_hooks_creates_missing_hooks_directory(self, tmp_path): + """Test repository setup creates hooks/post-commit when hooks is missing""" + setup = RepositorySetup(tmp_path) + + result = setup._create_git_hooks(False) + + post_commit = tmp_path / "hooks" / "post-commit" + assert result is True + assert post_commit.exists() + assert "gitHeadInfo.gin" in post_commit.read_text() + assert post_commit.stat().st_mode & 0o111 + + def test_setup_hooks_bootstraps_missing_hooks_directory(self, tmp_path): + """Test article-cli setup creates source hooks before installing them""" + init_git_repository(tmp_path) + git_hooks_dir = tmp_path / ".git" / "hooks" + shutil.rmtree(git_hooks_dir) + + manager = GitManager(tmp_path) + + result = manager.setup_hooks() + + assert result is True + assert (tmp_path / "hooks" / "post-commit").exists() + for hook_name in ["post-commit", "post-checkout", "post-merge"]: + installed_hook = git_hooks_dir / hook_name + assert installed_hook.exists() + assert "gitHeadInfo.gin" in installed_hook.read_text() + assert installed_hook.stat().st_mode & 0o111 + + def test_setup_hooks_preserves_existing_user_hook(self, tmp_path): + """Test setup appends a managed block without overwriting user hooks""" + init_git_repository(tmp_path) + git_hooks_dir = tmp_path / ".git" / "hooks" + user_hook = git_hooks_dir / "post-commit" + user_hook.write_text("#!/bin/sh\necho user hook\n") + user_hook.chmod(0o755) + + manager = GitManager(tmp_path) + + result = manager.setup_hooks() + + content = user_hook.read_text() + assert result is True + assert "echo user hook" in content + assert "# >>> article-cli gitinfo2 hook >>>" in content + assert "gitHeadInfo.gin" in content + + def test_setup_hooks_is_idempotent(self, tmp_path): + """Test repeated setup updates managed hooks without duplicating blocks""" + init_git_repository(tmp_path) + manager = GitManager(tmp_path) + + assert manager.setup_hooks() is True + assert manager.setup_hooks() is True + + installed_hook = tmp_path / ".git" / "hooks" / "post-commit" + content = installed_hook.read_text() + assert content.count("# >>> article-cli gitinfo2 hook >>>") == 1 + assert content.count("# <<< article-cli gitinfo2 hook <<<") == 1 + + def test_setup_hooks_supports_linked_worktree(self, tmp_path): + """Test setup resolves git root and hook path in linked worktrees""" + main_repo = tmp_path / "main" + worktree_repo = tmp_path / "worktree" + main_repo.mkdir() + init_git_repository(main_repo) + (main_repo / "README.md").write_text("initial\n") + subprocess.run(["git", "add", "README.md"], cwd=main_repo, check=True) + subprocess.run( + ["git", "commit", "-m", "initial"], + cwd=main_repo, + check=True, + capture_output=True, + ) + subprocess.run( + ["git", "worktree", "add", str(worktree_repo)], + cwd=main_repo, + check=True, + capture_output=True, + ) + + manager = GitManager(worktree_repo) + + result = manager.setup_hooks() + + hook_path = subprocess.run( + ["git", "rev-parse", "--git-path", "hooks/post-commit"], + cwd=worktree_repo, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + installed_hook = Path(hook_path) + if not installed_hook.is_absolute(): + installed_hook = worktree_repo / installed_hook + + assert result is True + assert manager.repo_root == worktree_repo.resolve() + assert installed_hook.exists() + assert "gitHeadInfo.gin" in installed_hook.read_text() + + def test_refresh_gitinfo2_metadata_updates_local_copy(self, tmp_path): + """Test refresh runs the hook and copies gitHeadInfo.gin locally""" + git_dir = tmp_path / ".git" + hooks_dir = tmp_path / "hooks" + git_dir.mkdir() + hooks_dir.mkdir() + + hook = hooks_dir / "post-commit" + hook.write_text( + "#!/bin/sh\n" + "cat > .git/gitHeadInfo.gin <<'EOF'\n" + "\\usepackage[firsttagdescribe={v0.5.0}]{gitexinfo}\n" + "EOF\n" + ) + + result = refresh_gitinfo2_metadata(tmp_path) + + assert result is True + assert (tmp_path / "gitHeadLocal.gin").read_text() == ( + "\\usepackage[firsttagdescribe={v0.5.0}]{gitexinfo}\n" + ) + + def test_refresh_gitinfo2_metadata_ignores_dirty_local_copy(self, tmp_path): + """Test gitHeadLocal.gin alone does not mark the version dirty""" + subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True) + subprocess.run( + ["git", "config", "user.email", "test@example.com"], + cwd=tmp_path, + check=True, + ) + subprocess.run( + ["git", "config", "user.name", "Test User"], + cwd=tmp_path, + check=True, + ) + (tmp_path / "main.tex").write_text("\\documentclass{article}\n") + (tmp_path / "gitHeadLocal.gin").write_text("stale\n") + subprocess.run(["git", "add", "."], cwd=tmp_path, check=True) + subprocess.run( + ["git", "commit", "-m", "initial"], + cwd=tmp_path, + check=True, + capture_output=True, + ) + subprocess.run(["git", "tag", "v0.5.0"], cwd=tmp_path, check=True) + (tmp_path / "gitHeadLocal.gin").write_text("dirty local metadata\n") + + result = refresh_gitinfo2_metadata(tmp_path) + + content = (tmp_path / "gitHeadLocal.gin").read_text() + assert result is True + assert "firsttagdescribe={v0.5.0}" in content + assert "reltag={v0.5.0-0-" in content + assert "-*}" not in content class TestFullRepositoryInit: @@ -399,6 +746,34 @@ def test_init_article_repository(self): assert (Path(temp_dir) / ".gitignore").exists() assert (Path(temp_dir) / ".vscode" / "settings.json").exists() assert (Path(temp_dir) / ".github" / "workflows").exists() + assert_generated_project_files_parse(Path(temp_dir)) + + def test_init_typst_article_repository(self): + """Test full Typst article repository initialization""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup.init_repository( + title="Test Typst Article", + authors=["Author One"], + group_id="12345", + force=False, + project_type="typst-article", + style="lncs", + ) + + root = Path(temp_dir) + assert result is True + assert (root / "main.typ").exists() + assert_generated_project_files_parse(root) + + pyproject = tomllib.loads((root / "pyproject.toml").read_text()) + workflow = (root / ".github" / "workflows" / "latex.yml").read_text() + + assert pyproject["tool"]["article-cli"]["latex"]["engine"] == "typst" + assert pyproject["tool"]["article-cli"]["project"]["style"] == "lncs" + assert pyproject["tool"]["article-cli"]["documents"]["main"] == "main.typ" + assert "typst-community/setup-typst@v4" in workflow + assert "Compile Typst document" in workflow def test_init_presentation_repository(self): """Test full presentation repository initialization""" @@ -422,6 +797,7 @@ def test_init_presentation_repository(self): pyproject = (Path(temp_dir) / "pyproject.toml").read_text() assert 'type = "presentation"' in pyproject assert "[tool.article-cli.presentation]" in pyproject + assert_generated_project_files_parse(Path(temp_dir)) def test_init_poster_repository(self): """Test full poster repository initialization""" @@ -443,6 +819,41 @@ def test_init_poster_repository(self): pyproject = (Path(temp_dir) / "pyproject.toml").read_text() assert 'type = "poster"' in pyproject assert "[tool.article-cli.poster]" in pyproject + assert_generated_project_files_parse(Path(temp_dir)) + + def test_init_multi_document_repository_with_fonts(self): + """Test workflow and config rendering for output dirs, fonts, and extra docs.""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup.init_repository( + title="Test Multi Document", + authors=["Author"], + group_id="12345", + force=False, + project_type="presentation", + additional_documents=["poster.tex"], + output_dir="build", + fonts_dir="fonts", + install_fonts=True, + ) + + assert result is True + root = Path(temp_dir) + assert_generated_project_files_parse(root) + + pyproject = tomllib.loads((root / "pyproject.toml").read_text()) + workflow = (root / ".github" / "workflows" / "latex.yml").read_text() + + workflow_config = pyproject["tool"]["article-cli"]["workflow"] + assert workflow_config["output_dir"] == "build" + assert workflow_config["fonts_dir"] == "fonts" + assert workflow_config["install_fonts"] is True + assert workflow_config["runner_policy"] == "github" + assert workflow_config["bibliography"] == "off" + assert workflow_config["release"] == "github" + assert "Install custom fonts" in workflow + assert "poster.tex" in workflow + assert "poster.pdf" in workflow class TestConfigProjectMethods: diff --git a/tests/test_services.py b/tests/test_services.py new file mode 100644 index 0000000..3fd00bb --- /dev/null +++ b/tests/test_services.py @@ -0,0 +1,246 @@ +""" +Tests for article-cli service boundaries. +""" + +from pathlib import Path +from types import SimpleNamespace + +from article_cli.config import Config +from article_cli.services.bibliography import ( + BibliographyService, + BibliographyUpdateOptions, +) +from article_cli.services.compiler import CompileOptions, CompilerService +from article_cli.services.git import GitService +from article_cli.services.release import ReleaseService +from article_cli.services.workflow import WorkflowService + + +class FakeLatexCompiler: + """Capture LaTeX compiler calls.""" + + instances = [] + + def __init__(self, config): + self.config = config + self.calls = [] + self.instances.append(self) + + def compile(self, **kwargs): + self.calls.append(kwargs) + return True + + +class FakeTypstCompiler: + """Capture Typst compiler calls.""" + + instances = [] + + def __init__(self, config): + self.config = config + self.calls = [] + self.instances.append(self) + + def compile(self, **kwargs): + self.calls.append(kwargs) + return True + + +class FakeGitManager: + """Capture git manager calls.""" + + instances = [] + + def __init__(self, repo_root=None): + self.repo_root = Path(repo_root or ".").resolve() + self.calls = [] + self.instances.append(self) + + def clean_latex_files(self, extensions): + self.calls.append(("clean_latex_files", extensions)) + return True + + def setup_hooks(self, dry_run=False): + self.calls.append(("setup_hooks", dry_run)) + return True + + def create_release(self, version, auto_push=False, dry_run=False): + self.calls.append(("create_release", version, auto_push, dry_run)) + return True + + def tag_exists(self, tag): + self.calls.append(("tag_exists", tag)) + return False + + def dirty_files(self, ignore_gitinfo=True): + self.calls.append(("dirty_files", ignore_gitinfo)) + return [] + + def create_tag(self, tag, force=False): + self.calls.append(("create_tag", tag, force)) + return True + + def push_tag(self, tag): + self.calls.append(("push_tag", tag)) + return True + + def list_releases(self, count=5): + self.calls.append(("list_releases", count)) + return True + + def delete_release(self, version, remote=False): + self.calls.append(("delete_release", version, remote)) + return True + + +class FakeRepositorySetup: + """Capture repository setup calls.""" + + instances = [] + + def __init__(self, repo_path=None): + self.repo_path = repo_path + self.calls = [] + self.instances.append(self) + + def init_repository(self, **kwargs): + self.calls.append(kwargs) + return True + + +class FailingUpdater: + """Updater that should not be instantiated during dry runs.""" + + def __init__(self, **kwargs): + raise AssertionError( + "dry-run bibliography update should not instantiate updater" + ) + + +def reset_fakes(): + """Reset fake class state between tests.""" + FakeLatexCompiler.instances = [] + FakeTypstCompiler.instances = [] + FakeGitManager.instances = [] + FakeRepositorySetup.instances = [] + + +def test_compiler_service_dispatches_latex_with_shared_policy(tmp_path): + """Compiler service resolves project policy before calling the LaTeX compiler.""" + reset_fakes() + (tmp_path / "paper.tex").write_text("\\documentclass{article}\n") + config_file = tmp_path / "pyproject.toml" + config_file.write_text( + """ +[tool.article-cli.documents] +main = "paper.tex" + +[tool.article-cli.latex] +engine = "xelatex" +shell_escape = true + +[tool.article-cli.workflow] +output_dir = "build" +""" + ) + + service = CompilerService( + Config(config_file, quiet=True), + cwd=tmp_path, + latex_compiler_cls=FakeLatexCompiler, + git_manager_cls=FakeGitManager, + ) + + assert service.compile(CompileOptions(clean_first=True, clean_after=True)) is True + assert FakeLatexCompiler.instances[0].calls == [ + { + "tex_file": "paper.tex", + "engine": "xelatex", + "shell_escape": True, + "output_dir": "build", + "watch": False, + } + ] + assert len(FakeGitManager.instances) == 2 + assert all( + instance.calls[0][0] == "clean_latex_files" + for instance in FakeGitManager.instances + ) + + +def test_compiler_service_dispatches_typst(tmp_path): + """Compiler service dispatches Typst documents to the Typst compiler.""" + reset_fakes() + (tmp_path / "main.typ").write_text("= Title\n") + + service = CompilerService( + Config(quiet=True), + cwd=tmp_path, + typst_compiler_cls=FakeTypstCompiler, + ) + + assert service.compile(CompileOptions(font_paths=["fonts"], watch=True)) is True + assert FakeTypstCompiler.instances[0].calls == [ + { + "typ_file": "main.typ", + "output_dir": None, + "font_paths": ["fonts"], + "watch": True, + } + ] + + +def test_bibliography_service_dry_run_does_not_instantiate_updater(tmp_path): + """Bibliography dry runs validate configuration without touching Zotero.""" + config_file = tmp_path / "pyproject.toml" + config_file.write_text( + """ +[tool.article-cli.zotero] +api_key = "test-key" +group_id = "4709047" +output_file = "references.bib" +""" + ) + args = SimpleNamespace(api_key=None, user_id=None, group_id=None, output=None) + + service = BibliographyService( + Config(config_file, quiet=True), + updater_cls=FailingUpdater, + ) + + assert service.update(args, BibliographyUpdateOptions(dry_run=True)) is True + + +def test_git_release_and_workflow_services_delegate_to_implementation(): + """Thin services preserve existing implementation contracts.""" + reset_fakes() + + git_service = GitService(manager_cls=FakeGitManager) + release_service = ReleaseService(manager_cls=FakeGitManager) + workflow_service = WorkflowService(setup_cls=FakeRepositorySetup) + + assert git_service.setup_hooks(dry_run=True) is True + assert release_service.create("v1.0.0", auto_push=True, dry_run=True) is True + assert release_service.list(count=3) is True + assert release_service.delete("v1.0.0", remote=True) is True + assert ( + workflow_service.initialize_repository( + title="Title", + authors=["A. Author"], + project_type="typst-article", + additional_documents=["supplement.tex"], + output_dir="build", + ) + is True + ) + + assert FakeGitManager.instances[0].calls == [("setup_hooks", True)] + assert FakeGitManager.instances[1].calls == [ + ("tag_exists", "v1.0.0"), + ("dirty_files", True), + ("dirty_files", True), + ("list_releases", 3), + ("delete_release", "v1.0.0", True), + ] + assert FakeRepositorySetup.instances[0].calls[0]["project_type"] == "typst-article" + assert FakeRepositorySetup.instances[0].calls[0]["output_dir"] == "build" diff --git a/tests/test_template_renderer.py b/tests/test_template_renderer.py new file mode 100644 index 0000000..3527eca --- /dev/null +++ b/tests/test_template_renderer.py @@ -0,0 +1,49 @@ +from importlib.resources import files + +import pytest +from jinja2 import UndefinedError + +from article_cli.repository_setup import RepositorySetup +from article_cli.template_renderer import TemplateRenderer + + +def test_template_package_resources_are_available(): + """Template files should be importable package resources.""" + template_root = files("article_cli.templates") + + assert template_root.joinpath("article/main.tex.j2").is_file() + assert template_root.joinpath("article/main.typ.j2").is_file() + assert template_root.joinpath("article/lncs.tex.j2").is_file() + assert template_root.joinpath("github/latex.yml.j2").is_file() + + +def test_template_renderer_uses_strict_undefined(): + """Missing template context should fail loudly.""" + renderer = TemplateRenderer() + + with pytest.raises(UndefinedError): + renderer.render("article/main.tex.j2", {"title": "Missing Authors"}) + + +def test_template_renderer_write_statuses(tmp_path): + """Template writes should report created, skipped, and overwritten files.""" + renderer = TemplateRenderer() + target = tmp_path / "main.tex" + context = {"title": "Test", "authors_latex": "Author"} + + first = renderer.write("article/main.tex.j2", target, context) + second = renderer.write("article/main.tex.j2", target, context) + third = renderer.write("article/main.tex.j2", target, context, force=True) + + assert first.status == "created" + assert second.status == "skipped" + assert third.status == "overwritten" + + +def test_article_template_matches_golden_file(tmp_path): + """Representative article template output should remain stable.""" + setup = RepositorySetup(tmp_path) + rendered = setup._get_article_template("Golden Article", ["Alice", "Bob"]) + golden = (files("tests.golden").joinpath("article_main.tex")).read_text() + + assert rendered == golden diff --git a/tests/test_typst_compiler.py b/tests/test_typst_compiler.py index 5dc742c..f5d1a11 100644 --- a/tests/test_typst_compiler.py +++ b/tests/test_typst_compiler.py @@ -130,8 +130,11 @@ def test_compile_nonexistent_file(self, compiler, tmp_path): assert result is False @patch("subprocess.run") - def test_compile_once_success(self, mock_run, compiler, mock_typ_path, tmp_path): + def test_compile_once_success( + self, mock_run, compiler, mock_typ_path, tmp_path, monkeypatch + ): """Test successful Typst compilation""" + monkeypatch.setattr("article_cli.typst_compiler.shutil.which", lambda _: None) # Create mock PDF file pdf_file = tmp_path / "test.pdf" pdf_file.write_bytes(b"%PDF-1.4 mock pdf content") @@ -174,8 +177,11 @@ def test_compile_once_typst_not_found(self, mock_run, compiler, mock_typ_path): assert result is False @patch("subprocess.run") - def test_compile_with_font_paths(self, mock_run, compiler, mock_typ_path, tmp_path): + def test_compile_with_font_paths( + self, mock_run, compiler, mock_typ_path, tmp_path, monkeypatch + ): """Test compilation with custom font paths""" + monkeypatch.setattr("article_cli.typst_compiler.shutil.which", lambda _: None) pdf_file = tmp_path / "test.pdf" pdf_file.write_bytes(b"%PDF-1.4 mock pdf content") mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") @@ -190,6 +196,27 @@ def test_compile_with_font_paths(self, mock_run, compiler, mock_typ_path, tmp_pa assert "fonts/custom" in call_args assert result is True + @patch("subprocess.run") + def test_compile_once_reports_pdf_page_count( + self, mock_run, compiler, mock_typ_path, tmp_path, monkeypatch, capsys + ): + """Successful Typst compilation should report page count when available.""" + monkeypatch.setattr( + "article_cli.typst_compiler.shutil.which", + lambda command: "/usr/bin/pdfinfo" if command == "pdfinfo" else None, + ) + pdf_file = tmp_path / "test.pdf" + pdf_file.write_bytes(b"%PDF-1.4 mock pdf content") + mock_run.side_effect = [ + MagicMock(returncode=0, stdout="", stderr=""), + MagicMock(returncode=0, stdout="Pages: 3\n", stderr=""), + ] + + result = compiler._compile_once(mock_typ_path, None, []) + + assert result is True + assert "PDF pages: 3" in capsys.readouterr().out + class TestTypstCompilerDependencies: """Test cases for dependency checking""" @@ -248,14 +275,16 @@ def compiler(self): def mock_typ_file(self, tmp_path): """Create a mock .typ file""" typ_file = tmp_path / "presentation.typ" - typ_file.write_text(""" + typ_file.write_text( + """ #set page(paper: "presentation-16-9") #set text(size: 24pt) = Title Slide Hello, World! -""") +""" + ) return typ_file def test_compile_merges_config_font_paths(self, tmp_path): diff --git a/tests/test_workflow_ci.py b/tests/test_workflow_ci.py new file mode 100644 index 0000000..d20b0f7 --- /dev/null +++ b/tests/test_workflow_ci.py @@ -0,0 +1,107 @@ +""" +Tests for generated GitHub Actions workflow policy. +""" + +from pathlib import Path + +import yaml + +from article_cli.repository_setup import RepositorySetup + +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - exercised on Python < 3.11 + import tomli as tomllib # type: ignore[no-redef] + + +def _init_project(tmp_path: Path, **kwargs: object) -> tuple[str, dict]: + """Initialize a project and return workflow text plus parsed pyproject.""" + setup = RepositorySetup(tmp_path) + assert setup.init_repository( + title="Workflow Test", + authors=["A. Author"], + group_id="12345", + **kwargs, + ) + workflow_text = (tmp_path / ".github" / "workflows" / "latex.yml").read_text() + yaml.safe_load(workflow_text) + pyproject = tomllib.loads((tmp_path / "pyproject.toml").read_text()) + return workflow_text, pyproject + + +def test_default_workflow_is_portable_and_policy_driven(tmp_path): + """Default generated CI should use public runners and no Zotero secret.""" + workflow_text, pyproject = _init_project(tmp_path, project_type="article") + + workflow = yaml.safe_load(workflow_text) + workflow_config = pyproject["tool"]["article-cli"]["workflow"] + + assert workflow["jobs"]["workflow-setup"]["runs-on"] == "ubuntu-24.04" + assert workflow_config["runner_policy"] == "github" + assert workflow_config["bibliography"] == "off" + assert workflow_config["release"] == "github" + assert "/orgs/feelpp/actions/runners" not in workflow_text + assert "TOKEN_RUNNER" not in workflow_text + assert "self-texlive" not in workflow_text + assert "ZOTERO_API_KEY" not in workflow_text + assert "article-cli doctor --json" in workflow_text + assert "article-cli-doctor.json" in workflow_text + + +def test_workflow_can_opt_into_self_hosted_runner_and_bibliography_check(tmp_path): + """Self-hosted discovery and Zotero checks should be explicit opt-ins.""" + workflow_text, pyproject = _init_project( + tmp_path, + project_type="presentation", + ci_runner_policy="self-hosted-auto", + ci_self_hosted_org="cemosis", + ci_self_hosted_label="self-texlive", + ci_bibliography="check", + ci_artifact_includes=["./results/**"], + ) + + workflow_config = pyproject["tool"]["article-cli"]["workflow"] + + assert workflow_config["runner_policy"] == "self-hosted-auto" + assert workflow_config["self_hosted_org"] == "cemosis" + assert workflow_config["bibliography"] == "check" + assert workflow_config["artifact_includes"] == ["./results/**"] + assert "/orgs/cemosis/actions/runners" in workflow_text + assert "/orgs/feelpp/actions/runners" not in workflow_text + assert "TOKEN_RUNNER" in workflow_text + assert "article-cli bib update --check" in workflow_text + assert "./results/**" in workflow_text + + +def test_workflow_required_bibliography_fails_without_secret(tmp_path): + """The required bibliography policy should fail when the secret is absent.""" + workflow_text, pyproject = _init_project( + tmp_path, + project_type="article", + ci_bibliography="required", + ) + + workflow_config = pyproject["tool"]["article-cli"]["workflow"] + + assert workflow_config["bibliography"] == "required" + assert "ZOTERO_API_KEY is required" in workflow_text + assert "exit 1" in workflow_text + assert "article-cli bib update" in workflow_text + + +def test_workflow_release_job_can_be_disabled_for_typst(tmp_path): + """Typst workflows should parse and support release job opt-out.""" + workflow_text, pyproject = _init_project( + tmp_path, + project_type="typst-article", + ci_release_policy="off", + ) + + workflow = yaml.safe_load(workflow_text) + workflow_config = pyproject["tool"]["article-cli"]["workflow"] + + assert workflow_config["release"] == "off" + assert "release" not in workflow["jobs"] + assert "check" in workflow["jobs"] + assert "typst-community/setup-typst@v4" in workflow_text + assert "runs-on: ubuntu-24.04" in workflow_text diff --git a/tests/test_zotero_bibliography.py b/tests/test_zotero_bibliography.py new file mode 100644 index 0000000..33416c7 --- /dev/null +++ b/tests/test_zotero_bibliography.py @@ -0,0 +1,166 @@ +""" +Tests for deterministic Zotero BibTeX synchronization. +""" + +from pathlib import Path + +from article_cli.zotero import ( + ZoteroBibTexUpdater, + extract_bibtex_keys, + extract_citation_keys, +) + + +class FakeResponse: + """Minimal response object for Zotero updater tests.""" + + def __init__(self, text: str, total: int = 1, status_code: int = 200): + self.text = text + self.status_code = status_code + self.reason = "OK" if status_code < 400 else "temporary failure" + self.headers = {"Total-Results": str(total)} + + def raise_for_status(self): + """Raise a requests-compatible error for failed responses.""" + if self.status_code < 400: + return + + import requests + + error = requests.exceptions.HTTPError(self.reason) + error.response = self + raise error + + +class FakeSession: + """Return queued fake responses and record requests.""" + + def __init__(self, responses): + self.responses = list(responses) + self.calls = [] + + def get(self, url, params=None, timeout=30): + self.calls.append({"url": url, "params": params, "timeout": timeout}) + return self.responses.pop(0) + + +def make_updater(tmp_path: Path, responses) -> ZoteroBibTexUpdater: + """Create an updater with a fake session.""" + updater = ZoteroBibTexUpdater( + api_key="test-key", + user_id="1234", + output_file=str(tmp_path / "references.bib"), + ) + updater.session = FakeSession(responses) + updater.retry_backoff = 0 + return updater + + +def test_update_writes_deterministic_bibtex_and_skips_unchanged_file(tmp_path): + """Default output has no timestamp and does not rewrite unchanged content.""" + bibtex = """ +@misc{zeta, + title = {Zeta} +} + +@article{alpha, + title = {Alpha} +} +""" + updater = make_updater(tmp_path, [FakeResponse(bibtex, total=2)]) + + assert updater.update(backup=True) is True + + output = tmp_path / "references.bib" + content = output.read_text() + assert "% Generated:" not in content + assert "% Total entries: 2" in content + assert content.index("@article{alpha") < content.index("@misc{zeta") + + updater.session = FakeSession([FakeResponse(bibtex, total=2)]) + assert updater.update(backup=True) is True + assert not (tmp_path / "references.bib.backup").exists() + + +def test_update_check_reports_stale_bibliography_without_writing(tmp_path): + """Check mode reports stale content and leaves the file untouched.""" + output = tmp_path / "references.bib" + output.write_text("stale content\n") + updater = make_updater( + tmp_path, + [FakeResponse("@misc{fresh,\n title = {Fresh}\n}\n", total=1)], + ) + + assert updater.update(check=True, backup=True) is False + assert output.read_text() == "stale content\n" + assert not (tmp_path / "references.bib.backup").exists() + + +def test_collection_url_supports_zotero_collections_and_subcollections(tmp_path): + """Collection keys are encoded in the Zotero API URL.""" + updater = ZoteroBibTexUpdater( + api_key="test-key", + group_id="4709047", + collection_id="ABC123", + output_file=str(tmp_path / "references.bib"), + ) + + assert updater._build_url().endswith("/groups/4709047/collections/ABC123/items") + + +def test_include_local_can_write_separate_merged_output(tmp_path): + """Local manual entries can be merged without changing Zotero-only output.""" + local = tmp_path / "local_references.bib" + local.write_text("@misc{local,\n title = {Local}\n}\n") + merged = tmp_path / "references.all.bib" + updater = make_updater( + tmp_path, + [FakeResponse("@misc{zotero,\n title = {Zotero}\n}\n", total=1)], + ) + + assert ( + updater.update( + include_local=True, + local_file=str(local), + merged_output_file=str(merged), + backup=False, + ) + is True + ) + + zotero_only = (tmp_path / "references.bib").read_text() + merged_content = merged.read_text() + assert "zotero" in zotero_only + assert "local" not in zotero_only + assert "zotero" in merged_content + assert "local" in merged_content + assert f"% Local entries: {local}" in merged_content + + +def test_citation_completeness_detects_missing_keys(tmp_path): + """Citation checks report source citations missing from BibTeX.""" + source = tmp_path / "paper.tex" + source.write_text(r"\cite{known,missing}") + updater = make_updater( + tmp_path, + [FakeResponse("@misc{known,\n title = {Known}\n}\n", total=1)], + ) + + assert updater.update(check_citations=True, citation_sources=[source]) is False + assert extract_citation_keys(source) == {"known", "missing"} + assert extract_bibtex_keys((tmp_path / "references.bib").read_text()) == {"known"} + + +def test_transient_zotero_failures_are_retried(tmp_path, monkeypatch): + """Transient HTTP failures are retried before the update fails.""" + monkeypatch.setattr("article_cli.zotero.time.sleep", lambda _: None) + updater = make_updater( + tmp_path, + [ + FakeResponse("", total=1, status_code=503), + FakeResponse("@misc{retry,\n title = {Retry}\n}\n", total=1), + ], + ) + + assert updater.update(backup=False) is True + assert len(updater.session.calls) == 2 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..bd861cd --- /dev/null +++ b/uv.lock @@ -0,0 +1,1868 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] + +[[package]] +name = "article-cli" +version = "1.5.0" +source = { editable = "." } +dependencies = [ + { name = "jinja2" }, + { name = "pyyaml" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.33.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "build", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "build", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "flake8" }, + { name = "mypy" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-cov" }, + { name = "twine" }, + { name = "types-requests", version = "2.32.4.20260107", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "types-requests", version = "2.33.0.20260508", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "black" }, + { name = "build", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "build", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "flake8" }, + { name = "mypy" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-cov" }, + { name = "twine" }, + { name = "types-requests", version = "2.32.4.20260107", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "types-requests", version = "2.33.0.20260508", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = "==25.11.0" }, + { name = "build", marker = "extra == 'dev'", specifier = ">=1.2.0" }, + { name = "flake8", marker = "extra == 'dev'", specifier = ">=6.0" }, + { name = "jinja2", specifier = ">=3.1" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0,<2.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, + { name = "pytest-cov", marker = "extra == 'dev'" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "requests", specifier = ">=2.28.0" }, + { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.0" }, + { name = "twine", marker = "extra == 'dev'", specifier = ">=5.0.0" }, + { name = "types-requests", marker = "extra == 'dev'" }, +] +provides-extras = ["dev"] + +[package.metadata.requires-dev] +dev = [ + { name = "black", specifier = "==25.11.0" }, + { name = "build", specifier = ">=1.2.0" }, + { name = "flake8", specifier = ">=6.0" }, + { name = "mypy", specifier = ">=1.0,<2.0" }, + { name = "pytest", specifier = ">=7.0" }, + { name = "pytest-cov" }, + { name = "twine", specifier = ">=5.0.0" }, + { name = "types-requests" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "black" +version = "25.11.0" +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.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.9.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytokens" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/d2/6caccbc96f9311e8ec3378c296d4f4809429c43a6cd2394e3c390e86816d/black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e", size = 1743501, upload-time = "2025-11-10T01:59:06.202Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/b986d57828b3f3dccbf922e2864223197ba32e74c5004264b1c62bc9f04d/black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0", size = 1597308, upload-time = "2025-11-10T01:57:58.633Z" }, + { url = "https://files.pythonhosted.org/packages/39/8e/8b58ef4b37073f52b64a7b2dd8c9a96c84f45d6f47d878d0aa557e9a2d35/black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37", size = 1656194, upload-time = "2025-11-10T01:57:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/9c2267a7955ecc545306534ab88923769a979ac20a27cf618d370091e5dd/black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03", size = 1347996, upload-time = "2025-11-10T01:57:22.391Z" }, + { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, + { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, + { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" }, + { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9a/5b2c0e3215fe748fcf515c2dd34658973a1210bf610e24de5ba887e4f1c8/black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06", size = 1743063, upload-time = "2025-11-10T02:02:43.175Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/245164c6efc27333409c62ba54dcbfbe866c6d1957c9a6c0647786e950da/black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2", size = 1596867, upload-time = "2025-11-10T02:00:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/1a3859a7da205f3d50cf3a8bec6bdc551a91c33ae77a045bb24c1f46ab54/black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc", size = 1655678, upload-time = "2025-11-10T01:57:09.028Z" }, + { url = "https://files.pythonhosted.org/packages/56/1a/6dec1aeb7be90753d4fcc273e69bc18bfd34b353223ed191da33f7519410/black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc", size = 1347452, upload-time = "2025-11-10T01:57:01.871Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, +] + +[[package]] +name = "build" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and os_name == 'nt'" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pyproject-hooks", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/ec/bf5ae0a7e5ab57abe8aabdd0759c971883895d1a20c49ae99f8146840c3c/build-1.4.4.tar.gz", hash = "sha256:f832ae053061f3fb524af812dc94b8b84bac6880cd587630e3b5d91a6a9c1703", size = 89220, upload-time = "2026-04-22T20:53:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-py3-none-any.whl", hash = "sha256:8c3f48a6090b39edec1a273d2d57949aaf13723b01e02f9d518396887519f64d", size = 25921, upload-time = "2026-04-22T20:53:43.251Z" }, +] + +[[package]] +name = "build" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and os_name == 'nt'" }, + { name = "importlib-metadata", version = "9.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and python_full_version < '3.10.2'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pyproject-hooks", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and 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/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/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/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/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/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/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/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/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/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/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/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/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" }, + { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/ef725f8eb19b5a261b30f78efa9252ef9d017985cb499102f6f49834cd12/charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217", size = 299121, upload-time = "2026-04-02T09:28:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2f12878fbc680fbbb52386cd39a379801f62eaca74fc8b323381325f0f04/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5", size = 200612, upload-time = "2026-04-02T09:28:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b6/10c84e789126ca97d4a7228863a30481e786980a8b8cfcbf4f30658ca63c/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9", size = 221041, upload-time = "2026-04-02T09:28:17.554Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/c414866a138400b2e81973d006da7f694cfeaf895ef07d2cba9a8743841a/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a", size = 216323, upload-time = "2026-04-02T09:28:18.863Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/bdcf94997e06b223d826df3abed45a5ad6e17f609b7df9d25cd23b5bde30/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc", size = 208419, upload-time = "2026-04-02T09:28:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/1a/64/3f9142293c88b1b10e199649ed1330f070c2a68e305335a5819fa7f25fa7/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00", size = 195016, upload-time = "2026-04-02T09:28:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d1/d8a6b7dd5c5636b76ce0d080bc57d8e56c7bbd6bc2ac941529a35e41d84a/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776", size = 206115, upload-time = "2026-04-02T09:28:23.259Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8c/60ebe912379627d023eb96995b40bc50308729f210f43d66109ca0a7bbd2/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319", size = 204022, upload-time = "2026-04-02T09:28:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2a/41816ceda78a551cbfdfbeab6f3891152b0e3f758ce6580c2c18c829f774/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24", size = 195914, upload-time = "2026-04-02T09:28:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9b/7c7f4b7f11525fcbdfba752455314ac60646bae91cdd671d531c1f7a97c6/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42", size = 222159, upload-time = "2026-04-02T09:28:27.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/301682e7469bdbfa2ce219a804f0668b2266ab8520570d85d3b3ef483ea3/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4", size = 206154, upload-time = "2026-04-02T09:28:28.848Z" }, + { url = "https://files.pythonhosted.org/packages/20/ec/90339ff5cdc598b265748c1f231c7d7fbd9123a92cee10f757e0b1448de4/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67", size = 217423, upload-time = "2026-04-02T09:28:30.248Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e7/a7a6147f8e3375676309cf584b25c72a3bab784ea4085b0011fa07b23aeb/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274", size = 210604, upload-time = "2026-04-02T09:28:31.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/62/d9340c7a79c393e57807d7fb6c57e82060687891f81b74d3201958b919c1/charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366", size = 144631, upload-time = "2026-04-02T09:28:33.158Z" }, + { url = "https://files.pythonhosted.org/packages/21/e7/92901117e2ddc8facfe8235a3ecd4eb482185b2ad5d5b6606b37c1afea06/charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444", size = 154710, upload-time = "2026-04-02T09:28:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4f/e1fb138201ad9a32499dd9a98aa4a5a5441fbf7f56b52b619a54b7ee8777/charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c", size = 143716, upload-time = "2026-04-02T09:28:35.908Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, + { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "47.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version <= '3.9'", +] +dependencies = [ + { name = "cffi", marker = "python_full_version <= '3.9' and platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version <= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" }, + { url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" }, + { url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/81/75/d691e284750df5d9569f2b1ce4a00a71e1d79566da83b2b3e5549c84917f/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", size = 4587867, upload-time = "2026-04-24T19:54:40.619Z" }, + { url = "https://files.pythonhosted.org/packages/07/d6/1b90f1a4e453009730b4545286f0b39bb348d805c11181fc31544e4f9a65/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", size = 4627192, upload-time = "2026-04-24T19:54:42.849Z" }, + { url = "https://files.pythonhosted.org/packages/dc/53/cb358a80e9e359529f496870dd08c102aa8a4b5b9f9064f00f0d6ed5b527/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", size = 4587486, upload-time = "2026-04-24T19:54:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/8b/57/aaa3d53876467a226f9a7a82fd14dd48058ad2de1948493442dfa16e2ffd/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", size = 4626327, upload-time = "2026-04-24T19:54:47.813Z" }, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version > '3.9' and python_full_version < '3.10'", +] +dependencies = [ + { name = "cffi", marker = "python_full_version > '3.9' and platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version > '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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/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.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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 = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + +[[package]] +name = "id" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, +] + +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "zipp", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +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/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]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools", version = "10.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "more-itertools", version = "11.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/7b/c3081ff1af947915503121c649f26a778e1a2101fd525f74aef997d75b7e/jaraco_context-6.1.1.tar.gz", hash = "sha256:bc046b2dc94f1e5532bd02402684414575cc11f565d929b6563125deb0a6e581", size = 15832, upload-time = "2026-03-07T15:46:04.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/49/c152890d49102b280ecf86ba5f80a8c111c3a155dafa3bd24aeb64fde9e1/jaraco_context-6.1.1-py3-none-any.whl", hash = "sha256:0df6a0287258f3e364072c3e40d5411b20cafa30cb28c4839d24319cecf9f808", size = 7005, upload-time = "2026-03-07T15:46:03.515Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version >= '3.10' and python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools", version = "10.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "more-itertools", version = "11.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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 = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", version = "9.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context", version = "6.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jaraco-context", version = "6.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", version = "3.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and sys_platform == 'linux'" }, + { name = "secretstorage", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "librt" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/cb/c1945e506893b5b8577fb45a60c80e3ffe4a82092a04a6f29b0b951d9a24/librt-0.10.0.tar.gz", hash = "sha256:1aba1e8aa4e3307a7be68a74149545fde7451964dc0235a8bec5704a17bdda42", size = 191799, upload-time = "2026-05-05T16:31:23.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/18/827e5c1262a88c2602e86f99aee0f288ffea3280dbd2ff448858ef9dc6e9/librt-0.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dc99f9642100b86e5f6bb14cdc9970009e31a9ef7d64df6704b7018451524a3", size = 76461, upload-time = "2026-05-05T16:29:00.422Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/54254e30287f5a5abec6fef22d976987476e966be5fdff51fe8c2d5d73d1/librt-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8298cedfcfaff3790000bd057aaaa3df1b0ab54cf7b48eeab16184cbb1bc66b9", size = 79740, upload-time = "2026-05-05T16:29:01.926Z" }, + { url = "https://files.pythonhosted.org/packages/4c/20/e93264b52113669d98d3b63ff94d4ce0c4dd49ae0503f1788440a884e5f0/librt-0.10.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7dbe312dbf76468255b79a7ba311236fde620f2f7055fc09d421e31340314e", size = 243472, upload-time = "2026-05-05T16:29:03.373Z" }, + { url = "https://files.pythonhosted.org/packages/35/ad/34a5141178e8b18a4cfa45d1a0d523c84397e2abd5d06fea2d846da687e8/librt-0.10.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:56ed90c48c19249012dadfd79a1bc13bd5168ea60a70722d330a3a600c0b1852", size = 232073, upload-time = "2026-05-05T16:29:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/97/1f/67240e910cd9f9ab1498c1470738345fc29dce5dc9719db1e0e09d1e861f/librt-0.10.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d74ca0f4b2b09c117f913d4df01f6b934dff8a271096b35167d5264a31649f0", size = 256956, upload-time = "2026-05-05T16:29:06.516Z" }, + { url = "https://files.pythonhosted.org/packages/22/50/3a2b3482c27d607f6e8216d913c6bc592b9a2141d96990309452340a78e3/librt-0.10.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8eb2daa9375f93c0e55ff5e44a4bbe98f39e5fe52e1abf9c97acb67743b61bf8", size = 250593, upload-time = "2026-05-05T16:29:08.324Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1c/07dba133d79f93322fa17514062f1a2a50d6bdfb7baec4acf78193d7fad1/librt-0.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7b09b90e634e6dff57978cd358070046071e2b120501f10787aeb35425f504f6", size = 263582, upload-time = "2026-05-05T16:29:09.866Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/033f2c6d6ab0b48f15f02e5bf065521b11a51922806017f8b6274df30d69/librt-0.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2cf22fd379d60c739b800d4295ed34045f8b04aa8df9c12bd2f8f43f7fe672b7", size = 259307, upload-time = "2026-05-05T16:29:11.675Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/679046cd75d5a52c0104c890d8f69574ef4e619c683e59c15584d03a2457/librt-0.10.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:74c798793fcf29a84d442278ebe0bb1fff79fe58ac4106eeff7019cbba861423", size = 257342, upload-time = "2026-05-05T16:29:13.14Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d5/dbaac9c0884f78a53dda22b9ec92bb788e1400e762ed7623fa96928c8da5/librt-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc4f1573401e8dbe6c26511fe027620b0fb30ae9a7ab814e02e510626b8b5f9c", size = 280141, upload-time = "2026-05-05T16:29:14.922Z" }, + { url = "https://files.pythonhosted.org/packages/cc/81/71f18cf8eb340d9fda011498870910f6a8697aeb50833005d3d8107653fd/librt-0.10.0-cp310-cp310-win32.whl", hash = "sha256:e1428275f5fe3d4db6822e58d8b005a5b28ffca55e8433ebc051247fbe46429f", size = 62257, upload-time = "2026-05-05T16:29:16.226Z" }, + { url = "https://files.pythonhosted.org/packages/df/52/6bcebc2f870c4836bcb372be885fae7f17a1d25037d3a8250ef79fbe0124/librt-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:0708e9408f585b0f065081680583a577652099680ccf820c7538904322b679c3", size = 70321, upload-time = "2026-05-05T16:29:17.41Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a3/1472717d2325adacc8d335ba2e4078015c09d75b599f3cf48e967b3d306e/librt-0.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01b4500ca3a625450c032a9142a8e843923ce263fa8a92ad1b38927cabe2fe72", size = 76045, upload-time = "2026-05-05T16:29:18.731Z" }, + { url = "https://files.pythonhosted.org/packages/a6/31/bfe32355d4b369aef3d7aa442df663bb5558c2ffa2de286cb2956346bc24/librt-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b7e42d1b3e300d20bfc87e72ffd62f0a92a2cb3c35f7bf90df90c9d2a49f74c", size = 79466, upload-time = "2026-05-05T16:29:20.052Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f1/83f8a2c715ba2cac9b7387a5a5cea25f717f7184320cfe48b36bed9c58e9/librt-0.10.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8ef7b8c61ce3a1b597cd3e15348ff1574325165c2e7ce09a718154cde2a7950", size = 242283, upload-time = "2026-05-05T16:29:21.596Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/c3a4ce94857f0004a542f86662806383611858f522722db58efaec0a1472/librt-0.10.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e73c84f72d1fa0d6eaa7a1930b436ba8d2c90c58d77bfabb09995a69ad35f6c0", size = 230735, upload-time = "2026-05-05T16:29:23.335Z" }, + { url = "https://files.pythonhosted.org/packages/d1/41/e962bb26c7728eb7b3a69e490d0c800fd9968a6970e390c1f18ddb56093d/librt-0.10.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9728cb98713bd862fb8f4fd6a642d1896c86058a41d77c70f3d5cee75e725275", size = 256606, upload-time = "2026-05-05T16:29:24.91Z" }, + { url = "https://files.pythonhosted.org/packages/66/3a/4e46a707b1ecc993fd691071623b9beab89703a63bd21cc7807e06c28209/librt-0.10.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:648b7e941d20acd72f9652115e0e53facd98156d61f9ebf7a812bdef8bdccea9", size = 249739, upload-time = "2026-05-05T16:29:26.648Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f5/dc5b7eb294656ad23d4ff4cf8514208d54fe1026b909d726a0dc026689c9/librt-0.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c3e33747c068e86a9007c20fdb777eb5ba8d3d19136d7812f88e69a713041b6f", size = 261414, upload-time = "2026-05-05T16:29:28.702Z" }, + { url = "https://files.pythonhosted.org/packages/58/e4/990ed8d12c7f114ac8f8ccd47f7d9bd9704ef61acfcb1df4a05047da7710/librt-0.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d509c745bf7e77d1107cf05e6abb249dc03fad13eb39f2286a49deedaeb2bcd7", size = 256614, upload-time = "2026-05-05T16:29:30.357Z" }, + { url = "https://files.pythonhosted.org/packages/60/eb/52d2726c7fb22818507dc3cc166c8f36dd4a4b68a7be67f12006ac8777c1/librt-0.10.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:786ad5a15e99d0e0e74f3adbeecc198a5ac58f340be07e984723d1e0074838de", size = 255144, upload-time = "2026-05-05T16:29:32.106Z" }, + { url = "https://files.pythonhosted.org/packages/bc/df/bd5591a78f7531fce4b6eb9962aadc6adc9560a01570442a884b6e554abe/librt-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:075582d877a97ee3d8e77bda3689dbe617b14f6469224a2d80b4b6c38e3951aa", size = 279121, upload-time = "2026-05-05T16:29:33.688Z" }, + { url = "https://files.pythonhosted.org/packages/fd/df/7c2b838dfc89a1762dd156d8b0c39848a7a2845d725a50be5a6e021fb8ba/librt-0.10.0-cp311-cp311-win32.whl", hash = "sha256:75ecdc3f5a90065aa2af2e574706c5495adc392520762dcf10b1aa716f0b8090", size = 62593, upload-time = "2026-05-05T16:29:35.152Z" }, + { url = "https://files.pythonhosted.org/packages/91/19/22ff572981049a9d436a083dbea1572d0f5dc068b7353637d2dd9977c8f1/librt-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:b6f6084884131d8a52cb9d7095ff2aa52c1e786d9fdaefab1fb4515415e9e083", size = 70914, upload-time = "2026-05-05T16:29:36.407Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/1697cc64f4a5c7e9bce55e99c6d234a346beaedaefcd1e2ca90dd285f98c/librt-0.10.0-cp311-cp311-win_arm64.whl", hash = "sha256:0140bd62151160047e89b2730cb6f8506cdac5127baa1afb9231e4dd3fe7f681", size = 61176, upload-time = "2026-05-05T16:29:37.62Z" }, + { url = "https://files.pythonhosted.org/packages/12/8e/cbb5b6f6e45e65c10a42449a69eaccc44d73e6a081ea752fbc5221c6dc1c/librt-0.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b4b58a44b407e91f633dafee008de9ddea6aa2a555ed94929c099260910bd0ba", size = 77327, upload-time = "2026-05-05T16:29:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3d/8233cbee8e99e6a8992f02bfc2dec8d787509566a511d1fde2574ee7473f/librt-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:950b79b11762531bdf45a9df909d2f9a2a8445c70c88665c01d14c8511a27dc5", size = 79971, upload-time = "2026-05-05T16:29:40.96Z" }, + { url = "https://files.pythonhosted.org/packages/87/6f/5264b298cef2b72fc97d2dde56c66181eda35204bf5dcd1ed0c3d0a0a782/librt-0.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4538453f51be197633b425912c150e25b0667252d3741c53e8368176d98d9d37", size = 246559, upload-time = "2026-05-05T16:29:42.701Z" }, + { url = "https://files.pythonhosted.org/packages/07/7b/19b1b859cc60d5f99276cc2b3144d91556c6d1b1e4ebb50359696bebf7a8/librt-0.10.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:70b955f091beac93e994a0b7ec616934f63b3ea5c3d6d7af847562f935aceca7", size = 235216, upload-time = "2026-05-05T16:29:44.193Z" }, + { url = "https://files.pythonhosted.org/packages/6e/56/a2f40717142a8af46289f57874ef914353d8faccd5e4f8e594ab1e16e8c7/librt-0.10.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:483e685e06b6163728ba6c85d74315176be7190f432ec2a41226e5e14355d5f0", size = 263108, upload-time = "2026-05-05T16:29:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/ca/15c625c3bdc0167c01e04ef8878317e9713f3bfa788438342f7a94c7b22c/librt-0.10.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ac53d946a009d1a38c44a60812708c9458fb2a239a5f630d8e625571386650f", size = 255280, upload-time = "2026-05-05T16:29:48.087Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c5/ba301d571d9e05844e2435b73aba30bee77bb75ce155c9affcfd2173dd03/librt-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc8771c9fcf0ea894ca41fdc2abd83572c2fbda221f232d86e718614e57ff513", size = 268829, upload-time = "2026-05-05T16:29:49.628Z" }, + { url = "https://files.pythonhosted.org/packages/8b/60/af70e135bc1f1fe15dd3894b1e4bbefc7ecdf911749a925a39eb86ceb2a1/librt-0.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:70805dbc5257892ac572f86290a61e3c8d90224ecce1a8b2d1f7ed51965417f4", size = 262051, upload-time = "2026-05-05T16:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/c2/c8236eb8b421bac5a172ba208f965abaa89805da2a3fa112bdf1764caf8f/librt-0.10.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d3b4f300f7bcba6e2ff73fb8bef1898479e9772bfa2682998c636391633ec826", size = 264347, upload-time = "2026-05-05T16:29:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/15b6d32bc25dacd4a60886a683d8128d6219910c122202b995a40dd4f8d2/librt-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:943bc943f92f4fb3408fae62485c6a3ad68ce4f2ee205643a39641525c19a276", size = 286482, upload-time = "2026-05-05T16:29:54.675Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8e/b1b959bacd323eb4360579db992513e1406d1c6ef7edb57b5511fd0666fd/librt-0.10.0-cp312-cp312-win32.whl", hash = "sha256:6065c1a758fba1010b41401013903d3d5d2750eab425ddedd584abac31d0630e", size = 62955, upload-time = "2026-05-05T16:29:56.39Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4c/d4cd6e4b9fc24098e63cc85537d1b6689682aee96809c38f08072067cc2b/librt-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:d788ecbe208ab352dab0e105cc06057bf9a2fc7e58cabb0d751ad9e30062b9e2", size = 71191, upload-time = "2026-05-05T16:29:57.682Z" }, + { url = "https://files.pythonhosted.org/packages/2b/19/8641da1f63d24b92354a492f893c022d6b3a0df44e70c8eff49364613983/librt-0.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:6003d1f295bdba02656dc81308208fc060d0a51d8c0d0a6db70f7f3c57b9ba0a", size = 61432, upload-time = "2026-05-05T16:29:58.971Z" }, + { url = "https://files.pythonhosted.org/packages/e5/29/681a75c82f4cc90d29e4b257a3299b79fe13fe927a04c57b8109d70b6957/librt-0.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0ede79d682e73f91c1b599a76d78b7464b9b5d213754cedb13372d9df36e596", size = 77299, upload-time = "2026-05-05T16:30:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/62/24/0c7ca445a55d04be79cac19819437fd094782347fa116f6681844fa6143e/librt-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0ba0b131fdb336c8b9c948e397f4a7e649d0f783b529f07b647bf4961df392e", size = 79930, upload-time = "2026-05-05T16:30:01.555Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/1e2b8f6443ef9e9a81e89486ca70e22f3684f93db003ce6eaefc3d0839b9/librt-0.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2728117da2afb96fb957768725ee43dc9a2d73b031e02da424b818a3cdd3a275", size = 246195, upload-time = "2026-05-05T16:30:03.261Z" }, + { url = "https://files.pythonhosted.org/packages/74/61/9dc9e03de0439ad84c1c240aac8b747f12c90cb797ea6042f7bdb8d3410f/librt-0.10.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:723ba80594c49cdf0584196fc430752262605dc9449902fc9bd3d9b79976cb77", size = 234951, upload-time = "2026-05-05T16:30:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/635223117d7590875bca441275065a3bf491203ad4208bd1cc3ffd90c5a1/librt-0.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7292edaaca294a61a978c53a3c7d6130d099b0dfbc8f0a65916cdc6b891b9852", size = 262768, upload-time = "2026-05-05T16:30:06.638Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/b04152d0cd8b6ca2b428a8bd3230343230c35ed304a932f35b5375f2f828/librt-0.10.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89fe9d539f2c10a1666633eeeac507ce95dd06d9ecc58de3c6390dba156a3d3a", size = 255075, upload-time = "2026-05-05T16:30:08.216Z" }, + { url = "https://files.pythonhosted.org/packages/35/1e/25bac4c7f2ca36f0e612cade186970683cf79153d96beccc3a11a9e19b97/librt-0.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4efa7b9587503fa5b67f40593302b9c8836d211d222ff9f7cafe67be5f8f0b10", size = 268559, upload-time = "2026-05-05T16:30:10.1Z" }, + { url = "https://files.pythonhosted.org/packages/18/54/4601faab35b6632a13200faa146ca62bfd111ffbe2568be430d65c89493a/librt-0.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:22dc982ef59df0136df36092ccbdbb570ced8aafb33e49585739b2f1de1c13b6", size = 261753, upload-time = "2026-05-05T16:30:11.912Z" }, + { url = "https://files.pythonhosted.org/packages/1b/cf/39f4023509e94fade8b074666fa3292db9cb6b34ea5dcbe7af53df9fca1d/librt-0.10.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6f2e5f3606253a84cea719c94a3bb1c54487b5d617d0254d46e0920d8a06be3f", size = 264055, upload-time = "2026-05-05T16:30:13.465Z" }, + { url = "https://files.pythonhosted.org/packages/8e/00/40247209fc46a8e308a91412d5206aedf8efb667ee89eb625820106a5c2f/librt-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40884bfaa1e29f6b6a9be255007d8f359bfc9e61d68bdef8ed3158bfcbc95df9", size = 286190, upload-time = "2026-05-05T16:30:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/5566beb94431a985abe1787af5ef86e087750172ff9d0bbf20f93e88132d/librt-0.10.0-cp313-cp313-win32.whl", hash = "sha256:3cd34cd8254eba756660bff6c2da91278248184301054fe3e4feb073bdd49b14", size = 62949, upload-time = "2026-05-05T16:30:16.503Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c2/3ea3301d6c8dff51d39dbe8ed75db3dc92896947d4afb5eeadf821c1e67f/librt-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:7baac5313e2d8dce1386f97777a8d03ab28f5fe1e780b3b9ac2ee7544551fedc", size = 71152, upload-time = "2026-05-05T16:30:17.766Z" }, + { url = "https://files.pythonhosted.org/packages/3c/de/5d49cb92cadcbc77d3abc27b93fd6030ed8437487dde2eae38cab5e6704d/librt-0.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:afc5b4406c8e2515698d922a5c7823a009312835ea58196671fff40e35cb8166", size = 61336, upload-time = "2026-05-05T16:30:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/7165e08108cc185a13a9c069f0685e6ef92e70e07fddf7edf5e7348c6316/librt-0.10.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f09588a30e6a22ec624090d72a3ab1a6d4d5485c3ed739603e76aa3c16efa688", size = 76794, upload-time = "2026-05-05T16:30:20.392Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ef/bf8613febf651b90c5222ee79dea5ae58d4cc2b544df69d3033424448934/librt-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:131ade118d12bd7a0adc4e655474a553f1b76cf78385868885944d21d51e45e0", size = 79662, upload-time = "2026-05-05T16:30:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/b6/67/9eddd165c1d8397bdf99b38bf12b5a55b3def5035b49eedb49f2775d1430/librt-0.10.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8b9ab28e40d011c373a189eae900c916e66d6fbecf7983e9e4883089ee085ef", size = 242390, upload-time = "2026-05-05T16:30:23.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/d1/d95da80334501866cd37004ab5d7483220d05862fab4b5405394f0264f0d/librt-0.10.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:67c39bb30da73bae1f293d1ed8bc2f8f6642649dd0928d3600aeff3041ac23d6", size = 232603, upload-time = "2026-05-05T16:30:25.198Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fa/e6d64d28718bc1be4e1736fcb037ca1c4dfca927e7167df75a7d5215665e/librt-0.10.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c3273c6b774614f093c8927c2bf1b077d0fefde988fe98f46a333734e5597ab", size = 259187, upload-time = "2026-05-05T16:30:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/72/3f/3fdb77e7f937dad59cfd76b720be7e7643400ec76b2da35befab8d66ba30/librt-0.10.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9dd7c1b86a4baa583ab5db977484b93a2c474e69e96ef3e9538387ea54229cb9", size = 251846, upload-time = "2026-05-05T16:30:28.56Z" }, + { url = "https://files.pythonhosted.org/packages/18/ca/f4d49133dd86a6f55d79eca30bf412fa722f511a9abe67f62f57aa64e66a/librt-0.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a77385c5a202e831149f7ad03be9e67cf80e957e52c614e83dcb822c95222eb8", size = 264936, upload-time = "2026-05-05T16:30:30.491Z" }, + { url = "https://files.pythonhosted.org/packages/de/66/a8df2fbadc1f6c1827a096d11c40175bd526133480bd3bc88ec64a03d257/librt-0.10.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c6a5eafa74b5655bad59886138ed68426f098a6beb8cb95a71f2cc3cd8bb33fe", size = 258699, upload-time = "2026-05-05T16:30:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/1e3c83613fe05451bb969e27b68a573d177f08d5f63533cc29fec0989658/librt-0.10.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1fc93d0439204c50ab4d1512611ce2c206f1b369b419f69c7c27c761561e3291", size = 259825, upload-time = "2026-05-05T16:30:35.077Z" }, + { url = "https://files.pythonhosted.org/packages/09/24/5e2f926ee9d3ef348d9339526d7062abb5c44d8419e3179528c01d78c102/librt-0.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:79e713c178bc7a744adfbee6b4619a288eecc0c914da2a9313a20255abe2f0cf", size = 282548, upload-time = "2026-05-05T16:30:36.639Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7d/3e89ed6ad0162561fa8bef9df3195e24263104c955713cd0237d3711fad2/librt-0.10.0-cp314-cp314-win32.whl", hash = "sha256:2eba9d955a68c41d9f326be3da42f163ec3518b7ab20f1c826224e7bed71e0bf", size = 58970, upload-time = "2026-05-05T16:30:38.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/579e731c94a7086a268bfa3e7a4945cd47836bebd3cbf3faeafd2e7eaef9/librt-0.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbfaf7f5145e9917f5d18bffa298eff6a19d74e7b8b11dabdca95785befe8dbf", size = 67260, upload-time = "2026-05-05T16:30:39.804Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f8/235822b7ae0b2334f12ee18bcf2476d07924077a5efeea57dbe927704be2/librt-0.10.0-cp314-cp314-win_arm64.whl", hash = "sha256:8d6d385d1969849a6b1397114df22714b6ded917bada98668e3e974dc663477e", size = 57156, upload-time = "2026-05-05T16:30:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e3/9b919cbf1e8eb770bf91bb7df28125e0f1daf4587169afefd95402636e9a/librt-0.10.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:6c3a82d3bd32631ef5c79922dfc028520c9ad840255979ab4d908271818039ee", size = 79150, upload-time = "2026-05-05T16:30:42.761Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f5/72a944aa3bc3498169a168087eff58ca48b58bf1b704e59d091fd30739f3/librt-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d64cc66005dc324c9bb1fa3fc2841f529002f6eb15966d55e46d430f56955a6a", size = 82304, upload-time = "2026-05-05T16:30:44.082Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e3/fcc290a33e295019759472dfa794d204e43504b276ac65eab7fd9da20ea3/librt-0.10.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb562cd28c88cd2c6a9a6c78f99dc39348d6b16c94adc25de0e574acf1176e9", size = 272556, upload-time = "2026-05-05T16:30:45.497Z" }, + { url = "https://files.pythonhosted.org/packages/fd/54/546975e4c997573885e7f040a05012f8838e06fb12b0c3c1fbb76254e9d7/librt-0.10.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b809aa2854d019c28773b03605df22adc675ee4f3f4402d673581313e8906119", size = 256941, upload-time = "2026-05-05T16:30:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f1d03401571b331653acddbd4e8cd955c06d945241dd08b25192fac0d04b/librt-0.10.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc15acabdd519bd4176fdadc2119e5e3093485d86f89138daf47e5b4cedb983a", size = 285855, upload-time = "2026-05-05T16:30:48.86Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/62cf80ff046c339faf56718b3a940244d4beb70f1c6407289b5830ec11e9/librt-0.10.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b1b2d835307d08ddadd94568e2369648ec9173bd3eea6d7f52a1abe717c81f98", size = 275321, upload-time = "2026-05-05T16:30:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ea/da5918d4070362e9a4d2ee9cd34f9dc84902daad8fd4275f8504a727ff4e/librt-0.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d261c6a2f93335a5167887fb0223e8b98ffce20ee3fde242e8e58a37ece6d0e5", size = 293993, upload-time = "2026-05-05T16:30:52.577Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8d/68b6086bed1fcdc314c640ea04e31e52d18052e08059fa595409d66a51a9/librt-0.10.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e2ffd44963f8e7f68995504d90f9881d64e94dc1d8e310039b9526108fc0c0f7", size = 284254, upload-time = "2026-05-05T16:30:55.086Z" }, + { url = "https://files.pythonhosted.org/packages/06/c8/b810f1d84ec34a5a7ed93d7b510ab04164d75fbdf23088d5c3fbe6b08357/librt-0.10.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f285f6455ed495791c4d8630e5af732960adea93cac4c893d15619f2eae53e8", size = 284925, upload-time = "2026-05-05T16:30:56.728Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/3c82d4158c5a2c62528b8fccce65a8c9ad700e480e86f9389387435089a5/librt-0.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6034ff52e663d34c7b82ef2aa2f94ad7c1d939e2368e63b06844bc4d127d2e1", size = 307830, upload-time = "2026-05-05T16:30:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/99/3a/9c635ac3e8a00383ff689161d3eac8a30b3b2ddc711b40471e6b8983ea29/librt-0.10.0-cp314-cp314t-win32.whl", hash = "sha256:657860fd877fba6a241ea088ef99f63ca819945d3c715265da670bad56c37ebe", size = 60147, upload-time = "2026-05-05T16:31:00.293Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e8/6f65f3e565d4ac212cddddd552eacc8035ffdf941ca0ad6fe945a211d41f/librt-0.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56ded2d66010203a0cb5af063b609e3f079531a0e5e576d618dece859fd2e1af", size = 68649, upload-time = "2026-05-05T16:31:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/51/78/a0705a67cacd81e5fa01a5035b3adbdfbb43a7b8d4bd27e2b282ae61baf2/librt-0.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1ee63f30abf18ed4830fdbaf87b2b6f4bba1e198d46085c314edde4045e56715", size = 58247, upload-time = "2026-05-05T16:31:03.191Z" }, + { url = "https://files.pythonhosted.org/packages/5e/86/ba668426245b9531ae3f922f98e7f721149fb71aa81d5f0c5aea7ca49645/librt-0.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83628c28545a5f4d860b48fae7f62367c006ab7405898573f34af8b7dcb178a2", size = 77056, upload-time = "2026-05-05T16:31:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/ab/28/818421ff2432527fdd39fcce8a5909a916d03d3f32b4eb5f3285e3faa998/librt-0.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bcf57b4de07e2d4bd093636ee59dc1b64298f304148dd9c4f001f7c7897650d", size = 80372, upload-time = "2026-05-05T16:31:06.404Z" }, + { url = "https://files.pythonhosted.org/packages/19/b9/a9d6bb3e7524d285c729d6f6fa840841cadeddcf7de11073608821fd9fdb/librt-0.10.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2236c16bdb7c527eb671e4b599eec2c4229fddf80573de2bde529924f46db971", size = 243806, upload-time = "2026-05-05T16:31:07.805Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1f/9ecf1003461f9004f16073975981dd1496c496823a56f3a476aff86d8825/librt-0.10.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:c1efa2f494811b245427095225a4d0251aee33ba4cf6ba2b7a6a9a619bc1a2ff", size = 232029, upload-time = "2026-05-05T16:31:09.338Z" }, + { url = "https://files.pythonhosted.org/packages/87/48/ed64ab6e460b853e4263c3dbcce0e016bad1ffd569f54774323604cb822e/librt-0.10.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d14626d350af79eed4b4f8886530052e3f78a62e9e53d2699f726f99c3d1d122", size = 257203, upload-time = "2026-05-05T16:31:10.9Z" }, + { url = "https://files.pythonhosted.org/packages/41/10/f632735eadc006120416a873e4a230dbfca86e7e9ed56b5d546203b0b89b/librt-0.10.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b609f3461beae5608ca5219131ae5cdfea2e369818030abfc6ba7086830cde42", size = 250735, upload-time = "2026-05-05T16:31:12.494Z" }, + { url = "https://files.pythonhosted.org/packages/92/cf/d9f64349396e777c1861dbe35bffed2e9b7fff88064ea5ba7e73dd6f14e3/librt-0.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0e2338b67c8e72755ccd1ab77b027e3701b375a1e12b4576fdefdf9c46448274", size = 264026, upload-time = "2026-05-05T16:31:14.25Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2d/56f578d74464f3912690c3532666a9b338c853c62a36fb7acdc4b0479dad/librt-0.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:17cadff57139ff49beea0b17e50b28dfc3f9687126399696de4d2d8ae86ba7ff", size = 259755, upload-time = "2026-05-05T16:31:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/b0/10/4c544e7622034a2692aa9c492ea9a705bae5fc35435edd001336a2c09460/librt-0.10.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:5496102c8ed065c128d0f0fd10dcb3f9f3fd9b346954462d62af623f1b1ec7cd", size = 257624, upload-time = "2026-05-05T16:31:17.73Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a9/7972d0146152adf1330b027746c6e4be9030e7a5a38b78addcfdb628bdca/librt-0.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:537e1bfa459c1c92a263768a8a0c6fd0558049fa6c1b866d791eea711ae64114", size = 280487, upload-time = "2026-05-05T16:31:19.591Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4d/926856839d875659dfc23adb6ff295fdfca53a5b5f915319f23cb5611b73/librt-0.10.0-cp39-cp39-win32.whl", hash = "sha256:85aca5a7ddc5f2d4cba24eba35667d83893ff2980dbd5884be16f538a24351e4", size = 62623, upload-time = "2026-05-05T16:31:20.924Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f3/c017fe4337e263bac6a38d2768d687c06e82886d6c131c99179063006323/librt-0.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:e45e46ff5fdfc690e77bb8557d5ba56974c4006b744ddbd70cce99fec6bfbeb8", size = 70725, upload-time = "2026-05-05T16:31:22.182Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +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" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "more-itertools" +version = "11.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nh3" +version = "0.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/5f/1d19bdc7d27238e37f3672cdc02cb77c56a4a86d140cd4f4f23c90df6e16/nh3-0.3.5.tar.gz", hash = "sha256:45855e14ff056064fec77133bfcf7cd691838168e5e17bbef075394954dc9dc8", size = 20743, upload-time = "2026-04-25T10:44:16.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/b0/8587ac42a9627ab88e7e221601f1dfccbf4db80b2a29222ea63266dc9abc/nh3-0.3.5-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:23a312224875f72cd16bde417f49071451877e29ef646a60e50fcb69407cc18a", size = 1420126, upload-time = "2026-04-25T10:43:39.834Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/1dbc4d0c43f12e8c1784ede17eaee6f061d4fbe5505757c65c49b2ceab95/nh3-0.3.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387abd011e81959d5a35151a11350a0795c6edeb53ebfa02d2e882dc01299263", size = 793943, upload-time = "2026-04-25T10:43:41.363Z" }, + { url = "https://files.pythonhosted.org/packages/47/9f/d6758d7a14ee964bf439cc35ae4fa24a763a93399c8ef6f22bd11d532d29/nh3-0.3.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48f45e3e914be93a596431aa143dedf1582557bf41a58153c296048d6e3798c9", size = 841150, upload-time = "2026-04-25T10:43:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/d5d1ae8374612c98f390e1ea7c610fa6c9716259a03bbf4d15b269f40073/nh3-0.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0a09f51806fd51b4fedbf9ea2b61fef388f19aef0d62fe51199d41648be14588", size = 1008415, upload-time = "2026-04-25T10:43:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/d13a9c3fd2d9c131a2a281737380e9379eb0f8c33fea24c2b923aaafbb15/nh3-0.3.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c357f1d042c67f135a5e6babb2b0e3b9d9224ff4a3543240f597767b01384ffd", size = 1092706, upload-time = "2026-04-25T10:43:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/bb/57/2f3add7f8680fcc896afa6a675cb2bab09982853ee8af40bad621f6b61c4/nh3-0.3.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:38748140bf76383ab7ce2dce0ad4cb663855d8fbc9098f7f3483673d09616a17", size = 1048346, upload-time = "2026-04-25T10:43:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c3/2f9e4ffa82863074d1361bfe949bc46393d91b3411579dfbbd090b24cac5/nh3-0.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:84bdeb082544fbcb77a12c034dd77d7da0556fdc0727b787eb6214b958c15e29", size = 1029038, upload-time = "2026-04-25T10:43:48.569Z" }, + { url = "https://files.pythonhosted.org/packages/e8/10/2804deb3f3315184c9cae41702e293c87524b5a21f766b07d7fe3ffbcfbb/nh3-0.3.5-cp314-cp314t-win32.whl", hash = "sha256:c3aae321f67ae66cff2a627115f106a377d4475d10b0e13d97959a13486b9a88", size = 603263, upload-time = "2026-04-25T10:43:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/f6685248b49f7548fc9a8c335ab3a52f68610b72e8a61576447151e4e2e6/nh3-0.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c88605d8d468f7fc1b31e06129bc91d6c96f6c621776c9b504a0da9beac9df5f", size = 616866, upload-time = "2026-04-25T10:43:51.005Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/d8c9018635d4acfefde6b68470daa510eed715a350cbaa2f928ba0609f81/nh3-0.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:72c5bdedec27fa33de6a5326346ea8aa3fe54f6ac294d54c4b204fb66a9f1e79", size = 602566, upload-time = "2026-04-25T10:43:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/85/30/d162e99746a2fb1d98bb0ef23af3e201b156cf09f7de867c7390c8fe1c06/nh3-0.3.5-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3bb854485c9b33e5bb143ff3e49e577073bc6bc320f0ff8fc316dd89c0d3c101", size = 1442393, upload-time = "2026-04-25T10:43:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/25/8c/072120d506978ab053e1732d0efa7c86cb478fee0ee098fda0ac0d31cb34/nh3-0.3.5-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50d401ab2d8e86d59e2126e3ab2a2f45840c405842b626d9a51624b3a33b6878", size = 837722, upload-time = "2026-04-25T10:43:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/d4e06e28c5ad1c4b065f89737d02631bd49f1660b6ebcf17a87ffcd201da/nh3-0.3.5-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acfd354e61accbe4c74f8017c6e397a776916dfe47c48643cf7fd84ade826f93", size = 822872, upload-time = "2026-04-25T10:43:56.581Z" }, + { url = "https://files.pythonhosted.org/packages/0a/62/50659255213f241ec5797ae7427464c969397373e83b3659372b341ae869/nh3-0.3.5-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52d877980d7ca01dc3baf3936bf844828bc6f332962227a684ed79c18cce14c3", size = 1100031, upload-time = "2026-04-25T10:43:58.098Z" }, + { url = "https://files.pythonhosted.org/packages/00/7a/a12ae77593b2fcf3be25df7bc1c01967d0de448bdb4b6c7ec80fe4f5a74f/nh3-0.3.5-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:207c01801d3e9bb8ec08f08689346bdd30ce15b8bf60013a925d08b5388962a4", size = 1057669, upload-time = "2026-04-25T10:43:59.328Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/5647dc04c0233192a3956fc91708822b21403a06508cacf78083c68e7bf0/nh3-0.3.5-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea232933394d1d58bf7c4bb348dc4660eae6604e1ae81cd2ba6d9ed80d390f3b", size = 914795, upload-time = "2026-04-25T10:44:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0e/bf298920729f216adcb002acf7ea01b90842603d2e4e2ce9b900d9ee8fab/nh3-0.3.5-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe3a787dc76b50de6bee54ef242f26c41dfe47654428e3e94f0fae5bb6dd2cc1", size = 806976, upload-time = "2026-04-25T10:44:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/85/01/26761e1dc2b848e65a62c19e5d39ad446283287cd4afddc89f364ab86bc9/nh3-0.3.5-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:488928988caad25ba14b1eb5bc74e25e21f3b5e40341d956f3ce4a8bc19460dc", size = 834904, upload-time = "2026-04-25T10:44:03.454Z" }, + { url = "https://files.pythonhosted.org/packages/33/53/0766113e679540ac1edc1b82b1295aecd321eeb75d6fead70109a838b6ee/nh3-0.3.5-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c069570b06aa848457713ad7af4a9905691291548c4466a9ad78ee95808382b", size = 857159, upload-time = "2026-04-25T10:44:05.003Z" }, + { url = "https://files.pythonhosted.org/packages/58/36/734d353dfaf292fed574b8b3092f0ef79dc6404f3879f7faaa61a4701fad/nh3-0.3.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eeedc90ed8c42c327e8e10e621ccfa314fc6cce35d5929f4297ff1cdb89667c4", size = 1018600, upload-time = "2026-04-25T10:44:06.18Z" }, + { url = "https://files.pythonhosted.org/packages/6b/aa/d9c59c1b49669fcb7bababa55df82385f029ad5c2651f583c3a1141cfdd1/nh3-0.3.5-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:de8e8621853b6470fe928c684ee0d3f39ea8086cebafe4c416486488dea7b68d", size = 1103530, upload-time = "2026-04-25T10:44:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/cdd210bfb8d9d43fb02fc3c868336b9955934d8e15e66eb1d15a147b8af0/nh3-0.3.5-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:6ea58cc44d274c643b83547ca9654a0b1a817609b160601356f76a2b744c49ad", size = 1061754, upload-time = "2026-04-25T10:44:09.362Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cb/7a39e72e668c8445bdd95e494b3e21cfdddc68329be8ea3522c8befb46c4/nh3-0.3.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e49c9b564e6bcb03ecd2f057213df9a0de15a95812ac9db9600b590db23d3ae9", size = 1040938, upload-time = "2026-04-25T10:44:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/4c/fc2f9ed208a3801a319f59b5fea03cdc20cf3bd8af14be930d3a8de01224/nh3-0.3.5-cp38-abi3-win32.whl", hash = "sha256:559e4c73b689e9a7aa97ac9760b1bc488038d7c1a575aa4ab5a0e19ee9630c0f", size = 611445, upload-time = "2026-04-25T10:44:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/db/1a/e4c9b5e2ae13e6092c9ec16d8ca30646cb01fcdea245f36c5b08fd21fbd5/nh3-0.3.5-cp38-abi3-win_amd64.whl", hash = "sha256:45e6a65dc88a300a2e3502cb9c8e6d1d6b831d6fba7470643333609c6aab1f30", size = 626502, upload-time = "2026-04-25T10:44:13.682Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/19cd0671d1ba2762fb388fc149697d20d0568ccfeef833b11280a619e526/nh3-0.3.5-cp38-abi3-win_arm64.whl", hash = "sha256:8f85285700a18e9f3fc5bff41fe573fa84f81542ef13b48a89f9fecca0474d3b", size = 611069, upload-time = "2026-04-25T10:44:14.934Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +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.14.0" +source = { registry = "https://pypi.org/simple" } +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 = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +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/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 = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.13.5", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pluggy" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/51/2a/f125667ce48105bf1f4e50e03cfa7b24b8c4f47684d7f1cf4dcb6f6b1c15/pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3", size = 161464, upload-time = "2026-01-30T01:03:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/40/df/065a30790a7ca6bb48ad9018dd44668ed9135610ebf56a2a4cb8e513fd5c/pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1", size = 246159, upload-time = "2026-01-30T01:03:40.131Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1c/fd09976a7e04960dabc07ab0e0072c7813d566ec67d5490a4c600683c158/pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db", size = 259120, upload-time = "2026-01-30T01:03:41.233Z" }, + { url = "https://files.pythonhosted.org/packages/52/49/59fdc6fc5a390ae9f308eadeb97dfc70fc2d804ffc49dd39fc97604622ec/pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1", size = 262196, upload-time = "2026-01-30T01:03:42.696Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/d6734dccf0080e3dc00a55b0827ab5af30c886f8bc127bbc04bc3445daec/pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a", size = 103510, upload-time = "2026-01-30T01:03:43.915Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +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" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +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/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 = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.10'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.33.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +dependencies = [ + { name = "cryptography", version = "47.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version <= '3.9'" }, + { name = "cryptography", version = "48.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version > '3.9' and python_full_version < '3.10'" }, + { name = "jeepney", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "cryptography", version = "48.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jeepney", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "twine" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.33.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +dependencies = [ + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20260508" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/6b/eb226bdd61a982c9a03e02c657fb4ab001733506e6423906ac142331f2e3/types_requests-2.33.0.20260508.tar.gz", hash = "sha256:81b2ae5f0d20967714a6aa5ef9284c05570d7cb06b7de8f2a77b918b63ddd411", size = 23991, upload-time = "2026-05-08T04:50:56.818Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/96/080db0afdf2c5cc5fe512b41354e8d114fe8f65e9510c56ff8dfd40216ce/types_requests-2.33.0.20260508-py3-none-any.whl", hash = "sha256:fa01459cca184229713df03709db46a905325906d27e042cd4fd7ea3d15d3400", size = 20722, upload-time = "2026-05-08T04:50:55.548Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +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/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.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +]