From 52abf84728873e35b13be4f74ecf5d518f094f5c Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Wed, 18 Jun 2025 13:00:35 -0700 Subject: [PATCH 01/14] chore(pyproject): update and add modern repository tooling --- .cursor/rules/github-actions.mdc | 48 ++ .cursor/rules/markdown-documentation.mdc | 40 ++ .cursor/rules/python-development.mdc | 65 +++ .cursor/rules/python-testing.mdc | 38 ++ .cursor/rules/version-control.mdc | 41 ++ .cursor/rules/version-management.mdc | 44 ++ .github/CODEOWNERS | 4 + .github/FUNDING.yml | 1 + .github/actionlint.yaml | 2 + .github/workflows/ci.yaml | 127 +++++ .github/workflows/release.yaml | 84 +++ .gitignore | 26 +- .pre-commit-config.yaml | 40 ++ .python-version | 1 + .releaserc.json | 42 ++ README.md | 465 ++++++++++++++++ pyconfig/__init__.py | 290 +++++----- pyconfig/scripts.py | 188 ++++--- pyproject.toml | 36 ++ setup.py | 61 --- uv.lock | 649 +++++++++++++++++++++++ 21 files changed, 2007 insertions(+), 285 deletions(-) create mode 100644 .cursor/rules/github-actions.mdc create mode 100644 .cursor/rules/markdown-documentation.mdc create mode 100644 .cursor/rules/python-development.mdc create mode 100644 .cursor/rules/python-testing.mdc create mode 100644 .cursor/rules/version-control.mdc create mode 100644 .cursor/rules/version-management.mdc create mode 100644 .github/CODEOWNERS create mode 100644 .github/FUNDING.yml create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 .python-version create mode 100644 .releaserc.json create mode 100644 README.md create mode 100644 pyproject.toml delete mode 100644 setup.py create mode 100644 uv.lock diff --git a/.cursor/rules/github-actions.mdc b/.cursor/rules/github-actions.mdc new file mode 100644 index 0000000..cfee0dc --- /dev/null +++ b/.cursor/rules/github-actions.mdc @@ -0,0 +1,48 @@ +--- +description: GitHub Actions security and best practices +globs: [".github/workflows/*.yml", ".github/workflows/*.yaml", ".github/workflows/**/*.yml", ".github/workflows/**/*.yaml"] +alwaysApply: false +--- + +# GitHub Actions Guidelines + +## Security Best Practices + +- **Always use commit hashes for GitHub Actions** instead of version tags + - This prevents supply chain attacks by ensuring we're using exact versions + - Include the version tag as a comment for reference + - Example: `uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4` + +## Version Management + +- Keep actions up to date with their latest stable versions +- Use caching where appropriate to speed up workflows +- Follow security best practices for GitHub Actions + +## Version Checking Commands + +Use these commands to find latest versions: + +```bash +# For GitHub-hosted projects +gh release list --repo / --limit 1 + +# For GitHub Actions specifically +gh api repos///releases/latest + +# For GitHub Actions versions +gh api repos///tags +``` + +## Best Practices + +- Never guess or use potentially outdated versions +- Always verify versions from official repositories +- Keep GitHub Actions and their versions up to date +- Use commit hashes for GitHub Actions instead of version tags +- Include version tags as comments for reference + +Example format: +```yaml +uses: owner/repo@commit_hash # v1.2.3 +``` diff --git a/.cursor/rules/markdown-documentation.mdc b/.cursor/rules/markdown-documentation.mdc new file mode 100644 index 0000000..87183d1 --- /dev/null +++ b/.cursor/rules/markdown-documentation.mdc @@ -0,0 +1,40 @@ +--- +description: Documentation and markdown formatting guidelines +globs: *.md,**/*.md,README* +alwaysApply: false +--- + +# Documentation Guidelines + +@Docs https://github.github.com/gfm/ + +## Markdown Formatting + +- Keep all documentation files properly formatted +- Run markdown linting before committing documentation changes + - ALWAYS use `pre-commit` for linting and formatting + - ALWAYS use `pre-commit run [--all-files | --files [FILES ...]] prettier` to ensure markdown files conform to prettier's standards before running markdown lint + - ALWAYS use `pre-commit run [--all-files | --files [FILES ...]] markdownlint-cli2` hook for formatting a specific markdown file or all markdown files in the repository + - IF AND ONLY IF `pre-commit` is not available as a command, `uv run pre-commit` should be used as a fallback +- Use proper markdown formatting: + - Add blank lines before and after code blocks + - Use proper heading hierarchy + - Keep line length reasonable (88 characters) + - Use proper list formatting with blank lines between list items + +## Documentation Standards + +- Keep README.md updated with new features +- Document any breaking changes +- Add docstrings to all public APIs +- Follow the project's markdown style guide +- Use pre-commit hooks to enforce markdown formatting +- Check markdown files with `markdownlint-cli2` before committing + +## Content Guidelines + +- Write clear, concise documentation +- Include code examples where helpful +- Keep documentation up to date with code changes +- Use consistent terminology throughout +- Provide installation and usage instructions diff --git a/.cursor/rules/python-development.mdc b/.cursor/rules/python-development.mdc new file mode 100644 index 0000000..30495ff --- /dev/null +++ b/.cursor/rules/python-development.mdc @@ -0,0 +1,65 @@ +--- +description: Core Python development guidelines for pyconfig project +globs: *.py,**/*.py +alwaysApply: false +--- + +# Python Development Guidelines + +## Running Python commands + +- All commands are managed with `uv` and should use `uv run` + - NEVER try to run commands with `python -m` directly, always use `uv run` or `uv run python -m` + - ALWAYS prefer invoking commands directly like `uv run pytest` rather than the module like `uv run python -m pytest` + +## Code Style & Formatting + +- Python projects are formatted using `ruff` + - Ruff formatter documentation + - Files can be auto formatted using `uv run ruff format [OPTIONS] [FILES]...` +- ALWAYS use type hints for all function parameters and return values +- ALWAYS create an appropriate docstring for functions, classes, and modules following Google style + - ALWAYS describe possible edge cases, errors, and conditionals that materially affect the behavior of the documented code +- Follow PEP 8 standards +- Prefer descriptive variable and function names +- Keep line length to 88 characters (Black formatter default) + +## Project Structure + +- This project uses `uv` for dependency management +- Tests should be placed in the `tests/` directory with matching structure to the project codebase +- Use `pytest` for all testing + - ALWAYS use the format `uv run pytest [options] [file_or_dir] [file_or_dir] [...]` when running tests + - NEVER try to run `python -m pytest` + - Pytest documentation +- Follow the existing project structure when adding new modules + +## Dependencies & Imports + +- Use uv to manage dependencies via `pyproject.toml` + - Use appropriate `--group` arguments to `uv add` when adding dependencies so we don't add main package dependencies unnecessarily + - The default groups should be "dev" and "docs", with other specific groups created only by the user +- Group imports: standard library, third-party, local imports +- Use absolute imports when possible +- Avoid circular imports + +## Error Handling + +- Use specific exception types rather than bare `except:` +- Provide meaningful error messages +- Handle edge cases gracefully +- Use logging instead of print statements for debugging + +## Performance & Best Practices + +- Prefer list comprehensions over loops where readable +- Use context managers for resource management +- Avoid premature optimization but be mindful of performance + +## AI Assistant Instructions + +- Always suggest type hints when they're missing +- Recommend tests when implementing new functionality +- Point out potential security issues or edge cases +- Suggest more Pythonic approaches when applicable +- Consider backwards compatibility when making changes diff --git a/.cursor/rules/python-testing.mdc b/.cursor/rules/python-testing.mdc new file mode 100644 index 0000000..16f2629 --- /dev/null +++ b/.cursor/rules/python-testing.mdc @@ -0,0 +1,38 @@ +--- +description: Testing guidelines and best practices +globs: test/**/*, tests/**/*, **/test_*.py, **/*_test.py, test*/**/*.py, test*/*.py +alwaysApply: false +--- + +# Testing Guidelines + +## What to test + +- Start with creating exercise tests, testing intended behavior of functions, classes, and modules +- Try to minimally mock so we ensure that we aren't creating passthrough tests of no value +- Once we have good exercise tests, then continue to create more tests for common edge cases + +## Test Structure & Organization + +- Write comprehensive tests for all new functionality +- Use pytest fixtures appropriately +- Aim for high test coverage +- Test files should be named `test_*.py` +- Use descriptive test function names that explain what is being tested +- Tests should be placed in the `tests/` directory with matching structure to `pyconfig/` + +## Test Best Practices + +- Use pytest for all testing +- Group related tests into classes when appropriate +- Use parametrized tests for testing multiple scenarios +- Mock external dependencies appropriately +- Test both success and failure cases +- Include edge case testing + +## Fixtures & Setup + +- Use pytest fixtures for common test setup +- Prefer session, module, or class scoped fixtures when appropriate +- Keep fixtures focused and reusable +- Use temporary directories for file-based tests diff --git a/.cursor/rules/version-control.mdc b/.cursor/rules/version-control.mdc new file mode 100644 index 0000000..0b38aaa --- /dev/null +++ b/.cursor/rules/version-control.mdc @@ -0,0 +1,41 @@ +--- +description: Version control and commit message guidelines +globs: +alwaysApply: false +--- + +# Version Control Guidelines + +## Commit Message Standards + +- This project uses pre-commit hooks - ensure they pass before committing +- **Always use Conventional Commits for all commit messages** following the specification at +- Use the format: `(scope): ` - **scope is required** +- The scope should reflect the portion of the package that was modified, for example if your package is `pytool`: + - If modifying the package root, `pytool/__init__.py` then the scope is `pytool`, or the package name. + - If modifying `pytool/lang.py`, use scope `lang` + - If modifying `pytool/utils/helpers.py`, use scope `utils` + - If modifying multiple modules, use the most relevant or common scope + +## Commit Types + +- Common types: `feat:`, `fix:`, `docs:`, `style:`, `refactor:`, `test:`, `chore:` +- Use `!` or `BREAKING CHANGE:` footer for breaking changes +- Keep commits focused and atomic + +## Commit Organization + +- **Group changes into small logical commits** - instead of making one large commit with multiple bullet points in the message, break changes into separate commits where each commit represents a single logical change or feature addition/fix + +## Examples + +- `feat(cli): add new CLI command for file processing` +- `fix(parser): handle empty input files correctly` +- `docs(readme): update README with installation instructions` + +## Best Practices + +- Keep commits focused and atomic +- Write clear, descriptive commit messages +- Reference issues when applicable +- Use present tense in commit messages diff --git a/.cursor/rules/version-management.mdc b/.cursor/rules/version-management.mdc new file mode 100644 index 0000000..65e90d8 --- /dev/null +++ b/.cursor/rules/version-management.mdc @@ -0,0 +1,44 @@ +--- +description: Version checking and dependency management best practices +globs: +alwaysApply: false +--- + +# Version Management Best Practices + +## General Principles + +- Always check for the latest versions of dependencies and GitHub Actions +- Never guess or use potentially outdated versions +- Always verify versions from official repositories +- Keep GitHub Actions and their versions up to date + +## Version Checking Commands + +Use the following commands to find latest versions: + +```bash +# For GitHub-hosted projects +gh release list --repo / --limit 1 + +# For GitHub Actions specifically +gh api repos///releases/latest + +# For GitHub Actions versions +gh api repos///tags +``` + +## Dependency Management + +- Use uv to manage Python dependencies +- Keep `pyproject.toml` up to date with version constraints +- Regularly update dependencies to latest compatible versions +- Use dependency scanning tools when available +- Document version requirements clearly + +## Security Considerations + +- Use commit hashes for GitHub Actions instead of version tags +- Include version tags as comments for reference +- Regularly audit dependencies for security vulnerabilities +- Keep all tooling and dependencies updated diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..7db588e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +* @shakefu + +# Ignore certain files so they can be auto-updated +.pre-commit-config.yaml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..90e85f6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: shakefu diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..9261d80 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,2 @@ +self-hosted-runner: + labels: [] diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..17a8e57 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,127 @@ +--- +name: CI + +on: + # Allow this config to be reused by other workflows in the repo + workflow_call: + pull_request: + branches: [main] + +concurrency: + group: ci-${{ github.event.number || 'main' }} + cancel-in-progress: true + +jobs: + preview: + name: Release preview + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + with: + fetch-depth: 0 + - name: Generate notes + id: notes + uses: open-turo/actions-release/semantic-release@4d8a6b6aa2d051e7dba0429d8d08beef827b5ccd # v4 + with: + branches: ${{ github.head_ref }} + override-github-ref-name: ${{ github.head_ref }} + dry-run: true + ci: false + - name: Find Comment + uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2 + id: find + with: + issue-number: ${{ github.event.pull_request.number }} + body-includes: release-notes-preview + - name: Comment preview + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2 + if: steps.notes.outputs.new-release-notes != '' + with: + comment-id: ${{ steps.find.outputs.comment-id }} + issue-number: ${{ github.event.number }} + edit-mode: replace + body: | + + + ## Release notes preview + + ${{ steps.notes.outputs.new-release-notes }} + + lint: + name: Lint + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v5 + with: + python-version: ${{ matrix.python-version }} + - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 + + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v5 + with: + python-version: ${{ matrix.python-version }} + - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + with: + version: "0.7.12" + python-version: ${{ matrix.python-version }} + enable-cache: true + cache-dependency-glob: "pyproject.toml" + - env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Pytest + uv sync --locked --all-extras --dev + uv run pytest --cov + uv run coveralls + + docs: + name: Docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v5 + with: + python-version-file: ".python-version" + - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + with: + version: "0.7.12" + enable-cache: true + cache-dependency-glob: "pyproject.toml" + - env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Build docs + uv run script/docs + + checks: + name: Checks + needs: [lint, test, docs] + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Renovate / Auto-approve + if: github.actor == 'renovatebot' || github.actor == 'renovate[bot]' + uses: hmarr/auto-approve-action@44888193675f29a83e04faf4002fa8c0b537b1e4 # v3.2.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Renovate / Auto-merge + if: github.actor == 'renovatebot' || github.actor == 'renovate[bot]' + uses: pascalgn/automerge-action@22948e0bc22f0aa673800da838595a3e7347e584 # v0.15.6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MERGE_LABELS: dependencies + MERGE_METHOD: rebase diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..e7e3f07 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,84 @@ +--- +name: Release + +on: + push: + branches: [main] + +concurrency: + group: release + +jobs: + ci: + name: CI + uses: ./.github/workflows/ci.yaml + + release: + name: Release + needs: ci + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.new-release-version }} + published: ${{ steps.version.outputs.new-release-published }} + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + with: + fetch-depth: 0 + - id: version + uses: open-turo/actions-release/semantic-release@4d8a6b6aa2d051e7dba0429d8d08beef827b5ccd # v4 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + dry-run: true + ci: false + + publish: + name: Publish + needs: release + runs-on: ubuntu-latest + if: needs.release.outputs.published == 'true' + permissions: + contents: write + issues: write + pull-requests: write + id-token: write + steps: + - id: authenticate + uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2 + with: + app-id: ${{ secrets.BOT_CLIENT_ID }} + private-key: ${{ secrets.BOT_PRIVATE_KEY }} + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + with: + token: ${{ steps.authenticate.outputs.token }} + fetch-depth: 0 + - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v5 + with: + python-version-file: ".python-version" + - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 + with: + version: "0.7.12" + enable-cache: true + cache-dependency-glob: "pyproject.toml" + - run: | + # Install dependencies + uv sync --locked --all-extras --dev + - id: version + uses: open-turo/actions-release/semantic-release@4d8a6b6aa2d051e7dba0429d8d08beef827b5ccd # v4 + with: + github-token: ${{ steps.authenticate.outputs.token }} + dry-run: true + ci: false + - run: | + # Update version + uv version ${{ steps.version.outputs.new-release-version }} + - uses: actions-js/push@master + with: + message: "chore: ${{ steps.version.outputs.new-release-version }} [skip actions]" + github_token: ${{ steps.authenticate.outputs.token }} + - run: | + # Build package + uv build + - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1 + - uses: open-turo/actions-release/semantic-release@4d8a6b6aa2d051e7dba0429d8d08beef827b5ccd # v4 + with: + github-token: ${{ steps.authenticate.outputs.token }} diff --git a/.gitignore b/.gitignore index ec974c2..f413420 100644 --- a/.gitignore +++ b/.gitignore @@ -18,13 +18,31 @@ pip-log.txt # Unit test / coverage reports .coverage +.coverage.* +coverage.xml +htmlcov/ .tox +.nox +.cache +.pytest_cache/ +.hypothesis/ +*.cover +*.py,cover +.coverage_html/ +.pytest_cache -#Translations +# Translations *.mo -#Mr Developer +# Mr Developer .mr.developer.cfg -# Pyconfig -localconfig.py +# Vim +*.swp + +# Sphinx +docs/_build +docs/_output + +# Cursor +.cursorrules diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..45720df --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,40 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v9.22.0 + hooks: + - id: commitlint + stages: [commit-msg] + additional_dependencies: ["@open-turo/commitlint-config-conventional"] + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + stages: [pre-commit] + # - repo: https://github.com/rhysd/actionlint + # rev: v1.6.17 + # hooks: + # - id: actionlint + # - repo: https://github.com/jumanjihouse/pre-commit-hooks + # rev: 3.0.0 # or specific git tag + # hooks: + # - id: shellcheck + # - id: shfmt + - repo: https://github.com/DavidAnson/markdownlint-cli2 + rev: v0.18.1 + hooks: + - id: markdownlint-cli2 + args: ["--fix", ".cursorrules"] + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.11.13 + hooks: + # Run the Ruff linter. + - id: ruff + # Run the Ruff formatter. + - id: ruff-format diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..d457a7d --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,42 @@ +{ + "branches": ["main"], + "plugins": [ + "@semantic-release/commit-analyzer", + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "type": [ + { "type": "build", "section": "Build System", "hidden": false }, + { "type": "chore", "section": "Miscellaneous", "hidden": false }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": false + }, + { "type": "docs", "section": "Documentation", "hidden": false }, + { "type": "feat", "section": "Features", "hidden": false }, + { "type": "fix", "section": "Bug Fixes", "hidden": false }, + { + "type": "perf", + "section": "Performance Improvements", + "hidden": false + }, + { + "type": "refactor", + "section": "Code Refactoring", + "hidden": false + }, + { "type": "style", "section": "Styles", "hidden": false }, + { "type": "test", "section": "Tests", "hidden": false } + ] + }, + "writerOpts": { + "commitsSort": ["subject", "scope"] + } + } + ], + "@semantic-release/github" + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..af175e8 --- /dev/null +++ b/README.md @@ -0,0 +1,465 @@ +# pyconfig - Python-based singleton configuration + +[![Build Status](https://travis-ci.org/shakefu/pyconfig.svg?branch=master)](https://travis-ci.org/shakefu/pyconfig) + +This module provides python based configuration that is stored in a singleton +object to ensure consistency across your project. + +## Table of Contents + +- [Command Line](#command-line) +- [Etcd](#etcd) +- [Code Examples](#code-examples) +- [Changes](#changes) +- [Contributors](#contributors) + +## Command Line + +Pyconfig has a command line utility that lets you inspect your project to find +all the configuration keys defined. + +```bash +$ pyconfig -h +usage: pyconfig [-h] [-f F | -m M] [-v] [-l] [-a | -k] [-n] [-s] [-c] + +Helper for working with pyconfigs + +optional arguments: + -h, --help show this help message and exit + -f F, --filename F parse an individual file or directory + -m M, --module M parse a package or module, recursively looking inside it + -v, --view-call show the actual pyconfig call made (default: show namespace) + -l, --load-configs query the currently set value for each key found + -a, --all show keys which don't have defaults set + -k, --only-keys show a list of discovered keys without values + -n, --natural-sort sort by filename and line (default: alphabetical by key) + -s, --source show source annotations (implies --natural-sort) + -c, --color toggle output colors (default: True) +``` + +### Examples + +```bash +$ pyconfig --file . +humbledb.allow_explicit_request = True +humbledb.auto_start_request = True +humbledb.connection_pool = 300 +humbledb.tz_aware = True +humbledb.use_greenlets = False +humbledb.write_concern = 1 + +$ pyconfig --view-call --file . +pyconfig.get('humbledb.allow_explicit_request', True) +pyconfig.setting('humbledb.auto_start_request', True) +pyconfig.setting('humbledb.connection_pool', 300) +pyconfig.setting('humbledb.tz_aware', True) +pyconfig.setting('humbledb.use_greenlets', False) +pyconfig.setting('humbledb.write_concern', 1) + +$ pyconfig --source --file . +# ./humbledb/mongo.py, line 98 +humbledb.allow_explicit_request = True +# ./humbledb/mongo.py, line 178 +humbledb.connection_pool = 300 +# ./humbledb/mongo.py, line 181 +humbledb.auto_start_request = True +# ./humbledb/mongo.py, line 185 +humbledb.use_greenlets = False +# ./humbledb/mongo.py, line 188 +humbledb.tz_aware = True +# ./humbledb/mongo.py, line 191 +humbledb.write_concern = 1 +``` + +## Etcd + +### Version added: 3.0.0 + +Pyconfig has read-only support for configurations stored in etcd. The preferred +method for configuring Pyconfig to work with etcd is via ENV variables, since +they must be set as early as possible. It is also possible to use the Python +API to make Pyconfig work with etcd. + +Pyconfig uses a directory namespace to store its dot notation configuration key +names. By default, that namespace is `/config/`. + +At a minimum, `PYCONFIG_ETCD_HOSTS` must be set to get Pyconfig to try to +read a configuration from etcd using the default settings. + +You can set a value with `etcdctl` like: + +```bash +# The etcdctl command is provided by etcd and not part of pyconfig +etcdctl set /pyconfig/example/my.setting "from etcd" +``` + +And configure Pyconfig to connect and use that setting: + +```bash +$ export PYCONFIG_ETCD_PREFIX="/pyconfig/example/" +$ export PYCONFIG_ETCD_HOSTS="127.0.0.1:2379" +$ python +>>> import pyconfig +>>> pyconfig.get('my.setting') +'from etcd' +``` + +Because of Pyconfig's singleton nature, only one configuration can be accessed +at a time in this way. + +**Environment variables:** + +- `PYCONFIG_ETCD_PREFIX` - The namespace to prefix settings with (default: + `'/config/'`) +- `PYCONFIG_ETCD_HOSTS` - A comma separated list of hosts, like + `10.0.0.1:2379,10.0.0.2:2379` +- `PYCONFIG_ETCD_CACERT` - CA cert file to use for SSL +- `PYCONFIG_ETCD_CERT` - Client cert file to use for SSL client authentication +- `PYCONFIG_ETCD_KEY` - Client private key file to use for SSL client auth +- `PYCONFIG_ETCD_WATCH` - If this is set to a truthy value (a non-empty + string), then pyconfig will keep the local configuration synchronized with + etcd (_Version added: 3.1.0_) +- `PYCONFIG_ETCD_PROTOCOL` - Set this to force HTTPS connections even if not + using certificates. This should be a string of the form `https` or `http`. + (_Version added: 3.2.0_) +- `PYCONFIG_ETCD_AUTH` - Set this use Basic Authentication with requests. + This should be a string of the format `username:password`. (_Version added: + 3.2.0_) + +**Inheritance:** + +If you want to create a configuration that inherits from an existing +configuration, Pyconfig will look for a special key, which by default is set to +`config.inherit`. If this exists and is set to an etcd namespace, that +configuration will be used as the base for the current config. + +A typical use case would be a Test environment configuration which is derived +from a Development config. Below is a barebones example of how that might be +set up using `etcdctl` and Pyconfig. + +```bash +$ # Create the development settings +$ etcdctl set /config/app/dev/my.name example +$ etcdctl set /config/app/dev/my.hostname localhost +$ etcdctl set /config/app/dev/my.api.key abcdef0123456789 +$ # Create the test settings +$ etcdctl set /config/app/test/my.hostname test.example.com +$ # Tell it to inherit from the development settings +$ etcdctl set /config/app/test/config.inherit /config/app/dev/ +$ # Configure Pyconfig to use the test configuration +$ export PYCONFIG_ETCD_PREFIX="/config/app/test/" +$ export PYCONFIG_ETCD_HOSTS="127.0.0.1:2379" +$ python +>>> import pyconfig +>>> pyconfig.get('my.hostname') +'test.example.com' +>>> pyconfig.get('my.name') +'example' +``` + +## Code Examples + +The most basic usage allows you to get, retrieve and modify values. Pyconfig's +singleton provides convenient accessor methods for these actions: + +### Version changed: 3.0.0 + +As of version 3.0.0, keys are not case sensitive by default. + +```python +>>> import pyconfig +>>> pyconfig.get('my.setting', 'default') +'default' +>>> pyconfig.set('my.setting', 'new') +>>> pyconfig.get('my.setting', 'default') +'new' +>>> pyconfig.reload(clear=True) +>>> pyconfig.get('my.setting', 'default') +'default' +``` + +You can also opt-out of default values: + +```python +>>> import pyconfig +>>> pyconfig.get('my.setting', allow_default=False) +Traceback (most recent call last): + File "", line 1, in + File "pyconfig/__init__.py", line 275, in get + return Config().get(name, default, allow_default=allow_default) + File "pyconfig/__init__.py", line 234, in get + return self.settings[name] + LookupError: No setting "my.setting" +``` + +Pyconfig also provides shortcuts for giving classes property descriptors which +map to the current setting stored in the singleton: + +```python +>>> import pyconfig +>>> class MyClass(object): +... my_setting = pyconfig.setting('my.setting', 'default') +... +>>> MyClass.my_setting +'default' +>>> MyClass().my_setting +'default' +>>> pyconfig.set('my.setting', "Hello World!") +>>> MyClass.my_setting +'Hello World!' +>>> MyClass().my_setting +'Hello World!' +>>> pyconfig.reload(clear=True) +>>> MyClass.my_setting +'default' +``` + +The `Setting` class also supports preventing default values. When set this way, +all reads on the attribute will prevent the use of defaults: + +```python +>>> import pyconfig +>>> class MyClass(object): +... my_setting = pyconfig.setting('my.setting', allow_default=False) +... +>>> MyClass.my_setting +Traceback (most recent call last): + File "", line 1, in + File "pyconfig/__init__.py", line 84, in __get__ + allow_default=self.allow_default) + File "pyconfig/__init__.py", line 232, in get + raise LookupError('No setting "{}"'.format(name)) +LookupError: No setting "my.setting" +>>> pyconfig.set('my.setting', 'new_value') +>>> MyClass.my_setting +'value' +``` + +Pyconfig allows you to override settings via a python configuration file, that +defines its configuration keys as a module namespace. By default, Pyconfig will +look on your `PYTHONPATH` for a module named `localconfig`, and if it exists, it +will use this module namespace to update all configuration settings: + +```python +# __file__ = "$PYTHONPATH/localconfig.py" +from pyconfig import Namespace + +# Namespace objects allow you to use attribute assignment to create setting +# key names +my = Namespace() +my.setting = 'from_localconfig' +# Namespace objects implicitly return new nested Namespaces when accessing +# attributes that don't exist +my.nested.setting = 'also_from_localconfig' +``` + +With a `localconfig` on the `PYTHONPATH`, it will be loaded before any settings +are read: + +```python +>>> import pyconfig +>>> pyconfig.get('my.setting') +'from_localconfig' +>>> pyconfig.get('my.nested.setting') +'also_from_localconfig' +``` + +Pyconfig also allows you to create distutils plugins that are automatically +loaded. An example `setup.py`: + +```python +# __file__ = setup.py +from setuptools import setup + +setup( + name='pytest', + version='0.1.0-dev', + py_modules=['myconfig', 'anyconfig'], + entry_points={ + # The "my" in "my =" indicates a base namespace to use for + # the contained configuration. If you do not wish a base + # namespace, use "any" + 'pyconfig':[ + 'my = myconfig', + 'any = anyconfig', + ], + }, + ) +``` + +An example distutils plugin configuration file: + +```python +# __file__ = myconfig.py +from pyconfig import Namespace + +def some_callable(): + print "This callable was called." + print "You can execute any arbitrary code." + +setting = 'from_plugin' +nested = Namespace() +nested.setting = 'also_from_plugin' +``` + +Another example configuration file, without a base namespace: + +```python +# __file__ = anyconfig.py +from pyconfig import Namespace +other = Namespace() +other.setting = 'anyconfig_value' +``` + +Showing the plugin-specified settings: + +```python +>>> import pyconfig +>>> pyconfig.get('my.setting', 'default') +This callable was called. +You can execute any arbitrary code. +'from_plugin' +>>> pyconfig.get('my.nested.setting', 'default') +'also_from_plugin' +>>> pyconfig.get('other.setting', 'default') +'anyconfig_value' +``` + +More fancy stuff: + +```python +>>> # Reloading changes re-calls functions... +>>> pyconfig.reload() +This callable was called. +You can execute any arbitrary code. +>>> # This can be used to inject arbitrary code by changing a +>>> # localconfig.py or plugin and reloading a config... especially +>>> # when pyconfig.reload() is attached to a signal +>>> import signal +>>> signal.signal(signal.SIGUSR1, pyconfig.reload) +``` + +Pyconfig provides a `@reload_hook` decorator that allows you to register +functions or methods to be called when the configuration is reloaded: + +```python +>>> import pyconfig +>>> @pyconfig.reload_hook +... def reload(): +... print "Do something here." +... +>>> pyconfig.reload() +Do something here. +``` + +**Warning**: It should not be used to register large numbers of functions (e.g. +registering a bound method in a class's `__init__` method), since there is no +way to un-register a hook and it will cause a memory leak, since a bound method +maintains a strong reference to the bound instance. + +**Note**: Because the reload hooks are called without arguments, it will not +work with unbound methods or classmethods. + +## Changes + +This section contains descriptions of changes in each new version. + +### 3.2.0 + +- Adds `PYCONFIG_ETCD_PROTOCOL` and `PYCONFIG_ETCD_AUTH`. + +_Released August 17, 2017._ + +### 3.1.1 + +- Documentation fixes that makes rendering work on PyPI and GitHub again. + +_Released June 16, 2016._ + +### 3.1.0 + +- Adds the ability to watch etcd for changes to values. This can be enabled by + setting the environment variable `PYCONFIG_ETCD_WATCH=true`. + +_Released June 3, 2016._ + +### 3.0.2 + +- Fixes an issue when using Python 3 compatibility in Python 2.7 and PyOpenSSL. + +_Released September 28, 2015._ + +### 3.0.1 + +- Changes the default inherit depth to 2, which is more useful than 1. + +### 3.0.0 + +- Adds support for loading configurations from etcd, with inheritance. +- Use `pytool.lang.Namespace` instead of alternate implementation. +- Drops support for Python 2.6 and 3.2. +- Pyconfig setting keys are now case insensitive by default (Use + `pyconfig.set('pyconfig.case_sensitive', True)` to change the behavior) +- Adds new `clear()` method for wiping out the cached configuration. + +### Older Versions + +#### 2.2.1 + +- The command line tool will now attempt to handle source files which specify a + non-ascii encoding gracefully. + +#### 2.2.0 + +- Add `allow_default` keyword option to `get()` and `setting()`. Thanks + to [yarbelk](https://github.com/yarbelk)! + +#### 2.1.5 + +- Fix regression where `localconfig.py` wasn't being loaded on Python 2.7 due + to a logic flow error. Whoops! + +#### 2.1.4 + +- Broke Python 2.6 in 2.1.1, fixed again. + +#### 2.1.2-2.1.3 + +- Package clean up and fixing README to work on PyPI again. + +#### 2.1.1 + +- Fix bug that would break on Python 2.6 and 2.7 when using a localconfig.py. + +#### 2.1.0 + +- Pyconfig now works on Python 3, thanks to + [hfalcic](https://github.com/hfalcic)! + +#### 2.0.0 + +- Pyconfig now has the ability to show you what config keys are defined in a + directory. + +#### 1.2.0 + +- No longer uses Python 2.7 `format()`. Should work on 2.6 and maybe earlier. + +#### 1.1.2 + +- Move version string into `pyconfig.__version__` + +#### 1.1.1 + +- Fix bug with setup.py that prevented installation + +#### 1.1.0 + +- Allow for implicitly nesting Namespaces when accessing attributes that are + undefined + +## Contributors + +- [shakefu](http://github.com/shakefu) - Creator and maintainer +- [hfalcic](https://github.com/hfalcic) - Python 3 compatability +- [yarbelk](https://github.com/yarbelk) - `allow_default` option diff --git a/pyconfig/__init__.py b/pyconfig/__init__.py index 3651c7a..fb2e67d 100644 --- a/pyconfig/__init__.py +++ b/pyconfig/__init__.py @@ -3,15 +3,20 @@ ======== """ + from __future__ import print_function + +import logging import os -import sys import runpy -import logging +import sys import threading + try: + def iter_entry_points(group, **kwargs): from importlib.metadata import entry_points + for entry_point in entry_points(group=group, **kwargs): yield entry_point except ImportError: @@ -20,51 +25,50 @@ def iter_entry_points(group, **kwargs): except ImportError: raise ImportError("No module named 'importlib.metadata' or 'pkg_resources'") -import six import pytool from pytool.lang import Namespace - -__version__ = '3.2.3' +__version__ = "3.2.3" log = logging.getLogger(__name__) class Setting(object): - """ Setting descriptor. Allows class property style access of setting - values that are always up to date. + """Setting descriptor. Allows class property style access of setting + values that are always up to date. - If it is set with `allow_default` as `False`, calling the - attribute when its value is not set will raise a :exc:`LookupError` + If it is set with `allow_default` as `False`, calling the + attribute when its value is not set will raise a :exc:`LookupError` - :param str name: Setting key name - :param default: default value of setting. Defaults to None. - :param bool allow_default: If true, use the parameter default as - default if the key is not set, else raise - :exc:`LookupError` + :param str name: Setting key name + :param default: default value of setting. Defaults to None. + :param bool allow_default: If true, use the parameter default as + default if the key is not set, else raise + :exc:`LookupError` """ + def __init__(self, name, default=None, allow_default=True): self.name = name self.default = default self.allow_default = allow_default def __get__(self, instance, owner): - return Config().get(self.name, self.default, - allow_default=self.allow_default) + return Config().get(self.name, self.default, allow_default=self.allow_default) class Config(object): - """ Singleton configuration object that ensures consistent and up to date - setting values. + """Singleton configuration object that ensures consistent and up to date + setting values. """ + _self = dict( - _init=False, - settings={}, - reload_hooks=[], - mut_lock=threading.RLock(), - ) + _init=False, + settings={}, + reload_hooks=[], + mut_lock=threading.RLock(), + ) def __init__(self): # Use a borg singleton @@ -76,16 +80,16 @@ def __init__(self): self.load() def set(self, name, value): - """ Changes a setting value. + """Changes a setting value. - This implements a locking mechanism to ensure some level of thread - safety. + This implements a locking mechanism to ensure some level of thread + safety. - :param str name: Setting key name. - :param value: Setting value. + :param str name: Setting key name. + :param value: Setting value. """ - if not self.settings.get('pyconfig.case_sensitive', False): + if not self.settings.get("pyconfig.case_sensitive", False): name = name.lower() log.info(" %s = %s", name, repr(value)) @@ -94,15 +98,15 @@ def set(self, name, value): self.settings[name] = value def _update(self, conf_dict, base_name=None): - """ Updates the current configuration with the values in `conf_dict`. + """Updates the current configuration with the values in `conf_dict`. - :param dict conf_dict: Dictionary of key value settings. - :param str base_name: Base namespace for setting keys. + :param dict conf_dict: Dictionary of key value settings. + :param str base_name: Base namespace for setting keys. """ for name in conf_dict: # Skip private names - if name.startswith('_'): + if name.startswith("_"): continue value = conf_dict[name] # Skip Namespace if it's imported @@ -110,7 +114,7 @@ def _update(self, conf_dict, base_name=None): continue # Use a base namespace if base_name: - name = base_name + '.' + name + name = base_name + "." + name if isinstance(value, Namespace): for name, value in value.iteritems(name): self.set(name, value) @@ -139,20 +143,20 @@ def load(self, clear=False): defer = [] # Load all config plugins - for conf in iter_entry_points('pyconfig'): + for conf in iter_entry_points("pyconfig"): if conf.attrs: raise RuntimeError("config must be a module") mod_name = conf.module_name - base_name = conf.name if conf.name != 'any' else None + base_name = conf.name if conf.name != "any" else None log.info("Loading module '%s'", mod_name) mod_dict = runpy.run_module(mod_name) # If this module wants to be deferred, save it for later - if mod_dict.get('deferred', None) is deferred: + if mod_dict.get("deferred", None) is deferred: log.info("Deferring module '%s'", mod_name) - mod_dict.pop('deferred') + mod_dict.pop("deferred") defer.append((mod_name, base_name, mod_dict)) continue @@ -172,22 +176,23 @@ def load(self, clear=False): # Allow localconfig overrides mod_dict = None try: - mod_dict = runpy.run_module('localconfig') + mod_dict = runpy.run_module("localconfig") except ImportError: pass except ValueError as err: - if getattr(err, 'message') != '__package__ set to non-string': + if getattr(err, "message") != "__package__ set to non-string": raise # This is a bad work-around to make this work transparently... # shouldn't really access core stuff like this, but Fuck It[tm] - mod_name = 'localconfig' + mod_name = "localconfig" if sys.version_info < (2, 7): loader, code, fname = runpy._get_module_details(mod_name) else: _, loader, code, fname = runpy._get_module_details(mod_name) - mod_dict = runpy._run_code(code, {}, {}, mod_name, fname, loader, - pkg_name=None) + mod_dict = runpy._run_code( + code, {}, {}, mod_name, fname, loader, pkg_name=None + ) if mod_dict: log.info("Loading module 'localconfig'") @@ -196,24 +201,24 @@ def load(self, clear=False): self.call_reload_hooks() def call_reload_hooks(self): - """ Calls all the reload hooks that are registered. """ + """Calls all the reload hooks that are registered.""" # Call all registered reload hooks for hook in self.reload_hooks: hook() def get(self, name, default, allow_default=True): - """ Return a setting value. - - :param str name: Setting key name. - :param default: Default value of setting if it's not explicitly - set. - :param bool allow_default: If true, use the parameter default as - default if the key is not set, else raise - :exc:`LookupError` - :raises: :exc:`LookupError` if allow_default is false and the setting is - not set. + """Return a setting value. + + :param str name: Setting key name. + :param default: Default value of setting if it's not explicitly + set. + :param bool allow_default: If true, use the parameter default as + default if the key is not set, else raise + :exc:`LookupError` + :raises: :exc:`LookupError` if allow_default is false and the setting is + not set. """ - if not self.settings.get('pyconfig.case_sensitive', False): + if not self.settings.get("pyconfig.case_sensitive", False): name = name.lower() if name not in self.settings: if not allow_default: @@ -222,63 +227,64 @@ def get(self, name, default, allow_default=True): return self.settings[name] def reload(self, clear=False): - """ Reloads the configuration. """ + """Reloads the configuration.""" log.info("Reloading config.") self.load(clear) def add_reload_hook(self, hook): - """ Registers a reload hook that's called when :meth:`load` is called. + """Registers a reload hook that's called when :meth:`load` is called. - :param function hook: Hook to register. + :param function hook: Hook to register. """ self.reload_hooks.append(hook) def clear(self): - """ Clears all the cached configuration. """ + """Clears all the cached configuration.""" self.settings = {} + def reload(clear=False): - """ Shortcut method for calling reload. """ + """Shortcut method for calling reload.""" Config().reload(clear) def setting(name, default=None, allow_default=True): - """ Shortcut method for getting a setting descriptor. + """Shortcut method for getting a setting descriptor. - See :class:`pyconfig.Setting` for details. + See :class:`pyconfig.Setting` for details. """ return Setting(name, default, allow_default) def get(name, default=None, allow_default=True): - """ Shortcut method for getting a setting value. - - :param str name: Setting key name. - :param default: Default value of setting if it's not explicitly - set. Defaults to `None` - :param bool allow_default: If true, use the parameter default as - default if the key is not set, else raise - :exc:`KeyError`. Defaults to `None` - :raises: :exc:`KeyError` if allow_default is false and the setting is - not set. + """Shortcut method for getting a setting value. + + :param str name: Setting key name. + :param default: Default value of setting if it's not explicitly + set. Defaults to `None` + :param bool allow_default: If true, use the parameter default as + default if the key is not set, else raise + :exc:`KeyError`. Defaults to `None` + :raises: :exc:`KeyError` if allow_default is false and the setting is + not set. """ return Config().get(name, default, allow_default=allow_default) def set(name, value): - """ Shortcut method to change a setting. """ + """Shortcut method to change a setting.""" Config().set(name, value) def reload_hook(func): - """ Decorator for registering a reload hook. """ + """Decorator for registering a reload hook.""" Config().add_reload_hook(func) return func def clear(): - """ Shortcut for clearing all settings. """ + """Shortcut for clearing all settings.""" Config().clear() @@ -310,29 +316,31 @@ class etcd(object): Singleton for the etcd client and helper methods. """ + _self = dict( - _init=False, - client=None, - module=None, - watcher=None, - ) + _init=False, + client=None, + module=None, + watcher=None, + ) def __init__(self, *args, **kwargs): # Use a borg singleton self.__dict__ = self._self # Get config settings - self.prefix = kwargs.pop('prefix', env('PYCONFIG_ETCD_PREFIX', None)) - self.case_sensitive = get('pyconfig.case_sensitive', False) + self.prefix = kwargs.pop("prefix", env("PYCONFIG_ETCD_PREFIX", None)) + self.case_sensitive = get("pyconfig.case_sensitive", False) # Get inheritance settings # XXX shakefu: These might need env vars at some point - self.inherit = kwargs.pop('inherit', True) - self.inherit_key = kwargs.pop('inherit_key', 'config.inherit') - self.inherit_depth = kwargs.pop('inherit_depth', - env('PYCONFIG_INHERIT_DEPTH', 2)) + self.inherit = kwargs.pop("inherit", True) + self.inherit_key = kwargs.pop("inherit_key", "config.inherit") + self.inherit_depth = kwargs.pop( + "inherit_depth", env("PYCONFIG_INHERIT_DEPTH", 2) + ) # See if we should watch for changes - self.watching = kwargs.pop('watch', env('PYCONFIG_ETCD_WATCH', False)) + self.watching = kwargs.pop("watch", env("PYCONFIG_ETCD_WATCH", False)) # Only load the client the first time if not self._init: @@ -361,6 +369,7 @@ def init(self, hosts=None, cacert=None, client_cert=None, client_key=None): # Try to get the etcd module try: import etcd + self.module = etcd except ImportError: pass @@ -371,19 +380,19 @@ def init(self, hosts=None, cacert=None, client_cert=None, client_key=None): self._parse_jetconfig() # Check env for overriding configuration or pyconfig setting - hosts = env('PYCONFIG_ETCD_HOSTS', hosts) - protocol = env('PYCONFIG_ETCD_PROTOCOL', None) - cacert = env('PYCONFIG_ETCD_CACERT', cacert) - client_cert = env('PYCONFIG_ETCD_CERT', client_cert) - client_key = env('PYCONFIG_ETCD_KEY', client_key) + hosts = env("PYCONFIG_ETCD_HOSTS", hosts) + protocol = env("PYCONFIG_ETCD_PROTOCOL", None) + cacert = env("PYCONFIG_ETCD_CACERT", cacert) + client_cert = env("PYCONFIG_ETCD_CERT", client_cert) + client_key = env("PYCONFIG_ETCD_KEY", client_key) # Parse auth string if there is one username = None password = None - auth = env('PYCONFIG_ETCD_AUTH', None) + auth = env("PYCONFIG_ETCD_AUTH", None) if auth: - auth = auth.split(':') - auth.append('') + auth = auth.split(":") + auth.append("") username = auth[0] password = auth[1] @@ -395,28 +404,27 @@ def init(self, hosts=None, cacert=None, client_cert=None, client_key=None): kw = {} # Need this when passing a list of hosts to python-etcd, which we # always do, even if it's a list of one - kw['allow_reconnect'] = True + kw["allow_reconnect"] = True # Grab optional protocol argument if protocol: - kw['protocol'] = protocol + kw["protocol"] = protocol # Add auth to constructor if we got it if username: - kw['username'] = username + kw["username"] = username if password: - kw['password'] = password + kw["password"] = password # Assign the SSL args if we have 'em if cacert: - kw['ca_cert'] = os.path.abspath(cacert) + kw["ca_cert"] = os.path.abspath(cacert) if client_cert and client_key: - kw['cert'] = ((os.path.abspath(client_cert), - os.path.abspath(client_key))) + kw["cert"] = (os.path.abspath(client_cert), os.path.abspath(client_key)) elif client_cert: - kw['cert'] = os.path.abspath(client_cert) + kw["cert"] = os.path.abspath(client_cert) if cacert or client_cert or client_key: - kw['protocol'] = 'https' + kw["protocol"] = "https" self.client = self.module.Client(hosts, **kw) @@ -426,7 +434,7 @@ def load(self, prefix=None, depth=None): """ prefix = prefix or self.prefix - prefix = '/' + prefix.strip('/') + '/' + prefix = "/" + prefix.strip("/") + "/" if depth is None: depth = self.inherit_depth @@ -455,7 +463,7 @@ def load(self, prefix=None, depth=None): # Try to parse them as JSON strings, just in case it works try: value = pytool.json.from_json(value) - except: + except Exception: pass # Make the key lower-case if we're not case-sensitive @@ -464,14 +472,15 @@ def load(self, prefix=None, depth=None): # Strip off the prefix that we're using if key.startswith(prefix): - key = key[len(prefix):] + key = key[len(prefix) :] # Store the key/value to update the config update[key] = value # Access cached settings directly to avoid recursion - inherited = Config().settings.get(self.inherit_key, - update.get(self.inherit_key, None)) + inherited = Config().settings.get( + self.inherit_key, update.get(self.inherit_key, None) + ) if depth > 0 and inherited: log.info(" ... inheriting ...") inherited = self.load(inherited, depth - 1) or {} @@ -490,7 +499,7 @@ def get_watcher(self): return self.client.eternal_watch(self.prefix, recursive=True) def start_watching(self): - """ Begins watching etcd for changes. """ + """Begins watching etcd for changes.""" # Don't create a new watcher thread if we already have one running if self.watcher and self.watcher.is_alive(): return @@ -511,11 +520,11 @@ def _parse_hosts(self, hosts): return # If it's a string, we allow comma separated strings - if isinstance(hosts, six.string_types): + if isinstance(hosts, str): # Split comma-separated list - hosts = [host.strip() for host in hosts.split(',')] + hosts = [host.strip() for host in hosts.split(",")] # Split host and port - hosts = [host.split(':') for host in hosts] + hosts = [host.split(":") for host in hosts] # Coerce ports to int hosts = [(host[0], int(host[1])) for host in hosts] @@ -528,49 +537,51 @@ def _parse_jetconfig(self): (https://github.com/shakefu/jetconfig) that is very sloppy. """ - conf = env('JETCONFIG_ETCD', None) + conf = env("JETCONFIG_ETCD", None) if not conf: return - import urlparse + from urllib.parse import urlparse auth = None port = None - conf = conf.split(',').pop() - entry = urlparse.urlparse(conf) + conf = conf.split(",").pop() + entry = urlparse(conf) scheme = entry.scheme - host = entry.netloc or entry.path # Path is where it goes if there's no - # scheme on the URL + host = entry.netloc or entry.path # Path is where it goes if there's no + # scheme on the URL - if '@' in host: - auth, host = host.split('@') + if "@" in host: + auth, host = host.split("@") - if ':' in host: - host, port = host.split(':') + if ":" in host: + host, port = host.split(":") - if not port and scheme == 'https': - port = '443' + if not port and scheme == "https": + port = "443" if scheme: - os.environ['PYCONFIG_ETCD_PROTOCOL'] = scheme + os.environ["PYCONFIG_ETCD_PROTOCOL"] = scheme if auth: - os.environ['PYCONFIG_ETCD_AUTH'] = auth + os.environ["PYCONFIG_ETCD_AUTH"] = auth if port: host = host + ":" + port - os.environ['PYCONFIG_ETCD_HOSTS'] = host - + os.environ["PYCONFIG_ETCD_HOSTS"] = host # Getter and setter for the prefix to ensure it stays sync'd with the # config and stays normalized def _set_prefix(self, prefix): - if not prefix: return - set('pyconfig.etcd.prefix', '/' + prefix.strip('/') + '/') + if not prefix: + return + set("pyconfig.etcd.prefix", "/" + prefix.strip("/") + "/") + def _get_prefix(self): - return '/'+(get('pyconfig.etcd.prefix') or '/config/').strip('/')+'/' + return "/" + (get("pyconfig.etcd.prefix") or "/config/").strip("/") + "/" + prefix = property(_get_prefix, _set_prefix) @@ -581,6 +592,7 @@ class Watcher(threading.Thread): changes. """ + # Ensure this thread doesn't keep the server from exiting daemon = True @@ -592,17 +604,17 @@ def run(self): for event in etcd().get_watcher(): # We ignore all the events except for 'set', which changes them - if event.action != 'set': + if event.action != "set": continue # Strip the prefix off the key name - key = event.key.replace(etcd().prefix, '', 1) + key = event.key.replace(etcd().prefix, "", 1) # Try to coerce the value from JSON value = event.value try: value = pytool.json.from_json(value) - except: + except Exception: pass # Set the value back to the config @@ -617,10 +629,10 @@ def env(key, default): """ value = os.environ.get(key, None) if value is not None: - log.info(' %s = %r', key.lower().replace('_', '.'), value) + log.info(" %s = %r", key.lower().replace("_", "."), value) return value - key = key.lower().replace('_', '.') + key = key.lower().replace("_", ".") value = get(key) if value is not None: return value @@ -638,7 +650,5 @@ def env_key(key, default): my.database.host => MY_DATABASE_HOST """ - env = key.upper().replace('.', '_') + env = key.upper().replace(".", "_") return os.environ.get(env, default) - - diff --git a/pyconfig/scripts.py b/pyconfig/scripts.py index 4ba98bb..0423a2b 100644 --- a/pyconfig/scripts.py +++ b/pyconfig/scripts.py @@ -1,18 +1,19 @@ -from __future__ import print_function, unicode_literals +import _ast +import argparse +import ast import os import re -import ast import sys -import _ast -import argparse import pyconfig # Pygments is optional but allows for colorization of output try: - import pygments - import pygments.lexers - import pygments.formatters + import importlib + + pygments = importlib.import_module("pygments") + importlib.import_module("pygments.lexers") + importlib.import_module("pygments.formatters") pygments # Make Pyflakes stop bitching about this being unused except ImportError: pygments = None @@ -23,43 +24,66 @@ def main(): Main script for `pyconfig` command. """ - parser = argparse.ArgumentParser(description="Helper for working with " - "pyconfigs") + parser = argparse.ArgumentParser(description="Helper for working with pyconfigs") target_group = parser.add_mutually_exclusive_group() - target_group.add_argument('-f', '--filename', - help="parse an individual file or directory", - metavar='F') - target_group.add_argument('-m', '--module', - help="parse a package or module, recursively looking inside it", - metavar='M') - parser.add_argument('-v', '--view-call', - help="show the actual pyconfig call made (default: show namespace)", - action='store_true') - parser.add_argument('-l', '--load-configs', - help="query the currently set value for each key found", - action='store_true') + target_group.add_argument( + "-f", "--filename", help="parse an individual file or directory", metavar="F" + ) + target_group.add_argument( + "-m", + "--module", + help="parse a package or module, recursively looking inside it", + metavar="M", + ) + parser.add_argument( + "-v", + "--view-call", + help="show the actual pyconfig call made (default: show namespace)", + action="store_true", + ) + parser.add_argument( + "-l", + "--load-configs", + help="query the currently set value for each key found", + action="store_true", + ) key_group = parser.add_mutually_exclusive_group() - key_group.add_argument('-a', '--all', - help="show keys which don't have defaults set", - action='store_true') - key_group.add_argument('-k', '--only-keys', - help="show a list of discovered keys without values", - action='store_true') - parser.add_argument('-n', '--natural-sort', - help="sort by filename and line (default: alphabetical by key)", - action='store_true') - parser.add_argument('-s', '--source', - help="show source annotations (implies --natural-sort)", - action='store_true') - parser.add_argument('-c', '--color', - help="toggle output colors (default: %s)" % bool(pygments), - action='store_const', default=bool(pygments), - const=(not bool(pygments))) + key_group.add_argument( + "-a", + "--all", + help="show keys which don't have defaults set", + action="store_true", + ) + key_group.add_argument( + "-k", + "--only-keys", + help="show a list of discovered keys without values", + action="store_true", + ) + parser.add_argument( + "-n", + "--natural-sort", + help="sort by filename and line (default: alphabetical by key)", + action="store_true", + ) + parser.add_argument( + "-s", + "--source", + help="show source annotations (implies --natural-sort)", + action="store_true", + ) + parser.add_argument( + "-c", + "--color", + help="toggle output colors (default: %s)" % bool(pygments), + action="store_const", + default=bool(pygments), + const=(not bool(pygments)), + ) args = parser.parse_args() if args.color and not pygments: - _error("Pygments is required for color output.\n" - " pip install pygments") + _error("Pygments is required for color output.\n pip install pygments") if args.module: _handle_module(args) @@ -74,8 +98,9 @@ class Unparseable(object): easily parsed - e.g. was not a basic type. """ + def __repr__(self): - return '' + return "" class NotSet(object): @@ -83,8 +108,9 @@ class NotSet(object): This class represents a default value which is not set. """ + def __repr__(self): - return '' + return "" class _PyconfigCall(object): @@ -101,6 +127,7 @@ class _PyconfigCall(object): :type source: tuple """ + def __init__(self, method, key, default, source): self.method = method self.key = key @@ -117,7 +144,7 @@ def as_namespace(self, namespace=None): """ key = self.key if namespace and key.startswith(namespace): - key = key[len(namespace) + 1:] + key = key[len(namespace) + 1 :] return "%s = %s" % (self.get_key(), self._default() or NotSet()) @@ -141,7 +168,7 @@ def as_call(self): """ default = self._default() - default = ', ' + default if default else '' + default = ", " + default if default else "" return "pyconfig.%s(%r%s)" % (self.method, self.get_key(), default) def annotation(self): @@ -161,8 +188,8 @@ def get_key(self): if not isinstance(self.key, Unparseable): return self.key - line = self.source[self.col_offset:] - regex = re.compile('''pyconfig\.[eginst]+\(([^,]+).*?\)''') + line = self.source[self.col_offset :] + regex = re.compile("""pyconfig\.[eginst]+\(([^,]+).*?\)""") match = regex.match(line) if not match: return Unparseable() @@ -174,8 +201,8 @@ def _source_call_only(self): Return the source line stripped down to just the pyconfig call. """ - line = self.source[self.col_offset:] - regex = re.compile('''(pyconfig\.[eginst]+\(['"][^)]+?['"].*?\))''') + line = self.source[self.col_offset :] + regex = re.compile("""(pyconfig\.[eginst]+\(['"][^)]+?['"].*?\))""") match = regex.match(line) if not match: # Fuck it, return the whole line @@ -188,11 +215,11 @@ def _default_value_only(self): Return only the default value, if there is one. """ - line = self.source[self.col_offset:] - regex = re.compile('''pyconfig\.[eginst]+\(['"][^)]+?['"], ?(.*?)\)''') + line = self.source[self.col_offset :] + regex = re.compile("""pyconfig\.[eginst]+\(['"][^)]+?['"], ?(.*?)\)""") match = regex.match(line) if not match: - return '' + return "" return match.group(1) @@ -215,7 +242,7 @@ def _default(self): if default: return default # Otherwise just make it a string and go - return ', '.join(str(v) for v in self.default) + return ", ".join(str(v) for v in self.default) def __repr__(self): return self.as_call() @@ -273,8 +300,8 @@ def _get_module_filename(module): """ # Split up the module and its containing package, if it has one - module = module.split('.') - package = '.'.join(module[:-1]) + module = module.split(".") + package = ".".join(module[:-1]) module = module[-1] try: @@ -288,20 +315,20 @@ def _get_module_filename(module): # Get the module from that package module = getattr(package, module, None) - filename = getattr(module, '__file__', None) + filename = getattr(module, "__file__", None) if not filename: # No filename? Nothing to do here return Unparseable() # If we get a .pyc, strip the c to get .py so we can parse the source - if filename.endswith('.pyc'): + if filename.endswith(".pyc"): filename = filename[:-1] if not os.path.exists(filename) and os.path.isfile(filename): # If there's only a .pyc and no .py it's a compile package or # egg and we can't get at the source for parsing return Unparseable() # If we have a package, we want the directory not the init file - if filename.endswith('__init__.py'): + if filename.endswith("__init__.py"): filename = filename[:-11] # Yey, we found it @@ -348,7 +375,7 @@ def _parse_and_output(filename, args): for key, value in conf.settings.items(): if key in keys: continue - calls.append(_PyconfigCall('set', key, value, [None]*4)) + calls.append(_PyconfigCall("set", key, value, [None] * 4)) _output(calls, args) @@ -380,10 +407,10 @@ def _output(calls, args): out.append(_format_call(call, args)) keys.add(call.key) - out = '\n'.join(out) + out = "\n".join(out) if args.color: out = _colorize(out) - print(out, end=' ') + print(out, end=" ") # We're done here return @@ -400,10 +427,10 @@ def _output(calls, args): continue out.append(_format_call(call, args)) - out = '\n'.join(out) + out = "\n".join(out) if args.color: out = _colorize(out) - print(out, end=' ') + print(out, end=" ") def _format_call(call, args): @@ -415,9 +442,9 @@ def _format_call(call, args): :type call: :class:`_PyconfigCall` """ - out = '' + out = "" if args.source: - out += call.annotation() + '\n' + out += call.annotation() + "\n" if args.only_keys: out += call.get_key() @@ -444,9 +471,11 @@ def _colorize(output): # ['monokai', 'manni', 'rrt', 'perldoc', 'borland', 'colorful', 'default', # 'murphy', 'vs', 'trac', 'tango', 'fruity', 'autumn', 'bw', 'emacs', # 'vim', 'pastie', 'friendly', 'native'] - return pygments.highlight(output, - pygments.lexers.PythonLexer(), - pygments.formatters.Terminal256Formatter(style='monokai')) + return pygments.highlight( + output, + pygments.lexers.PythonLexer(), + pygments.formatters.Terminal256Formatter(style="monokai"), + ) def _parse_dir(directory, relpath): @@ -464,7 +493,7 @@ def _parse_dir(directory, relpath): pyconfig_calls = [] for root, dirs, files in os.walk(directory): for filename in files: - if not filename.endswith('.py'): + if not filename.endswith(".py"): continue filename = os.path.join(root, filename) pyconfig_calls.extend(_parse_file(filename, relpath)) @@ -482,7 +511,7 @@ def _parse_file(filename, relpath=None): :type relpath: str """ - with open(filename, 'r') as source: + with open(filename, "r") as source: source = source.read() pyconfig_calls = [] @@ -494,19 +523,19 @@ def _parse_file(filename, relpath=None): # Look for UTF-8 encoding first_lines = source[0:200] - match = re.match('^#.*coding[:=].?([a-zA-Z0-9-_]+).*', first_lines) + match = re.match("^#.*coding[:=].?([a-zA-Z0-9-_]+).*", first_lines) if match: try: coding = match.group(1) source = source.decode(coding) - except: + except Exception: print("# Error decoding file, may not parse correctly:", filename) try: # Split the source into lines so we can reference it easily - source = source.split('\n') - except: - print("# Error parsing file, ignoring:", filename); + source = source.split("\n") + except Exception: + print("# Error parsing file, ignoring:", filename) return [] # Make the filename relative to the given path, if needed @@ -524,12 +553,12 @@ def _parse_file(filename, relpath=None): # an Attribute node, otherwise skip it continue - if getattr(func.value, 'id', None) != 'pyconfig': + if getattr(func.value, "id", None) != "pyconfig": # If the Attribute value isn't a Name (doesn't have an `id`) or it # isn't 'pyconfig', then we skip continue - if func.attr not in ['get', 'set', 'setting']: + if func.attr not in ["get", "set", "setting"]: # If the Attribute attr isn't one of the pyconfig API methods, then # we skip continue @@ -546,7 +575,7 @@ def _parse_file(filename, relpath=None): for arg in call.args[1:]: args.append(_map_arg(arg)) - line = (filename, source[call.lineno-1], call.lineno, call.col_offset) + line = (filename, source[call.lineno - 1], call.lineno, call.col_offset) call = _PyconfigCall(func.attr, args[0], args[1:], line) pyconfig_calls.append(call) @@ -565,14 +594,13 @@ def _map_arg(arg): return arg.n elif isinstance(arg, _ast.Name): name = arg.id - if name == 'True': + if name == "True": return True - elif name == 'False': + elif name == "False": return False - elif name == 'None': + elif name == "None": return None return name else: # Everything else we don't bother with return Unparseable() - diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2b04ccb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "pyconfig" +version = "3.2.3" +description = "Python-based singleton configuration" +readme = "README.md" +license = "MIT" +authors = [ + {name = "Jacob Alheid", email = "shakefu@gmail.com"}, +] +requires-python = ">=3.9,<4.0" +dependencies = [ + "pytool>=6.0.3", + "simplejson >=3.19.2, <4.0.0", +] + +[dependency-groups] +etcd = [ + # "python-etcd", + "python-etcd>=0.4.5", +] +dev = [ + "configargparse >=1.7, <2.0", + "coverage >=6.5.0, <7.0.0", + "coveralls >=3.3.1, <4.0.0", + "pre-commit >=4.2.0, <5.0.0", + "pytest >=8.4.0, <9.0.0", + "pytest-cov >=4.1.0, <5.0.0", + "pytest-mock >=3.14.0, <4.0.0", + "ruff>=0.12.0", +] +docs = [ +] + +# This is required so setuptools doesn't see "script" as a package +[tool.setuptools] +packages = ["pyconfig"] diff --git a/setup.py b/setup.py deleted file mode 100644 index d07f434..0000000 --- a/setup.py +++ /dev/null @@ -1,61 +0,0 @@ -import multiprocessing, logging # Fix atexit bug -from setuptools import setup, find_packages - - -def readme(): - try: - return open('README.rst').read() - except: - pass - return '' - - -def version(): - try: - import re - return re.search("^__version__ = '(.*)'", - open('pyconfig/__init__.py').read(), re.M).group(1) - except: - raise RuntimeError("Could not get version") - - -setup( - name='pyconfig', - version=version(), - description="Python-based singleton configuration", - long_description=readme(), - author="Jacob Alheid", - author_email="shakefu@gmail.com", - url="http://github.com/shakefu/pyconfig", - packages=find_packages(exclude=['test']), - test_suite='nose.collector', - install_requires=[ - 'six', - 'pytool', - ], - tests_require=[ - 'nose', - 'coverage', - 'mock', - ], - extras_require={ - 'etcd': ['python-etcd'] - }, - entry_points={ - 'console_scripts':[ - 'pyconfig = pyconfig.scripts:main', - ], - }, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Utilities', - ], - ) - diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..d41a734 --- /dev/null +++ b/uv.lock @@ -0,0 +1,649 @@ +version = 1 +revision = 2 +requires-python = ">=3.9, <4.0" + +[[package]] +name = "certifi" +version = "2025.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, + { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, + { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, + { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, + { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "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 = "configargparse" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/6c9ef746dfcc2a32e26f3860bb4a011c008c392b83eabdfb598d1a8bbe5d/configargparse-1.7.1.tar.gz", hash = "sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9", size = 43958, upload-time = "2025-05-23T14:26:17.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size = 25607, upload-time = "2025-05-23T14:26:15.923Z" }, +] + +[[package]] +name = "coverage" +version = "6.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/66/38d1870cb7cf62da49add1d6803fdbcdef632b2808b5c80bcac35b7634d8/coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", size = 775224, upload-time = "2022-09-29T20:05:58.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/8d/5ec7d08f4601d2d792563fe31db5e9322c306848fec1e65ec8885927f739/coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", size = 185264, upload-time = "2022-09-29T20:04:39.481Z" }, + { url = "https://files.pythonhosted.org/packages/89/a2/cbf599e50bb4be416e0408c4cf523c354c51d7da39935461a9687e039481/coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", size = 185482, upload-time = "2022-09-29T20:04:41.703Z" }, + { url = "https://files.pythonhosted.org/packages/15/b0/3639d84ee8a900da0cf6450ab46e22517e4688b6cec0ba8ab6f8166103a2/coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", size = 214083, upload-time = "2022-09-29T20:04:43.294Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/c6025ba30f2ce21d20d5332c3819880fe8afdfc008c2e2f9c075c7b67543/coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", size = 212396, upload-time = "2022-09-29T20:04:44.809Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7d/d5211ea782b193ab8064b06dc0cc042cf1a4ca9c93a530071459172c550f/coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", size = 213270, upload-time = "2022-09-29T20:04:46.291Z" }, + { url = "https://files.pythonhosted.org/packages/10/9e/68e384940179713640743a010ac7f7c813d1087c8730a9c0bdfa73bdffd7/coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", size = 219188, upload-time = "2022-09-29T20:04:47.728Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8b/ca3fe3cfbd66d63181f6e6a06b8b494bb327ba8222d2fa628b392b9ad08a/coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", size = 217430, upload-time = "2022-09-29T20:04:49.098Z" }, + { url = "https://files.pythonhosted.org/packages/c0/18/2a0a9b3c29376ce04ceb7ca2948559dad76409a2c9b3f664756581101e16/coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", size = 218646, upload-time = "2022-09-29T20:04:50.582Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/7afba355bdabc550b3b2669e3432e71aec87d79400372d7686c09aab0acf/coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", size = 187602, upload-time = "2022-09-29T20:04:52.509Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a3/f45cb5d32de0751863945d22083c15eb8854bb53681b2e792f2066c629b9/coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", size = 188510, upload-time = "2022-09-29T20:04:54.421Z" }, + { url = "https://files.pythonhosted.org/packages/50/cf/455930004231fa87efe8be06d13512f34e070ddfee8b8bf5a050cdc47ab3/coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", size = 185433, upload-time = "2022-09-29T20:04:55.856Z" }, + { url = "https://files.pythonhosted.org/packages/36/f3/5cbd79cf4cd059c80b59104aca33b8d05af4ad5bf5b1547645ecee716378/coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", size = 217736, upload-time = "2022-09-29T20:04:57.242Z" }, + { url = "https://files.pythonhosted.org/packages/89/58/5ec19b43a6511288511f64fc4763d95af8403f5926e7e4556e6b29b03a26/coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", size = 215313, upload-time = "2022-09-29T20:04:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/6a/63/8e82513b7e4a1b8d887b4e85c1c2b6c9b754a581b187c0b084f3330ac479/coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", size = 217115, upload-time = "2022-09-29T20:05:00.194Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bc/c9d4fd6b3494d2cc1e26f4b98eb19206b92a59094617ad02d5689ac9d3c4/coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", size = 226072, upload-time = "2022-09-29T20:05:01.635Z" }, + { url = "https://files.pythonhosted.org/packages/78/98/253ce0cfcc3b352d3072940940ed44a035614f2abe781477f77038d21d9f/coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", size = 224486, upload-time = "2022-09-29T20:05:03.158Z" }, + { url = "https://files.pythonhosted.org/packages/4b/66/6e588f5dfc93ccedd06d6785c8143f17bb92b89247d50128d8789e9588d0/coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", size = 225537, upload-time = "2022-09-29T20:05:04.646Z" }, + { url = "https://files.pythonhosted.org/packages/ff/27/339089b558672f04e62d0cd2d49b9280270bad3bc95de24e7eb03deb4638/coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", size = 187586, upload-time = "2022-09-29T20:05:06.22Z" }, + { url = "https://files.pythonhosted.org/packages/e6/24/7fe8ededb4060dd8c3f1d86cb624fcb3452f66fbef5051ed7fab126c5c0c/coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", size = 188604, upload-time = "2022-09-29T20:05:09.007Z" }, + { url = "https://files.pythonhosted.org/packages/ea/52/c08080405329326a7ff16c0dfdb4feefaa8edd7446413df67386fe1bbfe0/coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", size = 185254, upload-time = "2022-09-29T20:05:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/e9/f23e8664ec4032d7802a1cf920853196bcbdce7b56408e3efe1b2da08f3c/coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", size = 185473, upload-time = "2022-09-29T20:05:41.817Z" }, + { url = "https://files.pythonhosted.org/packages/18/95/27f80dcd8273171b781a19d109aeaed7f13d78ef6d1e2f7134a5826fd1b4/coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", size = 213661, upload-time = "2022-09-29T20:05:43.157Z" }, + { url = "https://files.pythonhosted.org/packages/d6/0f/012a7370aaf61123a222b34b657dedc63e03ce2af8d064ac5c5afe14f29c/coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", size = 211997, upload-time = "2022-09-29T20:05:44.563Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/919f0fdc93d3991ca074894402074d847be8ac1e1d78e7e9e1c371b69a6f/coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", size = 212818, upload-time = "2022-09-29T20:05:46.02Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d9/b367c52cb1297414ba967e38fe9b5338ee4700a2d1592fc78532dc9f882f/coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", size = 218775, upload-time = "2022-09-29T20:05:47.832Z" }, + { url = "https://files.pythonhosted.org/packages/58/2c/213861cec1d9f6451d29c0b1838769b558f6a8c6844b001f6e98c37c4b1b/coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", size = 217024, upload-time = "2022-09-29T20:05:49.425Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/e712b61abf1282ce3ac9826473ab4b245a4319303cce2e4115a8de1435f2/coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", size = 218221, upload-time = "2022-09-29T20:05:51.238Z" }, + { url = "https://files.pythonhosted.org/packages/8f/17/e1d54e0e5a1e82dea1b1d9463dfe347ded58037beda00d326f943a9ef2d4/coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", size = 187617, upload-time = "2022-09-29T20:05:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/b6/08/a88a9f3a11bb2d97c7a6719535a984b009728433838fbc65766488867c80/coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987", size = 188527, upload-time = "2022-09-29T20:05:54.304Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "coveralls" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "docopt" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c5/6b8254092117fa366b022fbee9434224483ba38e0bbf36e80836bf10692a/coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea", size = 17964, upload-time = "2021-11-11T21:00:05.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/d6/e95db3c56afd1eff815db7dda01908f5e57770e47042209db448ab496197/coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026", size = 14700, upload-time = "2021-11-11T21:00:03.875Z" }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "docopt" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, +] + +[[package]] +name = "identify" +version = "2.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "pluggy" +version = "1.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 = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, +] + +[[package]] +name = "pyconfig" +version = "3.2.3" +source = { virtual = "." } +dependencies = [ + { name = "pytool" }, + { name = "simplejson" }, +] + +[package.dev-dependencies] +dev = [ + { name = "configargparse" }, + { name = "coverage" }, + { name = "coveralls" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "ruff" }, +] +etcd = [ + { name = "python-etcd" }, +] + +[package.metadata] +requires-dist = [ + { name = "pytool", specifier = ">=6.0.3" }, + { name = "simplejson", specifier = ">=3.19.2,<4.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "configargparse", specifier = ">=1.7,<2.0" }, + { name = "coverage", specifier = ">=6.5.0,<7.0.0" }, + { name = "coveralls", specifier = ">=3.3.1,<4.0.0" }, + { name = "pre-commit", specifier = ">=4.2.0,<5.0.0" }, + { name = "pytest", specifier = ">=8.4.0,<9.0.0" }, + { name = "pytest-cov", specifier = ">=4.1.0,<5.0.0" }, + { name = "pytest-mock", specifier = ">=3.14.0,<4.0.0" }, + { name = "ruff", specifier = ">=0.12.0" }, +] +docs = [] +etcd = [{ name = "python-etcd", specifier = ">=0.4.5" }] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245, upload-time = "2023-05-24T18:44:56.845Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/4b/8b78d126e275efa2379b1c2e09dc52cf70df16fc3b90613ef82531499d73/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a", size = 21949, upload-time = "2023-05-24T18:44:54.079Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, +] + +[[package]] +name = "python-etcd" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/da/616a4d073642da5dd432e5289b7c1cb0963cc5dde23d1ecb8d726821ab41/python-etcd-0.4.5.tar.gz", hash = "sha256:f1b5ebb825a3e8190494f5ce1509fde9069f2754838ed90402a8c11e1f52b8cb", size = 37270, upload-time = "2017-03-02T23:05:36.016Z" } + +[[package]] +name = "pytool" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simplejson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/13/9884ee0c6aa49774eeccd0558e26c9325bd15384b0ecff2038a29d457458/pytool-6.0.3.tar.gz", hash = "sha256:4cd8ae2aab998c672726194bc09115f2c7b25ad276b2fd6cc37df959f1abcbb2", size = 31060, upload-time = "2025-06-10T20:55:53.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/27/90a1da41e5c493f7f6173e8ddb29341d4e004b1d69626076a2cb5781d537/pytool-6.0.3-py3-none-any.whl", hash = "sha256:50c5284fbe8abe4170bd2c4c05301b424d9c469cd4254f7a4e91b8aefb61011a", size = 23607, upload-time = "2025-06-10T20:55:52.274Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" }, + { url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" }, + { url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" }, + { url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" }, + { url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" }, + { url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" }, + { url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" }, + { url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" }, + { url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" }, +] + +[[package]] +name = "simplejson" +version = "3.20.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/92/51b417685abd96b31308b61b9acce7ec50d8e1de8fbc39a7fd4962c60689/simplejson-3.20.1.tar.gz", hash = "sha256:e64139b4ec4f1f24c142ff7dcafe55a22b811a74d86d66560c8815687143037d", size = 85591, upload-time = "2025-02-15T05:18:53.15Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/c4/627214fb418cd4a17fb0230ff0b6c3bb4a85cbb48dd69c85dcc3b85df828/simplejson-3.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e580aa65d5f6c3bf41b9b4afe74be5d5ddba9576701c107c772d936ea2b5043a", size = 93790, upload-time = "2025-02-15T05:15:32.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/ca/56a6a2a33cbcf330c4d71af3f827c47e4e0ba791e78f2642f3d1ab02ff31/simplejson-3.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a586ce4f78cec11f22fe55c5bee0f067e803aab9bad3441afe2181693b5ebb5", size = 75707, upload-time = "2025-02-15T05:15:34.954Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c8/3d92b67e03a3b6207d97202669f9454ed700b35ade9bd4428265a078fb6c/simplejson-3.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74a1608f9e6e8c27a4008d70a54270868306d80ed48c9df7872f9f4b8ac87808", size = 75700, upload-time = "2025-02-15T05:15:37.144Z" }, + { url = "https://files.pythonhosted.org/packages/74/30/20001219d6fdca4aaa3974c96dfb6955a766b4e2cc950505a5b51fd050b0/simplejson-3.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03db8cb64154189a92a7786209f24e391644f3a3fa335658be2df2af1960b8d8", size = 138672, upload-time = "2025-02-15T05:15:38.547Z" }, + { url = "https://files.pythonhosted.org/packages/21/47/50157810876c2a7ebbd6e6346ec25eda841fe061fecaa02538a7742a3d2a/simplejson-3.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eea7e2b7d858f6fdfbf0fe3cb846d6bd8a45446865bc09960e51f3d473c2271b", size = 146616, upload-time = "2025-02-15T05:15:39.871Z" }, + { url = "https://files.pythonhosted.org/packages/95/60/8c97cdc93096437b0aca2745aca63c880fe2315fd7f6a6ce6edbb344a2ae/simplejson-3.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e66712b17d8425bb7ff8968d4c7c7fd5a2dd7bd63728b28356223c000dd2f91f", size = 134344, upload-time = "2025-02-15T05:15:42.091Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9e/da184f0e9bb3a5d7ffcde713bd41b4fe46cca56b6f24d9bd155fac56805a/simplejson-3.20.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2cc4f6486f9f515b62f5831ff1888886619b84fc837de68f26d919ba7bbdcbc", size = 138017, upload-time = "2025-02-15T05:15:43.542Z" }, + { url = "https://files.pythonhosted.org/packages/31/db/00d1a8d9b036db98f678c8a3c69ed17d2894d1768d7a00576e787ad3e546/simplejson-3.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3c2df555ee4016148fa192e2b9cd9e60bc1d40769366134882685e90aee2a1e", size = 140118, upload-time = "2025-02-15T05:15:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/52/21/57fc47eab8c1c73390b933a5ba9271f08e3e1ec83162c580357f28f5b97c/simplejson-3.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78520f04b7548a5e476b5396c0847e066f1e0a4c0c5e920da1ad65e95f410b11", size = 140314, upload-time = "2025-02-15T05:16:07.949Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cc/7cfd78d1e0fa5e57350b98cfe77353b6dfa13dce21afa4060e1019223852/simplejson-3.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f4bd49ecde87b0fe9f55cc971449a32832bca9910821f7072bbfae1155eaa007", size = 148544, upload-time = "2025-02-15T05:16:09.455Z" }, + { url = "https://files.pythonhosted.org/packages/63/26/1c894a1c2bd95dc8be0cf5a2fa73b0d173105b6ca18c90cb981ff10443d0/simplejson-3.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7eaae2b88eb5da53caaffdfa50e2e12022553949b88c0df4f9a9663609373f72", size = 141172, upload-time = "2025-02-15T05:16:10.966Z" }, + { url = "https://files.pythonhosted.org/packages/93/27/0717dccc10cd9988dbf1314def52ab32678a95a95328bb37cafacf499400/simplejson-3.20.1-cp310-cp310-win32.whl", hash = "sha256:e836fb88902799eac8debc2b642300748f4860a197fa3d9ea502112b6bb8e142", size = 74181, upload-time = "2025-02-15T05:16:12.361Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/593f896573f306519332d4287b1ab8b7b888c239bbd5159f7054d7055c2d/simplejson-3.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:b122a19b552b212fc3b5b96fc5ce92333d4a9ac0a800803e1f17ebb16dac4be5", size = 75738, upload-time = "2025-02-15T05:16:14.438Z" }, + { url = "https://files.pythonhosted.org/packages/76/59/74bc90d1c051bc2432c96b34bd4e8036875ab58b4fcbe4d6a5a76985f853/simplejson-3.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:325b8c107253d3217e89d7b50c71015b5b31e2433e6c5bf38967b2f80630a8ca", size = 92132, upload-time = "2025-02-15T05:16:15.743Z" }, + { url = "https://files.pythonhosted.org/packages/71/c7/1970916e0c51794fff89f76da2f632aaf0b259b87753c88a8c409623d3e1/simplejson-3.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88a7baa8211089b9e58d78fbc1b0b322103f3f3d459ff16f03a36cece0d0fcf0", size = 74956, upload-time = "2025-02-15T05:16:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/c8/0d/98cc5909180463f1d75fac7180de62d4cdb4e82c4fef276b9e591979372c/simplejson-3.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:299b1007b8101d50d95bc0db1bf5c38dc372e85b504cf77f596462083ee77e3f", size = 74772, upload-time = "2025-02-15T05:16:19.204Z" }, + { url = "https://files.pythonhosted.org/packages/e1/94/a30a5211a90d67725a3e8fcc1c788189f2ae2ed2b96b63ed15d0b7f5d6bb/simplejson-3.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ec618ed65caab48e81e3ed29586236a8e57daef792f1f3bb59504a7e98cd10", size = 143575, upload-time = "2025-02-15T05:16:21.337Z" }, + { url = "https://files.pythonhosted.org/packages/ee/08/cdb6821f1058eb5db46d252de69ff7e6c53f05f1bae6368fe20d5b51d37e/simplejson-3.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2cdead1d3197f0ff43373cf4730213420523ba48697743e135e26f3d179f38", size = 153241, upload-time = "2025-02-15T05:16:22.859Z" }, + { url = "https://files.pythonhosted.org/packages/4c/2d/ca3caeea0bdc5efc5503d5f57a2dfb56804898fb196dfada121323ee0ccb/simplejson-3.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3466d2839fdc83e1af42e07b90bc8ff361c4e8796cd66722a40ba14e458faddd", size = 141500, upload-time = "2025-02-15T05:16:25.068Z" }, + { url = "https://files.pythonhosted.org/packages/e1/33/d3e0779d5c58245e7370c98eb969275af6b7a4a5aec3b97cbf85f09ad328/simplejson-3.20.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d492ed8e92f3a9f9be829205f44b1d0a89af6582f0cf43e0d129fa477b93fe0c", size = 144757, upload-time = "2025-02-15T05:16:28.301Z" }, + { url = "https://files.pythonhosted.org/packages/54/53/2d93128bb55861b2fa36c5944f38da51a0bc6d83e513afc6f7838440dd15/simplejson-3.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f924b485537b640dc69434565463fd6fc0c68c65a8c6e01a823dd26c9983cf79", size = 144409, upload-time = "2025-02-15T05:16:29.687Z" }, + { url = "https://files.pythonhosted.org/packages/99/4c/dac310a98f897ad3435b4bdc836d92e78f09e38c5dbf28211ed21dc59fa2/simplejson-3.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e8eacf6a3491bf76ea91a8d46726368a6be0eb94993f60b8583550baae9439e", size = 146082, upload-time = "2025-02-15T05:16:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ee/22/d7ba958cfed39827335b82656b1c46f89678faecda9a7677b47e87b48ee6/simplejson-3.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d04bf90b4cea7c22d8b19091633908f14a096caa301b24c2f3d85b5068fb8", size = 154339, upload-time = "2025-02-15T05:16:32.719Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c8/b072b741129406a7086a0799c6f5d13096231bf35fdd87a0cffa789687fc/simplejson-3.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:69dd28d4ce38390ea4aaf212902712c0fd1093dc4c1ff67e09687c3c3e15a749", size = 147915, upload-time = "2025-02-15T05:16:34.291Z" }, + { url = "https://files.pythonhosted.org/packages/6c/46/8347e61e9cf3db5342a42f7fd30a81b4f5cf85977f916852d7674a540907/simplejson-3.20.1-cp311-cp311-win32.whl", hash = "sha256:dfe7a9da5fd2a3499436cd350f31539e0a6ded5da6b5b3d422df016444d65e43", size = 73972, upload-time = "2025-02-15T05:16:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/01/85/b52f24859237b4e9d523d5655796d911ba3d46e242eb1959c45b6af5aedd/simplejson-3.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:896a6c04d7861d507d800da7642479c3547060bf97419d9ef73d98ced8258766", size = 75595, upload-time = "2025-02-15T05:16:36.957Z" }, + { url = "https://files.pythonhosted.org/packages/8d/eb/34c16a1ac9ba265d024dc977ad84e1659d931c0a700967c3e59a98ed7514/simplejson-3.20.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f31c4a3a7ab18467ee73a27f3e59158255d1520f3aad74315edde7a940f1be23", size = 93100, upload-time = "2025-02-15T05:16:38.801Z" }, + { url = "https://files.pythonhosted.org/packages/41/fc/2c2c007d135894971e6814e7c0806936e5bade28f8db4dd7e2a58b50debd/simplejson-3.20.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:884e6183d16b725e113b83a6fc0230152ab6627d4d36cb05c89c2c5bccfa7bc6", size = 75464, upload-time = "2025-02-15T05:16:40.905Z" }, + { url = "https://files.pythonhosted.org/packages/0f/05/2b5ecb33b776c34bb5cace5de5d7669f9b60e3ca13c113037b2ca86edfbd/simplejson-3.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03d7a426e416fe0d3337115f04164cd9427eb4256e843a6b8751cacf70abc832", size = 75112, upload-time = "2025-02-15T05:16:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/1f3609a2792f06cd4b71030485f78e91eb09cfd57bebf3116bf2980a8bac/simplejson-3.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:000602141d0bddfcff60ea6a6e97d5e10c9db6b17fd2d6c66199fa481b6214bb", size = 150182, upload-time = "2025-02-15T05:16:43.557Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b0/053fbda38b8b602a77a4f7829def1b4f316cd8deb5440a6d3ee90790d2a4/simplejson-3.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af8377a8af78226e82e3a4349efdde59ffa421ae88be67e18cef915e4023a595", size = 158363, upload-time = "2025-02-15T05:16:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4b/2eb84ae867539a80822e92f9be4a7200dffba609275faf99b24141839110/simplejson-3.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15c7de4c88ab2fbcb8781a3b982ef883696736134e20b1210bca43fb42ff1acf", size = 148415, upload-time = "2025-02-15T05:16:47.861Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bd/400b0bd372a5666addf2540c7358bfc3841b9ce5cdbc5cc4ad2f61627ad8/simplejson-3.20.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:455a882ff3f97d810709f7b620007d4e0aca8da71d06fc5c18ba11daf1c4df49", size = 152213, upload-time = "2025-02-15T05:16:49.25Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/143f447bf6a827ee9472693768dc1a5eb96154f8feb140a88ce6973a3cfa/simplejson-3.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fc0f523ce923e7f38eb67804bc80e0a028c76d7868500aa3f59225574b5d0453", size = 150048, upload-time = "2025-02-15T05:16:51.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ea/dd9b3e8e8ed710a66f24a22c16a907c9b539b6f5f45fd8586bd5c231444e/simplejson-3.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76461ec929282dde4a08061071a47281ad939d0202dc4e63cdd135844e162fbc", size = 151668, upload-time = "2025-02-15T05:16:53Z" }, + { url = "https://files.pythonhosted.org/packages/99/af/ee52a8045426a0c5b89d755a5a70cc821815ef3c333b56fbcad33c4435c0/simplejson-3.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19c2da8c043607bde4d4ef3a6b633e668a7d2e3d56f40a476a74c5ea71949f", size = 158840, upload-time = "2025-02-15T05:16:54.851Z" }, + { url = "https://files.pythonhosted.org/packages/68/db/ab32869acea6b5de7d75fa0dac07a112ded795d41eaa7e66c7813b17be95/simplejson-3.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2578bedaedf6294415197b267d4ef678fea336dd78ee2a6d2f4b028e9d07be3", size = 154212, upload-time = "2025-02-15T05:16:56.318Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/e3132d454977d75a3bf9a6d541d730f76462ebf42a96fea2621498166f41/simplejson-3.20.1-cp312-cp312-win32.whl", hash = "sha256:339f407373325a36b7fd744b688ba5bae0666b5d340ec6d98aebc3014bf3d8ea", size = 74101, upload-time = "2025-02-15T05:16:57.746Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5d/4e243e937fa3560107c69f6f7c2eed8589163f5ed14324e864871daa2dd9/simplejson-3.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:627d4486a1ea7edf1f66bb044ace1ce6b4c1698acd1b05353c97ba4864ea2e17", size = 75736, upload-time = "2025-02-15T05:16:59.017Z" }, + { url = "https://files.pythonhosted.org/packages/c4/03/0f453a27877cb5a5fff16a975925f4119102cc8552f52536b9a98ef0431e/simplejson-3.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:71e849e7ceb2178344998cbe5ade101f1b329460243c79c27fbfc51c0447a7c3", size = 93109, upload-time = "2025-02-15T05:17:00.377Z" }, + { url = "https://files.pythonhosted.org/packages/74/1f/a729f4026850cabeaff23e134646c3f455e86925d2533463420635ae54de/simplejson-3.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b63fdbab29dc3868d6f009a59797cefaba315fd43cd32ddd998ee1da28e50e29", size = 75475, upload-time = "2025-02-15T05:17:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/50a2713fee8ff1f8d655b1a14f4a0f1c0c7246768a1b3b3d12964a4ed5aa/simplejson-3.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1190f9a3ce644fd50ec277ac4a98c0517f532cfebdcc4bd975c0979a9f05e1fb", size = 75112, upload-time = "2025-02-15T05:17:03.875Z" }, + { url = "https://files.pythonhosted.org/packages/45/86/ea9835abb646755140e2d482edc9bc1e91997ed19a59fd77ae4c6a0facea/simplejson-3.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1336ba7bcb722ad487cd265701ff0583c0bb6de638364ca947bb84ecc0015d1", size = 150245, upload-time = "2025-02-15T05:17:06.899Z" }, + { url = "https://files.pythonhosted.org/packages/12/b4/53084809faede45da829fe571c65fbda8479d2a5b9c633f46b74124d56f5/simplejson-3.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e975aac6a5acd8b510eba58d5591e10a03e3d16c1cf8a8624ca177491f7230f0", size = 158465, upload-time = "2025-02-15T05:17:08.707Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7d/d56579468d1660b3841e1f21c14490d103e33cf911886b22652d6e9683ec/simplejson-3.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a6dd11ee282937ad749da6f3b8d87952ad585b26e5edfa10da3ae2536c73078", size = 148514, upload-time = "2025-02-15T05:17:11.323Z" }, + { url = "https://files.pythonhosted.org/packages/19/e3/874b1cca3d3897b486d3afdccc475eb3a09815bf1015b01cf7fcb52a55f0/simplejson-3.20.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab980fcc446ab87ea0879edad41a5c28f2d86020014eb035cf5161e8de4474c6", size = 152262, upload-time = "2025-02-15T05:17:13.543Z" }, + { url = "https://files.pythonhosted.org/packages/32/84/f0fdb3625292d945c2bd13a814584603aebdb38cfbe5fe9be6b46fe598c4/simplejson-3.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f5aee2a4cb6b146bd17333ac623610f069f34e8f31d2f4f0c1a2186e50c594f0", size = 150164, upload-time = "2025-02-15T05:17:15.021Z" }, + { url = "https://files.pythonhosted.org/packages/95/51/6d625247224f01eaaeabace9aec75ac5603a42f8ebcce02c486fbda8b428/simplejson-3.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:652d8eecbb9a3b6461b21ec7cf11fd0acbab144e45e600c817ecf18e4580b99e", size = 151795, upload-time = "2025-02-15T05:17:16.542Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d9/bb921df6b35be8412f519e58e86d1060fddf3ad401b783e4862e0a74c4c1/simplejson-3.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8c09948f1a486a89251ee3a67c9f8c969b379f6ffff1a6064b41fea3bce0a112", size = 159027, upload-time = "2025-02-15T05:17:18.083Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/5950605e4ad023a6621cf4c931b29fd3d2a9c1f36be937230bfc83d7271d/simplejson-3.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cbbd7b215ad4fc6f058b5dd4c26ee5c59f72e031dfda3ac183d7968a99e4ca3a", size = 154380, upload-time = "2025-02-15T05:17:20.334Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/b74149557c5ec1e4e4d55758bda426f5d2ec0123cd01a53ae63b8de51fa3/simplejson-3.20.1-cp313-cp313-win32.whl", hash = "sha256:ae81e482476eaa088ef9d0120ae5345de924f23962c0c1e20abbdff597631f87", size = 74102, upload-time = "2025-02-15T05:17:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/db/a9/25282fdd24493e1022f30b7f5cdf804255c007218b2bfaa655bd7ad34b2d/simplejson-3.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:1b9fd15853b90aec3b1739f4471efbf1ac05066a2c7041bf8db821bb73cd2ddc", size = 75736, upload-time = "2025-02-15T05:17:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ba/d32fe890a5edaf4a8518adf043bccf7866b600123f512a6de0988cf36810/simplejson-3.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a8011f1dd1d676befcd4d675ebdbfdbbefd3bf350052b956ba8c699fca7d8cef", size = 93773, upload-time = "2025-02-15T05:18:28.231Z" }, + { url = "https://files.pythonhosted.org/packages/48/c7/361e7f6695b56001a04e0a5cc623cd6c82ea2f45e872e61213e405cc8a24/simplejson-3.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e91703a4c5fec53e36875ae426ad785f4120bd1d93b65bed4752eeccd1789e0c", size = 75697, upload-time = "2025-02-15T05:18:30.006Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2f/d0ff0b772d4ef092876eb85c99bc591c446b0502715551dad7dfc7f7c2c0/simplejson-3.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e39eaa57c7757daa25bcd21f976c46be443b73dd6c3da47fe5ce7b7048ccefe2", size = 75692, upload-time = "2025-02-15T05:18:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/26/94/cab4db9530b6ca9d62f16a260e8311b04130ccd670dab75e958fcb44590e/simplejson-3.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceab2ce2acdc7fbaa433a93006758db6ba9a659e80c4faa13b80b9d2318e9b17", size = 138106, upload-time = "2025-02-15T05:18:32.907Z" }, + { url = "https://files.pythonhosted.org/packages/40/22/11c0f746bdb44c297cea8a37d8f7ccb75ea6681132aadfb9f820d9a52647/simplejson-3.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d4f320c33277a5b715db5bf5b10dae10c19076bd6d66c2843e04bd12d1f1ea5", size = 146242, upload-time = "2025-02-15T05:18:35.223Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/b7c4c26f29b41cc41ba5f0224c47adbfa7f28427418edfd58ab122f3b584/simplejson-3.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b6436c48e64378fa844d8c9e58a5ed0352bbcfd4028369a9b46679b7ab79d2d", size = 133866, upload-time = "2025-02-15T05:18:36.998Z" }, + { url = "https://files.pythonhosted.org/packages/09/68/1e81ed83f38906c8859f2b973afb19302357d6003e724a6105cee0f61ec7/simplejson-3.20.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e18345c8dda5d699be8166b61f9d80aaee4545b709f1363f60813dc032dac53", size = 137444, upload-time = "2025-02-15T05:18:38.763Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6b/8d1e076c543277c1d603230eec24f4dd75ebce46d351c0679526d202981f/simplejson-3.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:90b573693d1526bed576f6817e2a492eaaef68f088b57d7a9e83d122bbb49e51", size = 139617, upload-time = "2025-02-15T05:18:40.36Z" }, + { url = "https://files.pythonhosted.org/packages/d1/46/7b74803de10d4157c5cd2e89028897fa733374667bc5520a44b23b6c887a/simplejson-3.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:272cc767826e924a6bd369ea3dbf18e166ded29059c7a4d64d21a9a22424b5b5", size = 139725, upload-time = "2025-02-15T05:18:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8f/9991582665a7b6d95415e439bb4fbaa4faf0f77231666675a0fd1de54107/simplejson-3.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:51b41f284d603c4380732d7d619f8b34bd04bc4aa0ed0ed5f4ffd0539b14da44", size = 148010, upload-time = "2025-02-15T05:18:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/54/ee/3c6e91989cdf65ec75e75662d9f15cfe167a792b893806169ea5b1da6fd2/simplejson-3.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6e6697a3067d281f01de0fe96fc7cba4ea870d96d7deb7bfcf85186d74456503", size = 140624, upload-time = "2025-02-15T05:18:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/9d/bd/05e13ebb7ead81c8b555f4ccc741ea7dfa0ef5c2a0c183d6a7bc50a02bca/simplejson-3.20.1-cp39-cp39-win32.whl", hash = "sha256:6dd3a1d5aca87bf947f3339b0f8e8e329f1badf548bdbff37fac63c17936da8e", size = 74148, upload-time = "2025-02-15T05:18:47.27Z" }, + { url = "https://files.pythonhosted.org/packages/88/c9/d8bf87aaebec5a4c3ccfd5228689578e2fe77027d6114a259255d54969bf/simplejson-3.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:463f1fca8fbf23d088e5850fdd0dd4d5faea8900a9f9680270bd98fd649814ca", size = 75732, upload-time = "2025-02-15T05:18:49.598Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/00f02a0a921556dd5a6db1ef2926a1bc7a8bbbfb1c49cfed68a275b8ab2b/simplejson-3.20.1-py3-none-any.whl", hash = "sha256:8a6c1bbac39fa4a79f83cbf1df6ccd8ff7069582a9fd8db1e52cea073bc2c697", size = 57121, upload-time = "2025-02-15T05:18:51.243Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.31.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, +] From 313e13cfaedc046f15acb3894f1814de2fc36090 Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Wed, 18 Jun 2025 15:04:56 -0700 Subject: [PATCH 02/14] build(script): add stub docs script --- script/docs | 1 + 1 file changed, 1 insertion(+) create mode 100755 script/docs diff --git a/script/docs b/script/docs new file mode 100755 index 0000000..34d0eaa --- /dev/null +++ b/script/docs @@ -0,0 +1 @@ +echo "::notice::🚀 No documentation present" From ac4271068d07fea0fbc2eac863172348d6d96b77 Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Wed, 18 Jun 2025 18:06:59 -0700 Subject: [PATCH 03/14] refactor(pyconfig): simplify entry points handling to use only importlib.metadata --- pyconfig/__init__.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pyconfig/__init__.py b/pyconfig/__init__.py index fb2e67d..74f45da 100644 --- a/pyconfig/__init__.py +++ b/pyconfig/__init__.py @@ -12,21 +12,17 @@ import sys import threading -try: +import pytool +from pytool.lang import Namespace - def iter_entry_points(group, **kwargs): - from importlib.metadata import entry_points - for entry_point in entry_points(group=group, **kwargs): - yield entry_point -except ImportError: - try: - from pkg_resources import iter_entry_points - except ImportError: - raise ImportError("No module named 'importlib.metadata' or 'pkg_resources'") +def iter_entry_points(group, **kwargs): + """Iterate over entry points for the given group using importlib.metadata.""" + from importlib.metadata import entry_points + + for entry_point in entry_points(group=group, **kwargs): + yield entry_point -import pytool -from pytool.lang import Namespace __version__ = "3.2.3" From 9fa4b9be118b4dfeca9ed36066a7b09f689ae21e Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Wed, 18 Jun 2025 18:07:10 -0700 Subject: [PATCH 04/14] test(config): migrate from nose to pytest and modernize assertions --- test/test_config.py | 177 ++++++++++++++++++++++++-------------------- 1 file changed, 97 insertions(+), 80 deletions(-) diff --git a/test/test_config.py b/test/test_config.py index 456d61e..6e1cdc9 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -1,39 +1,41 @@ """ Tests for pyconfig """ + from __future__ import print_function, unicode_literals + import os +from unittest import mock -import mock +import pytest import pyconfig -from nose.tools import eq_, raises, assert_raises def test_namespace_attr(): ns = pyconfig.Namespace() ns.test = True - eq_(ns.test, True) + assert ns.test is True def test_namespace_get_config(): ns = pyconfig.Namespace() ns.test = True - eq_(ns.as_dict('ns'), {'ns.test': True}) + assert ns.as_dict("ns") == {"ns.test": True} def test_namespace_nested_attr(): ns = pyconfig.Namespace() ns.nest = pyconfig.Namespace() ns.nest.test = True - eq_(ns.nest.test, True) + assert ns.nest.test is True def test_namespace_nested_get_config(): ns = pyconfig.Namespace() ns.nest = pyconfig.Namespace() ns.nest.test = True - eq_(ns.as_dict('ns'), {'ns.nest.test': True}) + assert ns.as_dict("ns") == {"ns.nest.test": True} def test_namespace_deep_nested(): @@ -43,8 +45,11 @@ def test_namespace_deep_nested(): ns.nest.test = True ns.nest.deep = pyconfig.Namespace() ns.nest.deep.test = True - eq_(ns.as_dict('ns'), {'ns.nest.test': True, 'ns.test': True, - 'ns.nest.deep.test': True}) + assert ns.as_dict("ns") == { + "ns.nest.test": True, + "ns.test": True, + "ns.nest.deep.test": True, + } def test_namespace_implicit_nesting(): @@ -52,106 +57,117 @@ def test_namespace_implicit_nesting(): ns.test = True ns.nest.test = True ns.nest.deep.test = True - eq_(ns.as_dict('ns'), {'ns.nest.test': True, 'ns.test': True, - 'ns.nest.deep.test': True}) + assert ns.as_dict("ns") == { + "ns.nest.test": True, + "ns.test": True, + "ns.nest.deep.test": True, + } def test_set_and_get(): - pyconfig.set('set_and_get', 'tested') - eq_(pyconfig.get('set_and_get'), 'tested') + pyconfig.set("set_and_get", "tested") + assert pyconfig.get("set_and_get") == "tested" def test_allow_default(): - eq_(pyconfig.get('test_allow_default1'), None) - eq_(pyconfig.get('test_allow_default2', default=None), None) - eq_(pyconfig.get('test_allow_default3', 'default_value', allow_default=True), - 'default_value') + assert pyconfig.get("test_allow_default1") is None + assert pyconfig.get("test_allow_default2", default=None) is None + assert ( + pyconfig.get("test_allow_default3", "default_value", allow_default=True) + == "default_value" + ) -@raises(LookupError) def test_get_no_default(): - pyconfig.get('get_no_default1', allow_default=False) + with pytest.raises(LookupError): + pyconfig.get("get_no_default1", allow_default=False) -@raises(LookupError) def test_config_get_no_default(): - pyconfig.Config().get('get_no_default2', None, allow_default=False) + with pytest.raises(LookupError): + pyconfig.Config().get("get_no_default2", None, allow_default=False) def test_set_get_change(): - pyconfig.set('set_get_change', 'testing') - eq_(pyconfig.get('set_get_change'), 'testing') - pyconfig.set('set_get_change', 'tested') - eq_(pyconfig.get('set_get_change'), 'tested') + pyconfig.set("set_get_change", "testing") + assert pyconfig.get("set_get_change") == "testing" + pyconfig.set("set_get_change", "tested") + assert pyconfig.get("set_get_change") == "tested" def test_setting(): class Test(object): - setting = pyconfig.Setting('test', 'value') - eq_(Test.setting, 'value') - eq_(Test().setting, 'value') + setting = pyconfig.Setting("test", "value") + + assert Test.setting == "value" + assert Test().setting == "value" def test_setting_change(): class Test(object): - setting = pyconfig.Setting('test_setting_change', 'value') - eq_(Test.setting, 'value') - eq_(Test().setting, 'value') - pyconfig.set('test_setting_change', 'value2') - eq_(Test.setting, 'value2') - eq_(Test().setting, 'value2') + setting = pyconfig.Setting("test_setting_change", "value") + + assert Test.setting == "value" + assert Test().setting == "value" + pyconfig.set("test_setting_change", "value2") + assert Test.setting == "value2" + assert Test().setting == "value2" def test_setting_no_default(): class Test(object): - setting_no_default = pyconfig.Setting('test_setting_no_default', - allow_default=False) + setting_no_default = pyconfig.Setting( + "test_setting_no_default", allow_default=False + ) - # lambda because assert_raises needs a callable - assert_raises(LookupError, lambda: Test.setting_no_default) - pyconfig.set('test_setting_no_default', 'new_value') - eq_(Test.setting_no_default, 'new_value') + with pytest.raises(LookupError): + Test.setting_no_default + pyconfig.set("test_setting_no_default", "new_value") + assert Test.setting_no_default == "new_value" def test_config_update(): conf = pyconfig.Config() conf.settings = {} - conf._update({'test_config_update': 'test_value'}, 'ns') - eq_(conf.settings, {'ns.test_config_update': 'test_value'}) - eq_(conf.get('ns.test_config_update', None), 'test_value') + conf._update({"test_config_update": "test_value"}, "ns") + assert conf.settings == {"ns.test_config_update": "test_value"} + assert conf.get("ns.test_config_update", None) == "test_value" def test_config_update_sans_private(): conf = pyconfig.Config() conf.settings = {} - conf._update({'_test_private': 'private', 'other': 'nonprivate'}, 'ns') - eq_(conf.settings, {'ns.other': 'nonprivate'}) - eq_(conf.get('ns.other', None), 'nonprivate') - eq_(conf.get('ns._test_private', None), None) + conf._update({"_test_private": "private", "other": "nonprivate"}, "ns") + assert conf.settings == {"ns.other": "nonprivate"} + assert conf.get("ns.other", None) == "nonprivate" + assert conf.get("ns._test_private", None) is None def test_config_update_skip_namespace_class(): conf = pyconfig.Config() conf.settings = {} - conf._update({'Namespace': pyconfig.Namespace}) - eq_(conf.settings, {}) + conf._update({"Namespace": pyconfig.Namespace}) + assert conf.settings == {} def test_config_update_nested_namespace(): conf = pyconfig.Config() conf.settings = {} ns = pyconfig.Namespace() - ns.value = 'value' - conf._update({'test': ns}, 'ns') - eq_(conf.get('ns.test.value', None), 'value') + ns.value = "value" + conf._update({"test": ns}, "ns") + assert conf.get("ns.test.value", None) == "value" def test_config_update_callable(): conf = pyconfig.Config() conf.settings = {} - call = lambda: 'value' - conf._update({'test_callable': call}, 'ns') - eq_(conf.get('ns.test_callable', None), 'value') + + def call(): + return "value" + + conf._update({"test_callable": call}, "ns") + assert conf.get("ns.test_callable", None) == "value" def test_reload_hook(): @@ -163,49 +179,50 @@ def test_reload_hook(): def test_setting_shortcut(): class Test(object): - setting = pyconfig.setting('test_setting_shortcut', 'tested') - setting_no_default = pyconfig.setting('setting_shortcut_no_default', - allow_default=False) - eq_(Test.setting, 'tested') - eq_(Test().setting, 'tested') - assert_raises(LookupError, lambda: Test.setting_no_default, ) + setting = pyconfig.setting("test_setting_shortcut", "tested") + setting_no_default = pyconfig.setting( + "setting_shortcut_no_default", allow_default=False + ) + assert Test.setting == "tested" + assert Test().setting == "tested" + with pytest.raises(LookupError): + Test.setting_no_default def test_get_default_with_various_values(): - eq_(pyconfig.get('default_num', 1), 1) - eq_(pyconfig.get('default_num', 1.0), 1.0) - eq_(pyconfig.get('default_none', None), None) - eq_(pyconfig.get('default_true', True), True) - eq_(pyconfig.get('default_false', False), False) - eq_(pyconfig.get('default_unicode', 'Test'), 'Test') - eq_(pyconfig.get('default_expr', 60*24), 60*24) - eq_(pyconfig.get('ns.test_namespace', 'pyconfig'), 'pyconfig') + assert pyconfig.get("default_num", 1) == 1 + assert pyconfig.get("default_num", 1.0) == 1.0 + assert pyconfig.get("default_none", None) is None + assert pyconfig.get("default_true", True) is True + assert pyconfig.get("default_false", False) is False + assert pyconfig.get("default_unicode", "Test") == "Test" + assert pyconfig.get("default_expr", 60 * 24) == 60 * 24 + assert pyconfig.get("ns.test_namespace", "pyconfig") == "pyconfig" def test_localconfig_py_actually_works(): - eq_(pyconfig.get('conf.local', False), True) + assert pyconfig.get("conf.local", False) is True def test_case_insensitivity(): - pyconfig.set('SomeSetting', True) - eq_(pyconfig.get('SomeSetting'), True) - eq_(pyconfig.get('somesetting'), True) + pyconfig.set("SomeSetting", True) + assert pyconfig.get("SomeSetting") is True + assert pyconfig.get("somesetting") is True def test_case_sensitive(): - pyconfig.set('pyconfig.case_sensitive', True) - pyconfig.set('CaseSensitive', True) - eq_(pyconfig.get('CaseSensitive'), True) - eq_(pyconfig.get('casesensitive'), None) + pyconfig.set("pyconfig.case_sensitive", True) + pyconfig.set("CaseSensitive", True) + assert pyconfig.get("CaseSensitive") is True + assert pyconfig.get("casesensitive") is None pyconfig.reload(clear=True) def test_env_key_should_return_default(): - eq_(pyconfig.env_key('testing.key', 1), 1) + assert pyconfig.env_key("testing.key", 1) == 1 def test_env_key_should_return_from_environ(): - os.environ['TESTING_KEY'] = "true" - eq_(pyconfig.env_key('testing.key', 1), "true") - + os.environ["TESTING_KEY"] = "true" + assert pyconfig.env_key("testing.key", 1) == "true" From 6213913ca0f38c4b2a95e52f513cc57e89aeaf33 Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Wed, 18 Jun 2025 18:07:35 -0700 Subject: [PATCH 05/14] chore(test): remove unused imports --- test/test_etcd.py | 92 ++++++++++++++++++++++++----------------------- test2/test2.py | 5 +-- 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/test/test_etcd.py b/test/test_etcd.py index 1b8e974..7e60358 100644 --- a/test/test_etcd.py +++ b/test/test_etcd.py @@ -3,7 +3,7 @@ import pytool from nose import SkipTest -from nose.tools import ok_, eq_ +from nose.tools import eq_ import pyconfig @@ -15,16 +15,18 @@ def setup(): if not pyconfig.etcd().configured: raise SkipTest("etcd not configured") - pyconfig.set('pyconfig.etcd.prefix', '/pyconfig_test/test/') + pyconfig.set("pyconfig.etcd.prefix", "/pyconfig_test/test/") client = pyconfig.etcd().client - client.set('pyconfig_test/test/pyconfig.number', pytool.json.as_json(1)) - client.set('pyconfig_test/test/pyconfig.boolean', pytool.json.as_json(True)) - client.set('pyconfig_test/test/pyconfig.string', pytool.json.as_json("Value")) - client.set('pyconfig_test/test/pyconfig.json', pytool.json.as_json({"a": "b"})) - client.set('pyconfig_test/test2/pyconfig.number', pytool.json.as_json(2)) - client.set('pyconfig_test/test2/config.inherit', - pytool.json.as_json('/pyconfig_test/test/')) + client.set("pyconfig_test/test/pyconfig.number", pytool.json.as_json(1)) + client.set("pyconfig_test/test/pyconfig.boolean", pytool.json.as_json(True)) + client.set("pyconfig_test/test/pyconfig.string", pytool.json.as_json("Value")) + client.set("pyconfig_test/test/pyconfig.json", pytool.json.as_json({"a": "b"})) + client.set("pyconfig_test/test2/pyconfig.number", pytool.json.as_json(2)) + client.set( + "pyconfig_test/test2/config.inherit", + pytool.json.as_json("/pyconfig_test/test/"), + ) def teardown(): @@ -32,101 +34,103 @@ def teardown(): return # Clean up the test namespace - pyconfig.etcd().client.delete('pyconfig_test/test', dir=True, recursive=True) - pyconfig.etcd().client.delete('pyconfig_test/test2', dir=True, recursive=True) - pyconfig.etcd().client.delete('pyconfig_test/watching', dir=True, recursive=True) - pyconfig.etcd().client.delete('pyconfig_test/', dir=True, recursive=True) + pyconfig.etcd().client.delete("pyconfig_test/test", dir=True, recursive=True) + pyconfig.etcd().client.delete("pyconfig_test/test2", dir=True, recursive=True) + pyconfig.etcd().client.delete("pyconfig_test/watching", dir=True, recursive=True) + pyconfig.etcd().client.delete("pyconfig_test/", dir=True, recursive=True) def test_using_correct_prefix(): - eq_(pyconfig.etcd().prefix, '/pyconfig_test/test/') + eq_(pyconfig.etcd().prefix, "/pyconfig_test/test/") def test_parse_hosts_single_host(): - host = pyconfig.etcd()._parse_hosts('127.0.0.1:2379') - eq_(host, (('127.0.0.1', 2379),)) + host = pyconfig.etcd()._parse_hosts("127.0.0.1:2379") + eq_(host, (("127.0.0.1", 2379),)) def test_parse_hosts_multiple_hosts(): - hosts = '10.0.0.1:2379,10.0.0.2:2379,10.0.0.3:2379' + hosts = "10.0.0.1:2379,10.0.0.2:2379,10.0.0.3:2379" hosts = pyconfig.etcd()._parse_hosts(hosts) - eq_(hosts, (('10.0.0.1', 2379), ('10.0.0.2', 2379), ('10.0.0.3', 2379))) + eq_(hosts, (("10.0.0.1", 2379), ("10.0.0.2", 2379), ("10.0.0.3", 2379))) def test_load_works(): conf = pyconfig.etcd().load() - eq_(conf.get('pyconfig.json'), {"a": "b"}) - eq_(conf.get('pyconfig.string'), 'Value') - eq_(conf.get('pyconfig.boolean'), True) - eq_(conf.get('pyconfig.number'), 1) + eq_(conf.get("pyconfig.json"), {"a": "b"}) + eq_(conf.get("pyconfig.string"), "Value") + eq_(conf.get("pyconfig.boolean"), True) + eq_(conf.get("pyconfig.number"), 1) def test_changing_prefix_works(): - pyconfig.etcd(prefix='pyconfig/other') - eq_(pyconfig.etcd().prefix, '/pyconfig/other/') + pyconfig.etcd(prefix="pyconfig/other") + eq_(pyconfig.etcd().prefix, "/pyconfig/other/") conf = pyconfig.etcd().load() eq_(conf, {}) - pyconfig.set('pyconfig.etcd.prefix', 'pyconfig_test/test') - eq_(pyconfig.etcd().prefix, '/pyconfig_test/test/') + pyconfig.set("pyconfig.etcd.prefix", "pyconfig_test/test") + eq_(pyconfig.etcd().prefix, "/pyconfig_test/test/") def test_inheritance_works(): - pyconfig.set('pyconfig.etcd.prefix', 'pyconfig_test/test2') + pyconfig.set("pyconfig.etcd.prefix", "pyconfig_test/test2") conf = pyconfig.etcd().load() - eq_(conf.get('pyconfig.json'), {"a": "b"}) - eq_(conf.get('pyconfig.string'), 'Value') - eq_(conf.get('pyconfig.boolean'), True) - eq_(conf.get('pyconfig.number'), 2) - eq_(conf.get('config.inherit'), '/pyconfig_test/test/') - pyconfig.set('pyconfig.etcd.prefix', 'pyconfig_test/test') + eq_(conf.get("pyconfig.json"), {"a": "b"}) + eq_(conf.get("pyconfig.string"), "Value") + eq_(conf.get("pyconfig.boolean"), True) + eq_(conf.get("pyconfig.number"), 2) + eq_(conf.get("config.inherit"), "/pyconfig_test/test/") + pyconfig.set("pyconfig.etcd.prefix", "pyconfig_test/test") def test_reload_work_with_inheritance(): - pyconfig.set('pyconfig.etcd.prefix', 'pyconfig_test/test2') + pyconfig.set("pyconfig.etcd.prefix", "pyconfig_test/test2") pyconfig.reload() def test_autoloading_etcd_config_works(): pyconfig.Config().clear() - pyconfig.set('pyconfig.etcd.prefix', 'pyconfig_test/test2') + pyconfig.set("pyconfig.etcd.prefix", "pyconfig_test/test2") pyconfig.reload() - eq_(pyconfig.get('pyconfig.string'), 'Value') - eq_(pyconfig.get('pyconfig.number'), 2) + eq_(pyconfig.get("pyconfig.string"), "Value") + eq_(pyconfig.get("pyconfig.number"), 2) def test_watching(): # Enable watching - os.environ['PYCONFIG_ETCD_WATCH'] = 'true' + os.environ["PYCONFIG_ETCD_WATCH"] = "true" pyconfig.Config().clear() - pyconfig.set('pyconfig.etcd.prefix', 'pyconfig_test/watching') + pyconfig.set("pyconfig.etcd.prefix", "pyconfig_test/watching") pyconfig.reload() # Wait for 20ms before writing to ensure the watcher thread is ready time.sleep(0.020) # Write a new value directly to etcd - pyconfig.etcd().client.write('pyconfig_test/watching/it.works', - pytool.json.as_json(True)) + pyconfig.etcd().client.write( + "pyconfig_test/watching/it.works", pytool.json.as_json(True) + ) # Try to get the value... this is a bit sloppy but good luck doing # something better retry = 50 while retry: retry -= 1 - if pyconfig.get('it.works', None) is not None: + if pyconfig.get("it.works", None) is not None: break # Wait 20ms more for it to show up time.sleep(0.020) - eq_(pyconfig.get('it.works', False), True) + eq_(pyconfig.get("it.works", False), True) + # TODO: # - Add tests for protocol environment variable def test_protocol_is_picked_up_and_used(): raise SkipTest("TODO") + # - Add tests for auth environment variable def test_auth_is_picked_up_and_used(): raise SkipTest("TODO") - diff --git a/test2/test2.py b/test2/test2.py index afac2a0..1aba7f0 100644 --- a/test2/test2.py +++ b/test2/test2.py @@ -1,5 +1,2 @@ -from pyconfig import Namespace, deferred - value = 2 -name = 'test2' - +name = "test2" From 01e6b996c6ad90b07783de4f49acb984137d72eb Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Wed, 18 Jun 2025 18:09:26 -0700 Subject: [PATCH 06/14] build(script): update docs to have shebang, oops --- script/docs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/docs b/script/docs index 34d0eaa..0684856 100755 --- a/script/docs +++ b/script/docs @@ -1 +1,3 @@ +#!/usr/bin/env bash + echo "::notice::🚀 No documentation present" From 507568a109935a2ab5daaaa1599004c8b25cd76e Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Wed, 18 Jun 2025 18:17:30 -0700 Subject: [PATCH 07/14] test(etcd): refactor to use pytest --- test/test_etcd.py | 99 ++++++++++++++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 35 deletions(-) diff --git a/test/test_etcd.py b/test/test_etcd.py index 7e60358..d1e0f8d 100644 --- a/test/test_etcd.py +++ b/test/test_etcd.py @@ -1,19 +1,20 @@ import os import time +import pytest import pytool -from nose import SkipTest -from nose.tools import eq_ import pyconfig -def setup(): +@pytest.fixture(scope="module", autouse=True) +def etcd_setup(): + """Setup etcd test data and clean up after tests.""" if not pyconfig.etcd().module: - raise SkipTest("etcd not installed") + pytest.skip("etcd not installed") if not pyconfig.etcd().configured: - raise SkipTest("etcd not configured") + pytest.skip("etcd not configured") pyconfig.set("pyconfig.etcd.prefix", "/pyconfig_test/test/") @@ -28,74 +29,102 @@ def setup(): pytool.json.as_json("/pyconfig_test/test/"), ) - -def teardown(): - if not pyconfig.etcd().configured: - return + yield # Clean up the test namespace - pyconfig.etcd().client.delete("pyconfig_test/test", dir=True, recursive=True) - pyconfig.etcd().client.delete("pyconfig_test/test2", dir=True, recursive=True) - pyconfig.etcd().client.delete("pyconfig_test/watching", dir=True, recursive=True) - pyconfig.etcd().client.delete("pyconfig_test/", dir=True, recursive=True) - - + if pyconfig.etcd().configured: + pyconfig.etcd().client.delete("pyconfig_test/test", dir=True, recursive=True) + pyconfig.etcd().client.delete("pyconfig_test/test2", dir=True, recursive=True) + pyconfig.etcd().client.delete( + "pyconfig_test/watching", dir=True, recursive=True + ) + pyconfig.etcd().client.delete("pyconfig_test/", dir=True, recursive=True) + + +@pytest.mark.skipif( + not pyconfig.etcd().module or not pyconfig.etcd().configured, + reason="etcd not installed or configured", +) def test_using_correct_prefix(): - eq_(pyconfig.etcd().prefix, "/pyconfig_test/test/") + assert pyconfig.etcd().prefix == "/pyconfig_test/test/" def test_parse_hosts_single_host(): host = pyconfig.etcd()._parse_hosts("127.0.0.1:2379") - eq_(host, (("127.0.0.1", 2379),)) + assert host == (("127.0.0.1", 2379),) def test_parse_hosts_multiple_hosts(): hosts = "10.0.0.1:2379,10.0.0.2:2379,10.0.0.3:2379" hosts = pyconfig.etcd()._parse_hosts(hosts) - eq_(hosts, (("10.0.0.1", 2379), ("10.0.0.2", 2379), ("10.0.0.3", 2379))) + assert hosts == (("10.0.0.1", 2379), ("10.0.0.2", 2379), ("10.0.0.3", 2379)) +@pytest.mark.skipif( + not pyconfig.etcd().module or not pyconfig.etcd().configured, + reason="etcd not installed or configured", +) def test_load_works(): conf = pyconfig.etcd().load() - eq_(conf.get("pyconfig.json"), {"a": "b"}) - eq_(conf.get("pyconfig.string"), "Value") - eq_(conf.get("pyconfig.boolean"), True) - eq_(conf.get("pyconfig.number"), 1) + assert conf.get("pyconfig.json") == {"a": "b"} + assert conf.get("pyconfig.string") == "Value" + assert conf.get("pyconfig.boolean") is True + assert conf.get("pyconfig.number") == 1 +@pytest.mark.skipif( + not pyconfig.etcd().module or not pyconfig.etcd().configured, + reason="etcd not installed or configured", +) def test_changing_prefix_works(): pyconfig.etcd(prefix="pyconfig/other") - eq_(pyconfig.etcd().prefix, "/pyconfig/other/") + assert pyconfig.etcd().prefix == "/pyconfig/other/" conf = pyconfig.etcd().load() - eq_(conf, {}) + assert conf == {} pyconfig.set("pyconfig.etcd.prefix", "pyconfig_test/test") - eq_(pyconfig.etcd().prefix, "/pyconfig_test/test/") + assert pyconfig.etcd().prefix == "/pyconfig_test/test/" +@pytest.mark.skipif( + not pyconfig.etcd().module or not pyconfig.etcd().configured, + reason="etcd not installed or configured", +) def test_inheritance_works(): pyconfig.set("pyconfig.etcd.prefix", "pyconfig_test/test2") conf = pyconfig.etcd().load() - eq_(conf.get("pyconfig.json"), {"a": "b"}) - eq_(conf.get("pyconfig.string"), "Value") - eq_(conf.get("pyconfig.boolean"), True) - eq_(conf.get("pyconfig.number"), 2) - eq_(conf.get("config.inherit"), "/pyconfig_test/test/") + assert conf.get("pyconfig.json") == {"a": "b"} + assert conf.get("pyconfig.string") == "Value" + assert conf.get("pyconfig.boolean") is True + assert conf.get("pyconfig.number") == 2 + assert conf.get("config.inherit") == "/pyconfig_test/test/" pyconfig.set("pyconfig.etcd.prefix", "pyconfig_test/test") +@pytest.mark.skipif( + not pyconfig.etcd().module or not pyconfig.etcd().configured, + reason="etcd not installed or configured", +) def test_reload_work_with_inheritance(): pyconfig.set("pyconfig.etcd.prefix", "pyconfig_test/test2") pyconfig.reload() +@pytest.mark.skipif( + not pyconfig.etcd().module or not pyconfig.etcd().configured, + reason="etcd not installed or configured", +) def test_autoloading_etcd_config_works(): pyconfig.Config().clear() pyconfig.set("pyconfig.etcd.prefix", "pyconfig_test/test2") pyconfig.reload() - eq_(pyconfig.get("pyconfig.string"), "Value") - eq_(pyconfig.get("pyconfig.number"), 2) + assert pyconfig.get("pyconfig.string") == "Value" + assert pyconfig.get("pyconfig.number") == 2 +@pytest.mark.skipif( + not pyconfig.etcd().module or not pyconfig.etcd().configured, + reason="etcd not installed or configured", +) def test_watching(): # Enable watching os.environ["PYCONFIG_ETCD_WATCH"] = "true" @@ -122,15 +151,15 @@ def test_watching(): # Wait 20ms more for it to show up time.sleep(0.020) - eq_(pyconfig.get("it.works", False), True) + assert pyconfig.get("it.works", False) is True # TODO: # - Add tests for protocol environment variable def test_protocol_is_picked_up_and_used(): - raise SkipTest("TODO") + pytest.skip("TODO") # - Add tests for auth environment variable def test_auth_is_picked_up_and_used(): - raise SkipTest("TODO") + pytest.skip("TODO") From 5731685ff0d9a16ce70c5293aaf9f77b2ed6f43b Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Thu, 19 Jun 2025 21:15:43 -0700 Subject: [PATCH 08/14] chore: remove legacy Travis CI configuration and RST documentation --- .travis.yml | 81 --------- README.rst | 490 ---------------------------------------------------- 2 files changed, 571 deletions(-) delete mode 100644 .travis.yml delete mode 100644 README.rst diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6187e21..0000000 --- a/.travis.yml +++ /dev/null @@ -1,81 +0,0 @@ -language: python -sudo: false - -python: - - 2.7 - - 3.8 - - 3.9 - - 3.10 - - 3.11 - - 3.12 - - -env: - - > - INSTALL_ETCD=2.1.1 - EXTRA_ETCD="[etcd]" - _SSL="`pwd`/test/ssl" - ETCD_TRUSTED_CA_FILE="${_SSL}/cacert.pem" - ETCD_CERT_FILE="${_SSL}/server.crt" - ETCD_KEY_FILE="${_SSL}/server.key" - ETCD_ADVERTISE_CLIENT_URLS="https://127.0.0.1:2379" - ETCD_LISTEN_CLIENT_URLS="https://127.0.0.1:2379" - PYCONFIG_ETCD_HOSTS="127.0.0.1:2379" - PYCONFIG_ETCD_CACERT="${_SSL}/cacert.pem" - PYOPENSSL="" - - > - INSTALL_ETCD=2.1.1 - EXTRA_ETCD="[etcd]" - _SSL="`pwd`/test/ssl" - ETCD_CLIENT_CERT_AUTH=1 - ETCD_TRUSTED_CA_FILE="${_SSL}/cacert.pem" - ETCD_CERT_FILE="${_SSL}/server.crt" - ETCD_KEY_FILE="${_SSL}/server.key" - ETCD_ADVERTISE_CLIENT_URLS="https://127.0.0.1:2379" - ETCD_LISTEN_CLIENT_URLS="https://127.0.0.1:2379" - PYCONFIG_ETCD_HOSTS="127.0.0.1:2379" - PYCONFIG_ETCD_CACERT="${_SSL}/cacert.pem" - PYCONFIG_ETCD_CERT="${_SSL}/client.crt" - PYCONFIG_ETCD_KEY="${_SSL}/client.key" - PYOPENSSL="" - - > - INSTALL_ETCD=2.1.1 - EXTRA_ETCD="[etcd]" - _SSL="`pwd`/test/ssl" - ETCD_CLIENT_CERT_AUTH=1 - ETCD_TRUSTED_CA_FILE="${_SSL}/cacert.pem" - ETCD_CERT_FILE="${_SSL}/server.crt" - ETCD_KEY_FILE="${_SSL}/server.key" - ETCD_ADVERTISE_CLIENT_URLS="https://127.0.0.1:2379" - ETCD_LISTEN_CLIENT_URLS="https://127.0.0.1:2379" - PYCONFIG_ETCD_HOSTS="127.0.0.1:2379" - PYCONFIG_ETCD_CACERT="${_SSL}/cacert.pem" - PYCONFIG_ETCD_CERT="${_SSL}/client.crt" - PYCONFIG_ETCD_KEY="${_SSL}/client.key" - PYOPENSSL="pyopenssl" - - > - INSTALL_ETCD=2.1.1 - EXTRA_ETCD="[etcd]" - PYCONFIG_ETCD_HOSTS="127.0.0.1:2379" - PYOPENSSL="" - - > - INSTALL_ETCD=2.1.1 - EXTRA_ETCD="" - PYOPENSSL="" - -install: pip install -e .${EXTRA_ETCD} ${PYOPENSSL} - -script: - - coverage run --source=pyconfig setup.py test - -before_script: - - curl -L https://github.com/coreos/etcd/releases/download/v${INSTALL_ETCD}/etcd-v${INSTALL_ETCD}-linux-amd64.tar.gz -o etcd-v${INSTALL_ETCD}-linux-amd64.tar.gz - - tar xzvf etcd-v${INSTALL_ETCD}-linux-amd64.tar.gz - - cd etcd-v${INSTALL_ETCD}-linux-amd64 - - ./etcd --version - - ./etcd & - - cd - - - pip install nose mock coverage coveralls - -after_success: - - coveralls diff --git a/README.rst b/README.rst deleted file mode 100644 index ac5ec26..0000000 --- a/README.rst +++ /dev/null @@ -1,490 +0,0 @@ -pyconfig - Python-based singleton configuration -=============================================== - -.. image:: https://travis-ci.org/shakefu/pyconfig.svg?branch=master - :target: https://travis-ci.org/shakefu/pyconfig - -This module provides python based configuration that is stored in a singleton -object to ensure consistency across your project. - -.. contents:: - :depth: 2 - :local: - -Command Line ------------- - -Pyconfig has a command line utility that lets you inspect your project to find -all the configuration keys defined. - -:: - - $ pyconfig -h - usage: pyconfig [-h] [-f F | -m M] [-v] [-l] [-a | -k] [-n] [-s] [-c] - - Helper for working with pyconfigs - - optional arguments: - -h, --help show this help message and exit - -f F, --filename F parse an individual file or directory - -m M, --module M parse a package or module, recursively looking inside it - -v, --view-call show the actual pyconfig call made (default: show namespace) - -l, --load-configs query the currently set value for each key found - -a, --all show keys which don't have defaults set - -k, --only-keys show a list of discovered keys without values - -n, --natural-sort sort by filename and line (default: alphabetical by key) - -s, --source show source annotations (implies --natural-sort) - -c, --color toggle output colors (default: True) - -**Example output** - -.. code-block:: bash - - $ pyconfig --file . - humbledb.allow_explicit_request = True - humbledb.auto_start_request = True - humbledb.connection_pool = 300 - humbledb.tz_aware = True - humbledb.use_greenlets = False - humbledb.write_concern = 1 - - $ pyconfig --view-call --file . - pyconfig.get('humbledb.allow_explicit_request', True) - pyconfig.setting('humbledb.auto_start_request', True) - pyconfig.setting('humbledb.connection_pool', 300) - pyconfig.setting('humbledb.tz_aware', True) - pyconfig.setting('humbledb.use_greenlets', False) - pyconfig.setting('humbledb.write_concern', 1) - - $ pyconfig --source --file . - # ./humbledb/mongo.py, line 98 - humbledb.allow_explicit_request = True - # ./humbledb/mongo.py, line 178 - humbledb.connection_pool = 300 - # ./humbledb/mongo.py, line 181 - humbledb.auto_start_request = True - # ./humbledb/mongo.py, line 185 - humbledb.use_greenlets = False - # ./humbledb/mongo.py, line 188 - humbledb.tz_aware = True - # ./humbledb/mongo.py, line 191 - humbledb.write_concern = 1 - - -Etcd ----- - -*Version added: 3.0.0* - -Pyconfig has read-only support for configurations stored in etcd. The preferred -method for configuring Pyconfig to work with etcd is via ENV variables, since -they must be set as early as possible. It is also possible to use the Python -API to make Pyconfig work with etcd. - -Pyconfig uses a directory namespace to store its dot notation configuration key -names. By default, that namespace is ``/config/``. - -At a minimum, ``PYCONFIG_ETCD_HOSTS`` must be set to get Pyconfig to try to -read a configuration from etcd using the default settings. - -You can set a value with `etcdctl` like: - -.. code-block:: bash - - $ # The etcdctl command is provided by etcd and not part of pyconfig - $ etcdctl set /pyconfig/example/my.setting "from etcd" - -And configure Pyconfig to connect and use that setting: - -.. code-block:: bash - - $ export PYCONFIG_ETCD_PREFIX="/pyconfig/example/" - $ export PYCONFIG_ETCD_HOSTS="127.0.0.1:2379" - $ python - >>> import pyconfig - >>> pyconfig.get('my.setting') - 'from etcd' - -Because of Pyconfig's singleton nature, only one configuration can be accessed -at a time in this way. - -**Environment variables:** - -* ``PYCONFIG_ETCD_PREFIX`` - The namespace to prefix settings with (default: - ``'/config/'``) -* ``PYCONFIG_ETCD_HOSTS`` - A comma separated list of hosts, like - ``10.0.0.1:2379,10.0.0.2:2379`` -* ``PYCONFIG_ETCD_CACERT`` - CA cert file to use for SSL -* ``PYCONFIG_ETCD_CERT`` - Client cert file to use for SSL client authentication -* ``PYCONFIG_ETCD_KEY`` - Client private key file to use for SSL client auth -* ``PYCONFIG_ETCD_WATCH`` - If this is set to a truthy value (a non-empty - string), then pyconfig will keep the local configuration synchronized with - etcd (*Version added: 3.1.0*) -* ``PYCONFIG_ETCD_PROTOCOL`` - Set this to force HTTPS connections even if not - using certificates. This should be a string of the form `https` or `http`. - (*Version added: 3.2.0*) -* ``PYCONFIG_ETCD_AUTH`` - Set this use Basic Authentication with requests. - This should be a string of the format `username:password`. (*Version added: - 3.2.0*) - -**Inheritance:** - -If you want to create a configuration that inherits from an existing -configuration, Pyconfig will look for a special key, which by default is set to -``config.inherit``. If this exists and is set to an etcd namespace, that -configuration will be used as the base for the current config. - -A typical use case would be a Test environment configuration which is derived -from a Development config. Below is a barebones example of how that might be -set up using `etcdctl` and Pyconfig. - -.. code-block:: bash - - $ # Create the development settings - $ etcdctl set /config/app/dev/my.name example - $ etcdctl set /config/app/dev/my.hostname localhost - $ etcdctl set /config/app/dev/my.api.key abcdef0123456789 - $ # Create the test settings - $ etcdctl set /config/app/test/my.hostname test.example.com - $ # Tell it to inherit from the development settings - $ etcdctl set /config/app/test/config.inherit /config/app/dev/ - $ # Configure Pyconfig to use the test configuration - $ export PYCONFIG_ETCD_PREFIX="/config/app/test/" - $ export PYCONFIG_ETCD_HOSTS="127.0.0.1:2379" - $ python - >>> import pyconfig - >>> pyconfig.get('my.hostname') - 'test.example.com' - >>> pyconfig.get('my.name') - 'example' - - -Code Examples -------------- - -The most basic usage allows you to get, retrieve and modify values. Pyconfig's -singleton provides convenient accessor methods for these actions: - -*Version changed: 3.0.0* - -As of version 3.0.0, keys are not case sensitive by default. - -.. code-block:: python - - >>> import pyconfig - >>> pyconfig.get('my.setting', 'default') - 'default' - >>> pyconfig.set('my.setting', 'new') - >>> pyconfig.get('my.setting', 'default') - 'new' - >>> pyconfig.reload(clear=True) - >>> pyconfig.get('my.setting', 'default') - 'default' - -You can also opt-out of default values: - -.. code-block:: python - - >>> import pyconfig - >>> pyconfig.get('my.setting', allow_default=False) - Traceback (most recent call last): - File "", line 1, in - File "pyconfig/__init__.py", line 275, in get - return Config().get(name, default, allow_default=allow_default) - File "pyconfig/__init__.py", line 234, in get - return self.settings[name] - LookupError: No setting "my.setting" - -Pyconfig also provides shortcuts for giving classes property descriptors which -map to the current setting stored in the singleton: - -.. code-block:: python - - >>> import pyconfig - >>> class MyClass(object): - ... my_setting = pyconfig.setting('my.setting', 'default') - ... - >>> MyClass.my_setting - 'default' - >>> MyClass().my_setting - 'default' - >>> pyconfig.set('my.setting', "Hello World!") - >>> MyClass.my_setting - 'Hello World!' - >>> MyClass().my_setting - 'Hello World!' - >>> pyconfig.reload(clear=True) - >>> MyClass.my_setting - 'default' - -The `Setting` class also supports preventing default values. When set this way, -all reads on the attribute will prevent the use of defaults: - -.. code-block:: python - - >>> import pyconfig - >>> class MyClass(object): - ... my_setting = pyconfig.setting('my.setting', allow_default=False) - ... - >>> MyClass.my_setting - Traceback (most recent call last): - File "", line 1, in - File "pyconfig/__init__.py", line 84, in __get__ - allow_default=self.allow_default) - File "pyconfig/__init__.py", line 232, in get - raise LookupError('No setting "{}"'.format(name)) - LookupError: No setting "my.setting" - >>> pyconfig.set('my.setting', 'new_value') - >>> MyClass.my_setting - 'value' - -Pyconfig allows you to override settings via a python configuration file, that -defines its configuration keys as a module namespace. By default, Pyconfig will -look on your ``PYTHONPATH`` for a module named ``localconfig``, and if it exists, it -will use this module namespace to update all configuration settings: - -.. code-block:: python - - # __file__ = "$PYTHONPATH/localconfig.py" - from pyconfig import Namespace - - # Namespace objects allow you to use attribute assignment to create setting - # key names - my = Namespace() - my.setting = 'from_localconfig' - # Namespace objects implicitly return new nested Namespaces when accessing - # attributes that don't exist - my.nested.setting = 'also_from_localconfig' - -With a ``localconfig`` on the ``PYTHONPATH``, it will be loaded before any settings -are read: - -.. code-block:: python - - >>> import pyconfig - >>> pyconfig.get('my.setting') - 'from_localconfig' - >>> pyconfig.get('my.nested.setting') - 'also_from_localconfig' - -Pyconfig also allows you to create distutils plugins that are automatically -loaded. An example ``setup.py``: - -.. code-block:: python - - # __file__ = setup.py - from setuptools import setup - - setup( - name='pytest', - version='0.1.0-dev', - py_modules=['myconfig', 'anyconfig'], - entry_points={ - # The "my" in "my =" indicates a base namespace to use for - # the contained configuration. If you do not wish a base - # namespace, use "any" - 'pyconfig':[ - 'my = myconfig', - 'any = anyconfig', - ], - }, - ) - -An example distutils plugin configuration file: - -.. code-block:: python - - # __file__ = myconfig.py - from pyconfig import Namespace - - def some_callable(): - print "This callable was called." - print "You can execute any arbitrary code." - - setting = 'from_plugin' - nested = Namespace() - nested.setting = 'also_from_plugin' - -Another example configuration file, without a base namespace: - -.. code-block:: python - - # __file__ = anyconfig.py - from pyconfig import Namespace - other = Namespace() - other.setting = 'anyconfig_value' - -Showing the plugin-specified settings: - -.. code-block:: python - - >>> import pyconfig - >>> pyconfig.get('my.setting', 'default') - This callable was called. - You can execute any arbitrary code. - 'from_plugin' - >>> pyconfig.get('my.nested.setting', 'default') - 'also_from_plugin' - >>> pyconfig.get('other.setting', 'default') - 'anyconfig_value' - -More fancy stuff: - -.. code-block:: python - - >>> # Reloading changes re-calls functions... - >>> pyconfig.reload() - This callable was called. - You can execute any arbitrary code. - >>> # This can be used to inject arbitrary code by changing a - >>> # localconfig.py or plugin and reloading a config... especially - >>> # when pyconfig.reload() is attached to a signal - >>> import signal - >>> signal.signal(signal.SIGUSR1, pyconfig.reload) - -Pyconfig provides a ``@reload_hook`` decorator that allows you to register -functions or methods to be called when the configuration is reloaded: - -.. code-block:: python - - >>> import pyconfig - >>> @pyconfig.reload_hook - ... def reload(): - ... print "Do something here." - ... - >>> pyconfig.reload() - Do something here. - -**Warning**: It should not be used to register large numbers of functions (e.g. -registering a bound method in a class's ``__init__`` method), since there is no -way to un-register a hook and it will cause a memory leak, since a bound method -maintains a strong reference to the bound instance. - -**Note**: Because the reload hooks are called without arguments, it will not -work with unbound methods or classmethods. - - -Changes -------- - -This section contains descriptions of changes in each new version. - -3.2.0 -^^^^^ - -* Adds `PYCONFIG_ETCD_PROTOCOL` and `PYCONFIG_ETCD_AUTH`. - - *Released August 17, 2017.* - -3.1.1 -^^^^^ - -* Documentation fixes that makes rendering work on PyPI and GitHub again. - - *Released June 16, 2016.* - -3.1.0 -^^^^^ - -* Adds the ability to watch etcd for changes to values. This can be enabled by - setting the environment variable ``PYCONFIG_ETCD_WATCH=true``. - - *Released June 3, 2016.* - -3.0.2 -^^^^^ - -* Fixes an issue when using Python 3 compatibility in Python 2.7 and PyOpenSSL. - - *Released September 28, 2015.* - -3.0.1 -^^^^^ - -* Changes the default inherit depth to 2, which is more useful than 1. - -3.0.0 -^^^^^ - -* Adds support for loading configurations from etcd, with inheritance. -* Use ``pytool.lang.Namespace`` instead of alternate implementation. -* Drops support for Python 2.6 and 3.2. -* Pyconfig setting keys are now case insensitive by default (Use - ``pyconfig.set('pyconfig.case_sensitive', True)`` to change the behavior) -* Adds new ``clear()`` method for wiping out the cached configuration. - -Older Versions -^^^^^^^^^^^^^^ - -2.2.1 -""""" - -* The command line tool will now attempt to handle source files which specify a - non-ascii encoding gracefully. - -2.2.0 -""""" - -* Add ``allow_default`` keyword option to ``get()`` and ``setting()``. Thanks - to `yarbelk `_! - -2.1.5 -""""" - -* Fix regression where ``localconfig.py`` wasn't being loaded on Python 2.7 due - to a logic flow error. Whoops! - -2.1.4 -""""" - -* Broke Python 2.6 in 2.1.1, fixed again. - -2.1.2-2.1.3 -""""""""""" - -* Package clean up and fixing README to work on PyPI again. - -2.1.1 -""""" - -* Fix bug that would break on Python 2.6 and 2.7 when using a localconfig.py. - -2.1.0 -""""" - -* Pyconfig now works on Python 3, thanks to - `hfalcic `_! - -2.0.0 -""""" -* Pyconfig now has the ability to show you what config keys are defined in a - directory. - -1.2.0 -""""" - -* No longer uses Python 2.7 ``format()``. Should work on 2.6 and maybe earlier. - -1.1.2 -""""" - -* Move version string into ``pyconfig.__version__`` - -1.1.1 -""""" - -* Fix bug with setup.py that prevented installation - -1.1.0 -""""" - -* Allow for implicitly nesting Namespaces when accessing attributes that are - undefined - -Contributors ------------- - -* `shakefu `_ - Creator and maintainer -* `hfalcic `_ - Python 3 compatability -* `yarbelk `_ - ``allow_default`` option - From b7e9b94bdc3630b354b602835705e31eec3efcd5 Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Thu, 19 Jun 2025 21:15:53 -0700 Subject: [PATCH 09/14] chore(test): clean up trailing whitespace in SSL certificates --- test/ssl/client.crt | 10 +++++----- test/ssl/server.crt | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/ssl/client.crt b/test/ssl/client.crt index 1e57538..a3708f5 100644 --- a/test/ssl/client.crt +++ b/test/ssl/client.crt @@ -23,15 +23,15 @@ Certificate: df:a8:ab:8d:75:b6:25:ea:8d Exponent: 65537 (0x10001) X509v3 extensions: - X509v3 Basic Constraints: + X509v3 Basic Constraints: CA:FALSE - Netscape Cert Type: + Netscape Cert Type: SSL Client - X509v3 Key Usage: + X509v3 Key Usage: Digital Signature, Key Encipherment - X509v3 Extended Key Usage: + X509v3 Extended Key Usage: TLS Web Client Authentication - Netscape Comment: + Netscape Comment: OpenSSL Certificate for SSL Client Signature Algorithm: sha1WithRSAEncryption 42:a0:2a:1a:5d:67:db:8b:a1:d6:94:c1:77:ec:c9:18:00:a4: diff --git a/test/ssl/server.crt b/test/ssl/server.crt index 8ddd9d1..2f64527 100644 --- a/test/ssl/server.crt +++ b/test/ssl/server.crt @@ -23,15 +23,15 @@ Certificate: 18:72:34:89:36:c2:44:74:9d Exponent: 65537 (0x10001) X509v3 extensions: - X509v3 Basic Constraints: + X509v3 Basic Constraints: CA:FALSE - Netscape Cert Type: + Netscape Cert Type: SSL Server - X509v3 Key Usage: + X509v3 Key Usage: Digital Signature, Key Encipherment - X509v3 Subject Alternative Name: + X509v3 Subject Alternative Name: IP Address:127.0.0.1 - Netscape Comment: + Netscape Comment: OpenSSL Certificate for SSL Server Signature Algorithm: sha1WithRSAEncryption 7d:6a:21:00:19:e8:6d:2b:c8:60:e9:23:2b:49:89:89:9d:4d: From 3a71522f1f921becda93bf1bf2843cdc3c42414c Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Thu, 19 Jun 2025 21:18:43 -0700 Subject: [PATCH 10/14] chore(test): modernize test packages to use pyproject.toml instead of setup.py --- test1/pyproject.toml | 20 ++++++++++++++++++++ test1/setup.py | 10 ---------- test1/test1.py | 2 +- test2/pyproject.toml | 20 ++++++++++++++++++++ test2/setup.py | 10 ---------- 5 files changed, 41 insertions(+), 21 deletions(-) create mode 100644 test1/pyproject.toml delete mode 100644 test1/setup.py create mode 100644 test2/pyproject.toml delete mode 100644 test2/setup.py diff --git a/test1/pyproject.toml b/test1/pyproject.toml new file mode 100644 index 0000000..d7f6683 --- /dev/null +++ b/test1/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "test1" +version = "1.0.0" +description = "Test package for pyconfig" +readme = "README.md" +license = "MIT" +authors = [ + {name = "Jacob Alheid", email = "shakefu@gmail.com"}, +] +requires-python = ">=3.8" +dependencies = [ + "pyconfig", +] + +[project.entry-points.pyconfig] +any = "test1" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/test1/setup.py b/test1/setup.py deleted file mode 100644 index 7288107..0000000 --- a/test1/setup.py +++ /dev/null @@ -1,10 +0,0 @@ -from setuptools import setup - -setup( - name='test1', - version='1.0.0', - py_modules=['test1'], - entry_points={ - 'pyconfig':['any = test1'], - } - ) diff --git a/test1/test1.py b/test1/test1.py index 73cafe5..7b8b23c 100644 --- a/test1/test1.py +++ b/test1/test1.py @@ -2,4 +2,4 @@ test = Namespace() test.value = 1 -test.name = 'test1' +test.name = "test1" diff --git a/test2/pyproject.toml b/test2/pyproject.toml new file mode 100644 index 0000000..d72f7c0 --- /dev/null +++ b/test2/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "test2" +version = "1.0.0" +description = "Test package for pyconfig" +readme = "README.md" +license = "MIT" +authors = [ + {name = "Jacob Alheid", email = "shakefu@gmail.com"}, +] +requires-python = ">=3.9" +dependencies = [ + "pyconfig", +] + +[project.entry-points.pyconfig] +any = "test2" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/test2/setup.py b/test2/setup.py deleted file mode 100644 index c6cd28e..0000000 --- a/test2/setup.py +++ /dev/null @@ -1,10 +0,0 @@ -from setuptools import setup - -setup( - name='test2', - version='1.0.0', - py_modules=['test2'], - entry_points={ - 'pyconfig':['any = test2'], - } - ) From 93e02dbac0f3c8daabc939f06e0299d64ce186cc Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Thu, 19 Jun 2025 21:30:27 -0700 Subject: [PATCH 11/14] fix(pyproject): reintroduce console script --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2b04ccb..d168370 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,11 @@ dependencies = [ "simplejson >=3.19.2, <4.0.0", ] +[project.scripts] +pyconfig = "pyconfig.scripts:main" + [dependency-groups] etcd = [ - # "python-etcd", "python-etcd>=0.4.5", ] dev = [ From ebbfb374dde684bb0065545dd20bd9cc8b855c26 Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Thu, 19 Jun 2025 21:50:33 -0700 Subject: [PATCH 12/14] feat(pyconfig): updated script to be python 3 compatible in AST usage --- pyconfig/scripts.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pyconfig/scripts.py b/pyconfig/scripts.py index 0423a2b..419a329 100644 --- a/pyconfig/scripts.py +++ b/pyconfig/scripts.py @@ -1,4 +1,3 @@ -import _ast import argparse import ast import os @@ -189,7 +188,7 @@ def get_key(self): return self.key line = self.source[self.col_offset :] - regex = re.compile("""pyconfig\.[eginst]+\(([^,]+).*?\)""") + regex = re.compile(r"""pyconfig\.[eginst]+\(([^,]+).*?\)""") match = regex.match(line) if not match: return Unparseable() @@ -202,7 +201,7 @@ def _source_call_only(self): """ line = self.source[self.col_offset :] - regex = re.compile("""(pyconfig\.[eginst]+\(['"][^)]+?['"].*?\))""") + regex = re.compile(r"""(pyconfig\.[eginst]+\(['"][^)]+?['"].*?\))""") match = regex.match(line) if not match: # Fuck it, return the whole line @@ -216,7 +215,7 @@ def _default_value_only(self): """ line = self.source[self.col_offset :] - regex = re.compile("""pyconfig\.[eginst]+\(['"][^)]+?['"], ?(.*?)\)""") + regex = re.compile(r"""pyconfig\.[eginst]+\(['"][^)]+?['"], ?(.*?)\)""") match = regex.match(line) if not match: return "" @@ -543,12 +542,12 @@ def _parse_file(filename, relpath=None): filename = os.path.relpath(filename, relpath) for call in ast.walk(nodes): - if not isinstance(call, _ast.Call): + if not isinstance(call, ast.Call): # Skip any node that isn't a Call continue func = call.func - if not isinstance(call.func, _ast.Attribute): + if not isinstance(call.func, ast.Attribute): # We're looking for calls to pyconfig.*, so the function has to be # an Attribute node, otherwise skip it continue @@ -567,8 +566,8 @@ def _parse_file(filename, relpath=None): args = [] if call.args: arg = call.args[0] - if isinstance(arg, _ast.Str): - args.append(arg.s) + if isinstance(arg, ast.Constant) and isinstance(arg.value, str): + args.append(arg.value) else: args.append(_map_arg(arg)) @@ -587,12 +586,13 @@ def _map_arg(arg): Return `arg` appropriately parsed or mapped to a usable value. """ - # Grab the easy to parse values - if isinstance(arg, _ast.Str): - return repr(arg.s) - elif isinstance(arg, _ast.Num): - return arg.n - elif isinstance(arg, _ast.Name): + # Python 3.8+ uses ast.Constant for literals + if isinstance(arg, ast.Constant): + if isinstance(arg.value, str): + return repr(arg.value) + else: + return arg.value + elif isinstance(arg, ast.Name): name = arg.id if name == "True": return True From 3a910ebb2a8df8d68f5e59238d5f5d5e70af7e41 Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Thu, 19 Jun 2025 22:24:21 -0700 Subject: [PATCH 13/14] fix(scripts): refactor for python 3, better testing, and some bug fixing test(scripts): add test suite for CLI usage --- pyconfig/scripts.py | 43 +++-- pyproject.toml | 6 + test/test_scripts.py | 440 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 471 insertions(+), 18 deletions(-) create mode 100644 test/test_scripts.py diff --git a/pyconfig/scripts.py b/pyconfig/scripts.py index 419a329..e103e0f 100644 --- a/pyconfig/scripts.py +++ b/pyconfig/scripts.py @@ -18,12 +18,11 @@ pygments = None -def main(): - """ - Main script for `pyconfig` command. - - """ - parser = argparse.ArgumentParser(description="Helper for working with pyconfigs") +def _create_parser(pygments_available): + """Creates the argument parser for the script.""" + parser = argparse.ArgumentParser( + description="Helper for working with pyconfigs", prog="pyconfig" + ) target_group = parser.add_mutually_exclusive_group() target_group.add_argument( "-f", "--filename", help="parse an individual file or directory", metavar="F" @@ -74,12 +73,21 @@ def main(): parser.add_argument( "-c", "--color", - help="toggle output colors (default: %s)" % bool(pygments), + help="toggle output colors (default: %s)" % pygments_available, action="store_const", - default=bool(pygments), - const=(not bool(pygments)), + default=pygments_available, + const=(not pygments_available), ) - args = parser.parse_args() + return parser + + +def main(argv=None): + """ + Main script for `pyconfig` command. + + """ + parser = _create_parser(bool(pygments)) + args = parser.parse_args(argv) if args.color and not pygments: _error("Pygments is required for color output.\n pip install pygments") @@ -141,11 +149,11 @@ def as_namespace(self, namespace=None): :attr:`key`, then that section of the key will be removed. """ - key = self.key - if namespace and key.startswith(namespace): + key = self.get_key() + if namespace and isinstance(key, str) and key.startswith(namespace + "."): key = key[len(namespace) + 1 :] - return "%s = %s" % (self.get_key(), self._default() or NotSet()) + return "%s = %s" % (key, self._default() or NotSet()) def as_live(self): """ @@ -231,6 +239,8 @@ def _default(self): # Check if it's iterable iter(self.default) except TypeError: + if self.default is None: + return "" return repr(self.default) # This is to look for unparsable values, and if we find one, we try to @@ -241,7 +251,7 @@ def _default(self): if default: return default # Otherwise just make it a string and go - return ", ".join(str(v) for v in self.default) + return ", ".join(repr(v) for v in self.default) def __repr__(self): return self.as_call() @@ -588,10 +598,7 @@ def _map_arg(arg): """ # Python 3.8+ uses ast.Constant for literals if isinstance(arg, ast.Constant): - if isinstance(arg.value, str): - return repr(arg.value) - else: - return arg.value + return arg.value elif isinstance(arg, ast.Name): name = arg.id if name == "True": diff --git a/pyproject.toml b/pyproject.toml index d168370..8aee41f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,3 +36,9 @@ docs = [ # This is required so setuptools doesn't see "script" as a package [tool.setuptools] packages = ["pyconfig"] + +[tool.pytest.ini_options] +addopts = "--cov --cov-report term-missing" + +[tool.coverage.run] +source = ["pyconfig"] diff --git a/test/test_scripts.py b/test/test_scripts.py new file mode 100644 index 0000000..6129238 --- /dev/null +++ b/test/test_scripts.py @@ -0,0 +1,440 @@ +""" +Tests for the pyconfig scripts. + +""" + +from pathlib import Path +from typing import Callable, Dict, Union + +import pytest +from _pytest.capture import CaptureFixture +from _pytest.monkeypatch import MonkeyPatch + +from pyconfig import scripts + +# Type alias for the recursive file definition structure +FileDef = Dict[str, Union[str, "FileDef"]] +# Type alias for the factory fixture +CreateFilesFixture = Callable[[FileDef], Path] + + +@pytest.fixture +def create_test_files(tmp_path: Path) -> CreateFilesFixture: + """A factory fixture to create temporary files and directories for testing. + + The created files are automatically cleaned up by pytest's tmp_path fixture. + """ + + def _create_files_recursively(base_path: Path, file_defs: FileDef) -> None: + for name, content in file_defs.items(): + path = base_path / name + if isinstance(content, dict): + path.mkdir() + _create_files_recursively(path, content) + else: + path.write_text(content) + + def _create_test_files(file_defs: FileDef) -> Path: + """ + Create files and directories from a dictionary of definitions. + + If a single file definition is provided, it returns the path to that file. + Otherwise, it returns the base temporary path. + + :param file_defs: A dictionary where keys are filenames and values are + their content. If content is a dict, a directory is + created and the process is repeated. + """ + _create_files_recursively(tmp_path, file_defs) + if len(file_defs) == 1: + name, content = next(iter(file_defs.items())) + if isinstance(content, str): + return tmp_path / name + return tmp_path + + return _create_test_files + + +def test_main_help(capsys: CaptureFixture) -> None: + """Tests that the --help flag works as expected.""" + with pytest.raises(SystemExit) as e: + scripts.main(["--help"]) + + assert e.value.code == 0 + + captured = capsys.readouterr() + assert "usage: pyconfig" in captured.out + assert "Helper for working with pyconfigs" in captured.out + + +def test_main_filename( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that the --filename flag works as expected.""" + test_file = create_test_files( + { + "test_config.py": """ +import pyconfig + +pyconfig.get('test.key.one') +pyconfig.setting('test.key.two', 'default_value') +pyconfig.set('test.key.three', True) +""" + } + ) + + scripts.main(["--filename", str(test_file), "--color"]) + + captured = capsys.readouterr() + expected_output = ( + "test.key.one = \n" + "test.key.three = True\n" + "test.key.two = 'default_value' " + ) + assert captured.out == expected_output + + +def test_main_only_keys( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that the --only-keys flag works as expected.""" + test_file = create_test_files( + { + "test_config.py": """ +import pyconfig + +pyconfig.get('test.key.one') +pyconfig.setting('test.key.two', 'default_value') +pyconfig.set('test.key.three', True) +""" + } + ) + scripts.main(["--filename", str(test_file), "--only-keys", "--color"]) + + captured = capsys.readouterr() + expected_output = "test.key.one\ntest.key.three\ntest.key.two " + assert captured.out == expected_output + + +def test_main_module( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that the --module flag works as expected.""" + # Create a package structure + tmp_path = create_test_files( + { + "testpkg": { + "__init__.py": "", + "sub": { + "__init__.py": "", + "mod.py": "import pyconfig\npyconfig.get('test.key.one')", + }, + } + } + ) + + # Add the temp path to sys.path so the module can be imported + import sys + + sys.path.insert(0, str(tmp_path)) + + try: + scripts.main(["--module", "testpkg.sub.mod", "--color"]) + finally: + # remove from path + sys.path.pop(0) + + captured = capsys.readouterr() + expected_output = "test.key.one = " + assert captured.out == expected_output + + +def test_main_no_pygments_color_error( + capsys: CaptureFixture, + monkeypatch: MonkeyPatch, + create_test_files: CreateFilesFixture, +) -> None: + """Tests that an error is raised when --color is used without pygments.""" + test_file = create_test_files( + {"test_config.py": "import pyconfig\npyconfig.get('k')"} + ) + + monkeypatch.setattr(scripts, "pygments", None) + + with pytest.raises(SystemExit) as e: + # We need to force color to be on, which it is by default when pygments + # is missing. + scripts.main(["--filename", str(test_file), "--color"]) + assert e.value.code == 1 + + captured = capsys.readouterr() + assert "Pygments is required" in captured.err + + +def test_main_view_call( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that the --view-call flag works as expected.""" + test_file = create_test_files( + {"test_config.py": "import pyconfig\npyconfig.get('test.key.one')"} + ) + scripts.main(["--filename", str(test_file), "--view-call", "--color"]) + + captured = capsys.readouterr() + expected_output = "pyconfig.get('test.key.one') " + assert captured.out == expected_output + + +def test_main_source( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that the --source flag works as expected.""" + test_file = create_test_files( + {"test_config.py": "import pyconfig\npyconfig.get('test.key.one')"} + ) + scripts.main(["--filename", str(test_file), "--source", "--color"]) + + captured = capsys.readouterr() + expected_output = f"# {test_file.name}, line 2\ntest.key.one = " + assert captured.out == expected_output + + +def test_main_parse_dir( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that parsing a directory works as expected.""" + test_dir = create_test_files( + { + "config_dir": { + "a.py": "import pyconfig\npyconfig.get('key.a')", + "b.py": "import pyconfig\npyconfig.get('key.b')", + } + } + ) + scripts.main(["--filename", str(test_dir / "config_dir"), "--color"]) + + captured = capsys.readouterr() + expected_output = "key.a = \nkey.b = " + assert captured.out == expected_output + + +def test_main_load_configs( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that the --load-configs flag works as expected.""" + test_file = create_test_files( + {"test_config.py": "import pyconfig\npyconfig.get('test.key.one')"} + ) + # Set a value in the config to be loaded + import pyconfig + + pyconfig.set("test.key.one", "loaded_value") + + scripts.main(["--filename", str(test_file), "--load-configs", "--color"]) + + captured = capsys.readouterr() + expected_output = "test.key.one = 'loaded_value' " + assert expected_output in captured.out + + # Clean up the config + del pyconfig.Config().settings["test.key.one"] + + +def test_main_natural_sort( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that the --natural-sort flag works as expected.""" + test_dir = create_test_files( + { + "b.py": "import pyconfig\npyconfig.get('key.b')", + "a.py": "import pyconfig\npyconfig.get('key.a')", + } + ) + scripts.main(["--filename", str(test_dir), "--natural-sort", "--color"]) + + captured = capsys.readouterr() + # The output should be sorted by filename (a.py, then b.py) + expected_output = "key.a = \nkey.b = " + assert captured.out == expected_output + + +def test_main_all_keys( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that the --all flag works as expected.""" + test_file = create_test_files( + { + "test_config.py": """ +import pyconfig + +pyconfig.get('test.key.one') +pyconfig.setting('test.key.two', 'default_value') +pyconfig.set('test.key.one', 'default_for_one') +""" + } + ) + scripts.main(["--filename", str(test_file), "--all", "--color"]) + + captured = capsys.readouterr() + assert "test.key.one = " in captured.out + assert "test.key.one = 'default_for_one'" in captured.out + assert "test.key.two = 'default_value'" in captured.out + + +def test_main_syntax_error_file( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that a file with a syntax error is handled gracefully.""" + test_file = create_test_files({"bad_syntax.py": "this is not python"}) + with pytest.raises(SystemExit) as e: + scripts.main(["--filename", str(test_file), "--color"]) + assert e.value.code == 1 + captured = capsys.readouterr() + assert "No pyconfig calls" in captured.err + + +def test_main_unparseable_call( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that a call that can't be parsed is handled correctly.""" + test_file = create_test_files( + {"unparseable.py": "import pyconfig\npyconfig.get('key', 'default')"} + ) + scripts.main(["-f", str(test_file), "--color"]) + + captured = capsys.readouterr() + assert "key = 'default'" in captured.out + + +def test_main_unparseable_pyconfig_call( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that a pyconfig call with an unparseable key is handled.""" + test_file = create_test_files( + {"unparseable.py": "import pyconfig\npyconfig.get(some_variable)"} + ) + scripts.main(["-f", str(test_file), "--color"]) + captured = capsys.readouterr() + assert "some_variable = " in captured.out + + +def test_main_module_not_found(capsys: CaptureFixture) -> None: + """Tests that a non-existent module raises an error.""" + with pytest.raises(SystemExit) as e: + scripts.main(["--module", "nonexistent.module"]) + assert e.value.code == 1 + captured = capsys.readouterr() + assert "Could not load module or package" in captured.err + + +def test_main_no_calls_found( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that a file with no pyconfig calls produces the correct message.""" + test_file = create_test_files({"no_calls.py": "print('hello')"}) + with pytest.raises(SystemExit) as e: + scripts.main(["--filename", str(test_file)]) + assert e.value.code == 1 + captured = capsys.readouterr() + assert "No pyconfig calls" in captured.err + + +def test_main_file_not_found(capsys: CaptureFixture) -> None: + """Tests that a non-existent file path raises an error.""" + with pytest.raises(SystemExit) as e: + scripts.main(["--filename", "nonexistent_file.py"]) + assert e.value.code == 1 + captured = capsys.readouterr() + assert "Could not determine file type" in captured.err + + +def test_main_unparseable_module( + capsys: CaptureFixture, monkeypatch: MonkeyPatch +) -> None: + """Tests that an unparseable module (e.g., no __file__) is handled.""" + monkeypatch.setattr( + scripts, "_get_module_filename", lambda _: scripts.Unparseable() + ) + with pytest.raises(SystemExit) as e: + scripts.main(["--module", "some.module"]) + assert e.value.code == 1 + captured = capsys.readouterr() + assert "Could not determine module source" in captured.err + + +def test_main_colorized_output( + capsys: CaptureFixture, + create_test_files: CreateFilesFixture, + monkeypatch: MonkeyPatch, +) -> None: + """Tests that the output is colorized when pygments is available.""" + # This test will only work if pygments is actually installed. + if not scripts.pygments: + pytest.skip("pygments not installed") + + # Keep a reference to the original function + original_create_parser = scripts._create_parser + + # Force color to be on + monkeypatch.setattr( + scripts, + "_create_parser", + lambda pygments_available: original_create_parser(True), + ) + + test_file = create_test_files({"c.py": "import pyconfig\npyconfig.get('key.c')"}) + scripts.main(["--filename", str(test_file)]) + + captured = capsys.readouterr() + # Check for the ANSI escape code for color. + assert "\x1b[" in captured.out + + +def test_pyconfig_call_as_namespace() -> None: + """Tests the as_namespace method of the _PyconfigCall class.""" + source = ("", "", 0, 0) + call = scripts._PyconfigCall("get", "my.app.key", None, source) + assert call.as_namespace(namespace="my.app") == "key = " + assert call.as_namespace(namespace="other.app") == "my.app.key = " + assert call.as_namespace() == "my.app.key = " + + +def test_pyconfig_call_as_namespace_unparseable() -> None: + """Tests the as_namespace method with an unparseable key.""" + source_line = "pyconfig.get(some_variable)" + source = ("unparseable.py", source_line, 1, 0) + call = scripts._PyconfigCall("get", scripts.Unparseable(), [], source) + assert call.as_namespace(namespace="my.app") == " = " + assert call.as_namespace() == " = " + + +def test_main_false_and_none_defaults( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests that False and None defaults are handled correctly.""" + test_file = create_test_files( + { + "test_config.py": """ +import pyconfig +pyconfig.setting('a', False) +pyconfig.setting('b', None) +""" + } + ) + scripts.main(["--filename", str(test_file), "--color"]) + + captured = capsys.readouterr() + assert "a = False" in captured.out + assert "b = None" in captured.out + + +def test_main_file_with_encoding( + capsys: CaptureFixture, create_test_files: CreateFilesFixture +) -> None: + """Tests parsing a file with a non-UTF-8 encoding.""" + # The content is still UTF-8, but we're testing the declaration parsing. + content = "# -*- coding: latin-1 -*-\nimport pyconfig\npyconfig.get('key.d')" + test_file = create_test_files({"encoded.py": content}) + scripts.main(["--filename", str(test_file), "--color"]) + captured = capsys.readouterr() + assert "key.d = " in captured.out From ef3b1752e966dfa7c332f7237e7e980fd8257418 Mon Sep 17 00:00:00 2001 From: Jacob Alheid Date: Thu, 19 Jun 2025 23:12:44 -0700 Subject: [PATCH 14/14] chore(gitignore): ignore VSCode settings, cursor rules --- .cursor/rules/.gitignore | 8 +++ .cursor/rules/github-actions.mdc | 48 ----------------- .cursor/rules/markdown-documentation.mdc | 40 --------------- .cursor/rules/python-development.mdc | 65 ------------------------ .cursor/rules/python-testing.mdc | 38 -------------- .cursor/rules/version-control.mdc | 41 --------------- .cursor/rules/version-management.mdc | 44 ---------------- .gitignore | 3 ++ 8 files changed, 11 insertions(+), 276 deletions(-) create mode 100644 .cursor/rules/.gitignore delete mode 100644 .cursor/rules/github-actions.mdc delete mode 100644 .cursor/rules/markdown-documentation.mdc delete mode 100644 .cursor/rules/python-development.mdc delete mode 100644 .cursor/rules/python-testing.mdc delete mode 100644 .cursor/rules/version-control.mdc delete mode 100644 .cursor/rules/version-management.mdc diff --git a/.cursor/rules/.gitignore b/.cursor/rules/.gitignore new file mode 100644 index 0000000..0a59435 --- /dev/null +++ b/.cursor/rules/.gitignore @@ -0,0 +1,8 @@ +python-development.mdc +python-general.mdc +python-github-actions.mdc +python-markdown-documentation.mdc +python-packaging-modernization.mdc +python-testing.mdc +python-version-control.mdc +python-version-management.mdc diff --git a/.cursor/rules/github-actions.mdc b/.cursor/rules/github-actions.mdc deleted file mode 100644 index cfee0dc..0000000 --- a/.cursor/rules/github-actions.mdc +++ /dev/null @@ -1,48 +0,0 @@ ---- -description: GitHub Actions security and best practices -globs: [".github/workflows/*.yml", ".github/workflows/*.yaml", ".github/workflows/**/*.yml", ".github/workflows/**/*.yaml"] -alwaysApply: false ---- - -# GitHub Actions Guidelines - -## Security Best Practices - -- **Always use commit hashes for GitHub Actions** instead of version tags - - This prevents supply chain attacks by ensuring we're using exact versions - - Include the version tag as a comment for reference - - Example: `uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4` - -## Version Management - -- Keep actions up to date with their latest stable versions -- Use caching where appropriate to speed up workflows -- Follow security best practices for GitHub Actions - -## Version Checking Commands - -Use these commands to find latest versions: - -```bash -# For GitHub-hosted projects -gh release list --repo / --limit 1 - -# For GitHub Actions specifically -gh api repos///releases/latest - -# For GitHub Actions versions -gh api repos///tags -``` - -## Best Practices - -- Never guess or use potentially outdated versions -- Always verify versions from official repositories -- Keep GitHub Actions and their versions up to date -- Use commit hashes for GitHub Actions instead of version tags -- Include version tags as comments for reference - -Example format: -```yaml -uses: owner/repo@commit_hash # v1.2.3 -``` diff --git a/.cursor/rules/markdown-documentation.mdc b/.cursor/rules/markdown-documentation.mdc deleted file mode 100644 index 87183d1..0000000 --- a/.cursor/rules/markdown-documentation.mdc +++ /dev/null @@ -1,40 +0,0 @@ ---- -description: Documentation and markdown formatting guidelines -globs: *.md,**/*.md,README* -alwaysApply: false ---- - -# Documentation Guidelines - -@Docs https://github.github.com/gfm/ - -## Markdown Formatting - -- Keep all documentation files properly formatted -- Run markdown linting before committing documentation changes - - ALWAYS use `pre-commit` for linting and formatting - - ALWAYS use `pre-commit run [--all-files | --files [FILES ...]] prettier` to ensure markdown files conform to prettier's standards before running markdown lint - - ALWAYS use `pre-commit run [--all-files | --files [FILES ...]] markdownlint-cli2` hook for formatting a specific markdown file or all markdown files in the repository - - IF AND ONLY IF `pre-commit` is not available as a command, `uv run pre-commit` should be used as a fallback -- Use proper markdown formatting: - - Add blank lines before and after code blocks - - Use proper heading hierarchy - - Keep line length reasonable (88 characters) - - Use proper list formatting with blank lines between list items - -## Documentation Standards - -- Keep README.md updated with new features -- Document any breaking changes -- Add docstrings to all public APIs -- Follow the project's markdown style guide -- Use pre-commit hooks to enforce markdown formatting -- Check markdown files with `markdownlint-cli2` before committing - -## Content Guidelines - -- Write clear, concise documentation -- Include code examples where helpful -- Keep documentation up to date with code changes -- Use consistent terminology throughout -- Provide installation and usage instructions diff --git a/.cursor/rules/python-development.mdc b/.cursor/rules/python-development.mdc deleted file mode 100644 index 30495ff..0000000 --- a/.cursor/rules/python-development.mdc +++ /dev/null @@ -1,65 +0,0 @@ ---- -description: Core Python development guidelines for pyconfig project -globs: *.py,**/*.py -alwaysApply: false ---- - -# Python Development Guidelines - -## Running Python commands - -- All commands are managed with `uv` and should use `uv run` - - NEVER try to run commands with `python -m` directly, always use `uv run` or `uv run python -m` - - ALWAYS prefer invoking commands directly like `uv run pytest` rather than the module like `uv run python -m pytest` - -## Code Style & Formatting - -- Python projects are formatted using `ruff` - - Ruff formatter documentation - - Files can be auto formatted using `uv run ruff format [OPTIONS] [FILES]...` -- ALWAYS use type hints for all function parameters and return values -- ALWAYS create an appropriate docstring for functions, classes, and modules following Google style - - ALWAYS describe possible edge cases, errors, and conditionals that materially affect the behavior of the documented code -- Follow PEP 8 standards -- Prefer descriptive variable and function names -- Keep line length to 88 characters (Black formatter default) - -## Project Structure - -- This project uses `uv` for dependency management -- Tests should be placed in the `tests/` directory with matching structure to the project codebase -- Use `pytest` for all testing - - ALWAYS use the format `uv run pytest [options] [file_or_dir] [file_or_dir] [...]` when running tests - - NEVER try to run `python -m pytest` - - Pytest documentation -- Follow the existing project structure when adding new modules - -## Dependencies & Imports - -- Use uv to manage dependencies via `pyproject.toml` - - Use appropriate `--group` arguments to `uv add` when adding dependencies so we don't add main package dependencies unnecessarily - - The default groups should be "dev" and "docs", with other specific groups created only by the user -- Group imports: standard library, third-party, local imports -- Use absolute imports when possible -- Avoid circular imports - -## Error Handling - -- Use specific exception types rather than bare `except:` -- Provide meaningful error messages -- Handle edge cases gracefully -- Use logging instead of print statements for debugging - -## Performance & Best Practices - -- Prefer list comprehensions over loops where readable -- Use context managers for resource management -- Avoid premature optimization but be mindful of performance - -## AI Assistant Instructions - -- Always suggest type hints when they're missing -- Recommend tests when implementing new functionality -- Point out potential security issues or edge cases -- Suggest more Pythonic approaches when applicable -- Consider backwards compatibility when making changes diff --git a/.cursor/rules/python-testing.mdc b/.cursor/rules/python-testing.mdc deleted file mode 100644 index 16f2629..0000000 --- a/.cursor/rules/python-testing.mdc +++ /dev/null @@ -1,38 +0,0 @@ ---- -description: Testing guidelines and best practices -globs: test/**/*, tests/**/*, **/test_*.py, **/*_test.py, test*/**/*.py, test*/*.py -alwaysApply: false ---- - -# Testing Guidelines - -## What to test - -- Start with creating exercise tests, testing intended behavior of functions, classes, and modules -- Try to minimally mock so we ensure that we aren't creating passthrough tests of no value -- Once we have good exercise tests, then continue to create more tests for common edge cases - -## Test Structure & Organization - -- Write comprehensive tests for all new functionality -- Use pytest fixtures appropriately -- Aim for high test coverage -- Test files should be named `test_*.py` -- Use descriptive test function names that explain what is being tested -- Tests should be placed in the `tests/` directory with matching structure to `pyconfig/` - -## Test Best Practices - -- Use pytest for all testing -- Group related tests into classes when appropriate -- Use parametrized tests for testing multiple scenarios -- Mock external dependencies appropriately -- Test both success and failure cases -- Include edge case testing - -## Fixtures & Setup - -- Use pytest fixtures for common test setup -- Prefer session, module, or class scoped fixtures when appropriate -- Keep fixtures focused and reusable -- Use temporary directories for file-based tests diff --git a/.cursor/rules/version-control.mdc b/.cursor/rules/version-control.mdc deleted file mode 100644 index 0b38aaa..0000000 --- a/.cursor/rules/version-control.mdc +++ /dev/null @@ -1,41 +0,0 @@ ---- -description: Version control and commit message guidelines -globs: -alwaysApply: false ---- - -# Version Control Guidelines - -## Commit Message Standards - -- This project uses pre-commit hooks - ensure they pass before committing -- **Always use Conventional Commits for all commit messages** following the specification at -- Use the format: `(scope): ` - **scope is required** -- The scope should reflect the portion of the package that was modified, for example if your package is `pytool`: - - If modifying the package root, `pytool/__init__.py` then the scope is `pytool`, or the package name. - - If modifying `pytool/lang.py`, use scope `lang` - - If modifying `pytool/utils/helpers.py`, use scope `utils` - - If modifying multiple modules, use the most relevant or common scope - -## Commit Types - -- Common types: `feat:`, `fix:`, `docs:`, `style:`, `refactor:`, `test:`, `chore:` -- Use `!` or `BREAKING CHANGE:` footer for breaking changes -- Keep commits focused and atomic - -## Commit Organization - -- **Group changes into small logical commits** - instead of making one large commit with multiple bullet points in the message, break changes into separate commits where each commit represents a single logical change or feature addition/fix - -## Examples - -- `feat(cli): add new CLI command for file processing` -- `fix(parser): handle empty input files correctly` -- `docs(readme): update README with installation instructions` - -## Best Practices - -- Keep commits focused and atomic -- Write clear, descriptive commit messages -- Reference issues when applicable -- Use present tense in commit messages diff --git a/.cursor/rules/version-management.mdc b/.cursor/rules/version-management.mdc deleted file mode 100644 index 65e90d8..0000000 --- a/.cursor/rules/version-management.mdc +++ /dev/null @@ -1,44 +0,0 @@ ---- -description: Version checking and dependency management best practices -globs: -alwaysApply: false ---- - -# Version Management Best Practices - -## General Principles - -- Always check for the latest versions of dependencies and GitHub Actions -- Never guess or use potentially outdated versions -- Always verify versions from official repositories -- Keep GitHub Actions and their versions up to date - -## Version Checking Commands - -Use the following commands to find latest versions: - -```bash -# For GitHub-hosted projects -gh release list --repo / --limit 1 - -# For GitHub Actions specifically -gh api repos///releases/latest - -# For GitHub Actions versions -gh api repos///tags -``` - -## Dependency Management - -- Use uv to manage Python dependencies -- Keep `pyproject.toml` up to date with version constraints -- Regularly update dependencies to latest compatible versions -- Use dependency scanning tools when available -- Document version requirements clearly - -## Security Considerations - -- Use commit hashes for GitHub Actions instead of version tags -- Include version tags as comments for reference -- Regularly audit dependencies for security vulnerabilities -- Keep all tooling and dependencies updated diff --git a/.gitignore b/.gitignore index f413420..23121ad 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ docs/_output # Cursor .cursorrules + +# VSCode +.vscode