diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..c21ddf1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: Bug report +about: Report a reproducible problem with Geocoder +title: "[Bug]: " +labels: bug +assignees: "" +--- + +## Summary + + + +## Steps To Reproduce + +1. +2. +3. + +## Expected Behavior + + + +## Actual Behavior + + + +## Environment + +- OS: +- Python version: +- Geocoder version or commit: +- Provider and command used: + +## Input And Logs + + + +## Additional Context + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..e5a1f4b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,27 @@ +--- +name: Feature request +about: Suggest an improvement to Geocoder +title: "[Feature]: " +labels: enhancement +assignees: "" +--- + +## Problem + + + +## Proposed Solution + + + +## Alternatives Considered + + + +## Example + + + +## Additional Context + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c19cb73..96e4c5c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,9 +1,9 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - time: "11:00" - commit-message: - prefix: "[Build]" +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + time: "11:00" + commit-message: + prefix: "[Build]" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..804c196 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,22 @@ +## Summary + + + +Closes # + +## Changes + + + +## Validation + + + +- [ ] Tests added or updated where behavior changed. +- [ ] Documentation updated where needed. +- [ ] `poetry run pytest` passes. +- [ ] `poetry run pylint src test` passes. + +## Review Notes + + diff --git a/.github/renovate.json b/.github/renovate.json index 44bde53..54d7d3a 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,8 +1,8 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["github>pangaeatech/.github", "customManagers:githubActionsVersions"], - "baseBranchPatterns": ["main"], - "branchPrefix": "renovate/", - "enabledManagers": ["poetry", "custom.regex"], - "lockFileMaintenance": { "enabled": true } -} +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["github>pangaeatech/.github", "customManagers:githubActionsVersions"], + "baseBranchPatterns": ["main"], + "branchPrefix": "renovate/", + "enabledManagers": ["poetry", "custom.regex"], + "lockFileMaintenance": { "enabled": true } +} diff --git a/.github/workflows/add-to-redmine.yml b/.github/workflows/add-to-redmine.yml deleted file mode 100644 index 31f567a..0000000 --- a/.github/workflows/add-to-redmine.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Add dependabot/mend PRs to Redmine -on: - pull_request: - types: [opened, reopened] - -permissions: - contents: write - pull-requests: write - repository-projects: read - -jobs: - add-to-redmine: - if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.user.login == 'renovate[bot]' || github.event.pull_request.user.login == 'mend-bolt-for-github[bot]' }} - uses: pangaeatech/.github/.github/workflows/add-to-redmine.yml@main - with: - pr_num: ${{github.event.pull_request.number}} - pr_url: ${{github.event.pull_request.html_url}} - pr_subject: ${{github.event.pull_request.title}} - pr_body: ${{github.event.pull_request.body}} - rm_url: "https://redmine.pangaeatech.com" - rm_project_id: "26cec-01" - rm_tracker_id: 29 - rm_version_id: 1038 - rm_field_id: 11 - secrets: - rm_key: ${{ secrets.REDMINE_API_KEY }} diff --git a/.github/workflows/auto-add-to-pr-and-redmine.yml b/.github/workflows/auto-add-to-pr-and-redmine.yml deleted file mode 100644 index a5d0763..0000000 --- a/.github/workflows/auto-add-to-pr-and-redmine.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Automatically Create PR and Add to Redmine on New Branches -concurrency: auto-pr-${{ github.ref }} -on: - push: - branches-ignore: - - "main" - - "master" - - "dependabot/**" - - "renovate/**" - - "whitesource/**" - - "testing" - - "import" - -permissions: - contents: write - pull-requests: write - repository-projects: read - actions: read - statuses: read - checks: read - -jobs: - pull-request: - runs-on: ubuntu-latest - outputs: - pr_url: ${{ steps.open-pr.outputs.pr_url }} - pr_num: ${{ steps.open-pr.outputs.pr_number }} - pr_created: ${{ steps.open-pr.outputs.pr_created }} - steps: - - uses: actions/checkout@v7 - - id: open-pr - run: | - gh pr view && echo "pr_created=false" >> $GITHUB_OUTPUT || { - gh pr create -d -B "main" -l "auto-pr" -t "${{ github.ref_name }}" -b "" - echo "pr_number=$(gh pr view --json number -q .number || echo "")" >> $GITHUB_OUTPUT - echo "pr_url=$(gh pr view --json url -q .url || echo "")" >> $GITHUB_OUTPUT - echo "pr_created=true" >> $GITHUB_OUTPUT - } - env: - GH_TOKEN: ${{ github.token }} - - extractnum: - needs: pull-request - if: ${{ needs.pull-request.outputs.pr_url && needs.pull-request.outputs.pr_created == 'true' }} - runs-on: ubuntu-latest - outputs: - issue: ${{ steps.extract.outputs.replaced }} - steps: - - id: extract - uses: frabert/replace-string-action@v2.5 - with: - pattern: "^(task|issue|bug)(\\d+)-.*$" - flags: "i" - string: "${{ github.ref_name }}" - replace-with: "$2" - - link-to-redmine: - needs: [extractnum, pull-request] - if: ${{ needs.pull-request.outputs.pr_url && needs.pull-request.outputs.pr_created == 'true' && needs.extractnum.outputs.issue != github.ref_name }} - uses: pangaeatech/.github/.github/workflows/link-to-redmine.yml@main - with: - pr_num: ${{ needs.pull-request.outputs.pr_num }} - pr_url: ${{ needs.pull-request.outputs.pr_url }} - rm_issue: ${{ needs.extractnum.outputs.issue }} - rm_url: "https://redmine.pangaeatech.com" - rm_field_id: 11 - secrets: - rm_key: ${{ secrets.REDMINE_API_KEY }} - - add-to-redmine: - needs: [extractnum, pull-request] - if: ${{ needs.pull-request.outputs.pr_url && needs.pull-request.outputs.pr_created == 'true' && needs.extractnum.outputs.issue == github.ref_name }} - uses: pangaeatech/.github/.github/workflows/add-to-redmine.yml@main - with: - pr_num: ${{ needs.pull-request.outputs.pr_num }} - pr_url: ${{ needs.pull-request.outputs.pr_url }} - pr_subject: ${{ github.ref_name }} - rm_url: "https://redmine.pangaeatech.com" - rm_project_id: "26cec-01" - rm_field_id: 11 - rm_tracker_id: 29 - rm_version_id: 1038 - secrets: - rm_key: ${{ secrets.REDMINE_API_KEY }} diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 466f8be..0419c0b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,65 +1,73 @@ -name: Test -concurrency: test-${{ github.head_ref || github.ref }} -on: - push: - branches: [main] - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: [main] - -permissions: - contents: read - -jobs: - build-and-test: - if: ${{ github.event_name == 'push' || (!github.event.pull_request.draft && !startsWith(github.event.pull_request.title, 'WIP ')) }} - runs-on: ubuntu-latest - - name: Build and Test - steps: - - uses: actions/checkout@v7 - with: - clean: true - lfs: true - submodules: true - ref: ${{ github.head_ref }} - - - name: Set up Python - uses: actions/setup-python@v7 - with: - python-version-file: "pyproject.toml" - - - name: Install poetry - uses: abatilo/actions-poetry@v4 - - - name: Set up local venv - run: | - poetry config virtualenvs.create true --local - poetry config virtualenvs.in-project true --local - - - run: poetry install - - - run: poetry run black --check . - - - run: poetry run pylint *.py */*.py - - - name: Run Coverage - run: | - poetry run coverage run --branch -m pytest test - poetry run coverage report --fail-under=85 ./*.py src/*.py - poetry run coverage html ./*.py src/*.py - - - name: Upload coverage artifacts - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v7 - with: - name: coverage - path: htmlcov/ - - - run: poetry run pdoc -o ./docs *.py - - - name: Upload documentation - uses: actions/upload-artifact@v7 - with: - name: documentation - path: docs/ +name: Test +concurrency: test-${{ github.head_ref || github.ref }} +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: [main] + +permissions: + contents: read + +jobs: + build-and-test: + if: ${{ github.event_name == 'push' || (!github.event.pull_request.draft && !startsWith(github.event.pull_request.title, 'WIP ')) }} + runs-on: ubuntu-latest + + name: Build and Test (Python ${{ matrix.python-version }}) + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - uses: actions/checkout@v7 + with: + clean: true + lfs: true + submodules: true + ref: ${{ github.head_ref }} + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python-version }} + + - name: Install poetry + uses: abatilo/actions-poetry@v4 + + - name: Set up local venv + run: | + poetry config virtualenvs.create true --local + poetry config virtualenvs.in-project true --local + + - run: poetry install + + - run: poetry run black --check . + + - name: Check docstrings and lint + run: poetry run pylint *.py */*.py + + - name: Check type hints + run: poetry run mypy src + + - name: Run Coverage + run: | + poetry run coverage run --branch -m pytest test + poetry run coverage report --fail-under=95 ./*.py src/*.py + poetry run coverage html ./*.py src/*.py + + - name: Upload coverage artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: coverage-python-${{ matrix.python-version }} + path: htmlcov/ + + - run: poetry run pdoc -o ./docs *.py + + - name: Upload documentation + uses: actions/upload-artifact@v7 + with: + name: documentation-python-${{ matrix.python-version }} + path: docs/ diff --git a/.github/workflows/update-redmine.yml b/.github/workflows/update-redmine.yml deleted file mode 100644 index 15a9910..0000000 --- a/.github/workflows/update-redmine.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Add PR Comments, Reviews and Status to Redmine -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - pull_request_review: - types: [submitted] - pull_request: - types: [closed, reopened, converted_to_draft, ready_for_review, review_requested] - -permissions: - contents: read - -jobs: - add-comment: - if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }} - uses: pangaeatech/.github/.github/workflows/update-redmine.yml@main - with: - pr_num: ${{github.event.issue.number}} - cmt_url: ${{github.event.comment.html_url}} - cmt_user: ${{github.event.comment.user.login}} - cmt_action: "Commented on the PR:" - cmt_body: ${{github.event.comment.body}} - rm_url: "https://redmine.pangaeatech.com" - rm_project_id: "26cec-01" - rm_field_id: 11 - secrets: - rm_key: ${{ secrets.REDMINE_API_KEY }} - - add-review-comment: - if: ${{ github.event_name == 'pull_request_review_comment' }} - uses: pangaeatech/.github/.github/workflows/update-redmine.yml@main - with: - pr_num: ${{github.event.pull_request.number}} - cmt_url: ${{github.event.comment.html_url}} - cmt_user: ${{github.event.comment.user.login}} - cmt_action: "Added a Review Comment to the PR:" - cmt_body: ${{github.event.comment.body}} - rm_url: "https://redmine.pangaeatech.com" - rm_project_id: "26cec-01" - rm_field_id: 11 - secrets: - rm_key: ${{ secrets.REDMINE_API_KEY }} - - add-review: - if: ${{ github.event_name == 'pull_request_review' }} - uses: pangaeatech/.github/.github/workflows/update-redmine.yml@main - with: - pr_num: ${{github.event.pull_request.number}} - cmt_url: ${{github.event.review.html_url}} - cmt_user: ${{github.event.review.user.login}} - cmt_action: "Reviewed the PR as: **${{github.event.review.state}}**" - cmt_body: ${{github.event.review.body}} - rm_url: "https://redmine.pangaeatech.com" - rm_project_id: "26cec-01" - rm_field_id: 11 - secrets: - rm_key: ${{ secrets.REDMINE_API_KEY }} - - add-status: - if: ${{ github.event_name == 'pull_request' && (github.event.action != 'closed' || !github.event.pull_request.merged) }} - uses: pangaeatech/.github/.github/workflows/update-redmine.yml@main - with: - pr_num: ${{github.event.pull_request.number}} - cmt_url: ${{github.event.pull_request.html_url}} - cmt_user: ${{github.event.sender.login}} - cmt_action: "Set the PR to: **${{github.event.action}}**" - rm_url: "https://redmine.pangaeatech.com" - rm_project_id: "26cec-01" - rm_field_id: 11 - secrets: - rm_key: ${{ secrets.REDMINE_API_KEY }} - - add-merged: - if: ${{ github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged }} - uses: pangaeatech/.github/.github/workflows/update-redmine.yml@main - with: - pr_num: ${{github.event.pull_request.number}} - cmt_url: ${{github.event.pull_request.html_url}} - cmt_user: ${{github.event.sender.login}} - cmt_action: "Set the PR to: **merged**" - rm_url: "https://redmine.pangaeatech.com" - rm_project_id: "26cec-01" - rm_field_id: 11 - secrets: - rm_key: ${{ secrets.REDMINE_API_KEY }} diff --git a/.pylintrc b/.pylintrc index f6da9d7..d57dc5f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -60,7 +60,10 @@ disable=multiple-imports, # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member +enable=c-extension-no-member, + missing-module-docstring, + missing-class-docstring, + missing-function-docstring [REPORTS] diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6a34d1a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +https://www.pangaeatech.com/contact +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1885f4d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,105 @@ +# Contributing to Geocoder + +Thank you for contributing. Please keep changes focused, include tests when +behavior changes, and do not commit API keys, credentials, or sensitive address +data. Community participation is governed by the [Code of +Conduct](.github/CODE_OF_CONDUCT.md). + +## Development Environment + +Geocoder supports Python `>=3.10,<3.15` and uses +[Poetry](https://python-poetry.org/) to manage dependencies. + +1. Fork the repository and clone your fork. +2. Create a branch from the current `main` branch. +3. Install the runtime and development dependencies: + + ```bash + poetry install + ``` + +4. Confirm the command-line interface is available: + + ```bash + poetry run geocoder --help + ``` + +Provider integration work may require `GEOCODIO_API_KEY` or +`GOOGLE_GEOCODING_API_KEY`. Set keys in your environment or in a local `.env` +file; never commit that file or its values. Prefer mocked provider responses in +tests so the test suite does not make network requests. + +## Tests And Checks + +Run the full test suite from the repository root: + +```bash +poetry run pytest test +``` + +Before opening a pull request, run the same checks enforced by CI: + +```bash +poetry run black --check . +poetry run pylint *.py */*.py +poetry run mypy src +poetry run coverage run --branch -m pytest test +poetry run coverage report --fail-under=95 ./*.py src/*.py +poetry run pdoc -o ./docs *.py +``` + +Use `poetry run black .` to format Python files. Add or update focused tests in +`test/` for changed behavior. Do not commit generated `docs/`, coverage output, +SQLite caches, or local credentials. + +## Branches And Commits + +Create branches from `main` using the established task-based convention: + +```text +Task- +``` + +For example, `Task27702-address-validation` or +`Task27956-prevent-repeat-api-calls`. If there is no issue, use a short +descriptive branch name such as `fix-cache-cleanup`. + +Write concise, imperative commit subjects. Start with the task number when one +exists, summarize one logical change, and avoid punctuation at the end: + +```text +Task27702 validate required address fields +Fix cache cleanup on failed requests +``` + +Keep unrelated formatting or refactoring out of functional commits. Explain +non-obvious context, compatibility impact, or follow-up work in the commit body +when needed. + +## Bug Reports + +Search existing issues before opening a new report. Use the [bug report +template](https://github.com/pangaeatech/geocoder/issues/new?template=bug_report.md) +and include: + +- A concise summary and reproducible steps. +- Expected and actual behavior, including the complete error or traceback. +- Your OS, Python version, Geocoder commit or version, provider, and command. +- A minimal sanitized workbook or sample rows and relevant logs. + +Never attach API keys, credentials, private addresses, or other sensitive data. +Report security vulnerabilities according to [SECURITY.md](SECURITY.md), not in +a public issue. + +## Pull Requests + +1. Open the pull request against `main` from a focused branch. +2. Complete the [pull request template](.github/pull_request_template.md), link + the related issue, and describe validation performed. +3. Add or update tests and documentation when the change affects behavior or + user-facing usage. +4. Ensure the checks above pass and address review feedback. + +Keep pull requests small enough to review. Call out provider-specific behavior, +API compatibility concerns, cache effects, and any work that remains for a +follow-up change. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b830d86 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Pangaea Information Technologies, Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b70eb13 --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +# Geocoder + +Geocoder reads address records from an Excel workbook, submits them to a selected +geocoding provider, and writes a new workbook containing the original fields, +normalized location results, match metadata, and optional raw provider responses. +Repeated queries are deduplicated within a run and can be stored in a local SQLite +cache to avoid paying for the same lookup again. + +## Requirements + +- Python `>=3.10,<3.15` +- [Poetry](https://python-poetry.org/) for the supported installation workflow + +## Installation + +Install the runtime dependencies: + +```bash +poetry install --only main +``` + +For development, testing, linting, and documentation tools, install the `dev` +group as well: + +```bash +poetry install +``` + +Run the command from the project root: + +```bash +poetry run geocoder --help +``` + +## Input Workbook + +The input must be an `.xlsx` workbook. Each worksheet to process needs these +columns (matching is case-insensitive): + +- `ADDRESS` +- `CITY` +- `STATEPROV` + +The following columns are optional: `ID`, `NAME`, `POSTALCODE`, `COUNTRY`, +`LATITUDE`, and `LONGITUDE`. Common names such as `street address`, `state`, +`province`, `zip code`, and `postal code` are recognized. + +For example: + +| ID | ADDRESS | CITY | STATEPROV | POSTALCODE | COUNTRY | +| --- | --- | --- | --- | --- | --- | +| 1 | 1600 Pennsylvania Ave NW | Washington | DC | 20500 | US | +| 2 | 111 Richmond St W | Toronto | ON | M5H 2G4 | CA | + +All worksheets are processed by default. Pass a worksheet name as the third +positional argument to process just one sheet. + +## Basic Usage + +Use the free Census provider to geocode a U.S. workbook: + +```bash +poetry run geocoder input.xlsx output.xlsx --api census +``` + +Process only the `Facilities` worksheet: + +```bash +poetry run geocoder input.xlsx output.xlsx Facilities --api census +``` + +Use the worksheet name as `COUNTRY` when a row does not provide that column: + +```bash +poetry run geocoder input.xlsx output.xlsx --api geocodio --countryPerSheet +``` + +Add the provider's raw JSON response to the output workbook for troubleshooting: + +```bash +poetry run geocoder input.xlsx output.xlsx --api google --debug +``` + +By default, requests are cached in `geocoder-cache.sqlite` in the current +directory. Choose a cache location, reuse one or more read-only caches, or disable +persistent caching: + +```bash +poetry run geocoder input.xlsx output.xlsx --api census --cache data/geocoder.sqlite +poetry run geocoder input.xlsx output.xlsx --api census --cacheRead archive.sqlite +poetry run geocoder input.xlsx output.xlsx --api census --noCache +``` + +## Providers + +| Provider | `--api` value | Coverage | API key | +| --- | --- | --- | --- | +| U.S. Census Bureau address batch service | `census` | U.S. addresses | Not required | +| Geocodio | `geocodio` | U.S., Canada, Mexico, and U.K. addresses | Required | +| Google Geocoding API | `google` | Coverage depends on Google Geocoding API availability | Required | + +`census` is the default provider. It sends records in batches of up to 10,000. +Geocodio also submits batches, while Google is queried one address at a time. + +## API Keys + +The Geocodio and Google providers require a key. Supply it directly with +`--apiKey`, set the corresponding environment variable, or put the variable in a +`.env` file in the working directory. Environment variables take precedence over +values in `.env`. + +```bash +# Geocodio +export GEOCODIO_API_KEY="your-key" +poetry run geocoder input.xlsx output.xlsx --api geocodio + +# Google +export GOOGLE_GEOCODING_API_KEY="your-key" +poetry run geocoder input.xlsx output.xlsx --api google + +# One-off key +poetry run geocoder input.xlsx output.xlsx --api geocodio --apiKey "your-key" +``` + +An equivalent `.env` file is: + +```dotenv +GEOCODIO_API_KEY=your-geocodio-key +GOOGLE_GEOCODING_API_KEY=your-google-key +``` + +Do not commit `.env` files or API keys. `.env` is ignored by this repository. + +## Output + +The output workbook retains the source fields as `SOURCE_*` columns and adds +`RESULT_*` columns for normalized provider results. It also includes: + +- `GEOCODER_API`: the provider used for the row. +- `MATCH_TYPE`: whether the provider reported an exact, partial, or no match. +- `ACCURACY`: the normalized precision score. +- `LOCATION_TYPE`: provider precision metadata. +- `MATCH_NOTES`: any notes returned while processing the record. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c57bc3e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,37 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in geocoder, please report it +privately rather than opening a public issue — this gives time to investigate +and release a fix before details are public. + +To report a vulnerability, contact the maintainer directly: +**https://www.pangaeatech.com/contact** + +Please include as much of the following as you can: + +- A description of the vulnerability and its potential impact +- Steps to reproduce it (a minimal example is ideal) +- The affected version/commit +- Any suggested fix or mitigation, if you have one + +## What to expect + +- Please allow a reasonable amount of time for a response before disclosing + publicly. +- You'll be credited in the fix/release notes if you'd like, unless you + prefer to remain anonymous. + +## Scope + +This project authenticates to multiple third-party services, including Google +and Geocodio. Vulnerabilities of particular interest include: + +- Anything that could leak or mishandle API keys or credentials +- Anything that could cause excessive charges to a user's account +- Dependency vulnerabilities with a realistic exploit path in this project's + usage + +Issues in third-party services this project depends on (Google Maps, Geocodio, +etc.) should be reported to those services directly, not here. diff --git a/geocoder.py b/geocoder.py index 80179b7..576e498 100755 --- a/geocoder.py +++ b/geocoder.py @@ -1,11 +1,8 @@ #!/usr/bin/python3 # -.- coding: utf-8 -.- -# -.- dependencies: Python 3.8+ -.- """ Geocoder - -Copyright (c) 2026 Pangaea Information Technologies, Ltd. """ from src.geocoder import main diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..2f98a57 --- /dev/null +++ b/opencode.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://opencode.ai/config.json", + + "permission": { + "bash": { + "poetry build *": "allow", + "poetry check *": "allow", + "poetry run black *": "allow", + "poetry run coverage *": "allow", + "poetry run mypy *": "allow", + "poetry run pdoc *": "allow", + "poetry run pylint *": "allow", + "poetry run pytest *": "allow", + "poetry show *": "allow" + }, + "edit": { + "**/.gitignore": "allow", + "**/.pylintrc": "allow", + "**/*.py": "allow", + "**/*.md": "allow", + "**/*.yml": "allow", + "**/poetry.lock": "allow", + "**/pyproject.toml": "allow", + "**/pytest.ini": "allow", + "**/renovate.json": "allow" + } + } +} diff --git a/poetry.lock b/poetry.lock index 9dabd87..b41fef3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,71 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.3 and should not be changed by hand. + +[[package]] +name = "ast-serialize" +version = "0.11.2" +description = "Python bindings for mypy AST serialization" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ast_serialize-0.11.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f6a8dfc5ab204a706f6e5d39c6f77c18c27ef084fa2081803a64a9160ce89277"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:cb073bfa15742699d408ac50f60878383b5665ae1791d1b6799ea6f08633cd77"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1d6ad94edbe93bf1dabc06c9f37d55b898fdabc456aa6d7ced5e23c14f795f32"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b2801cf2221bd922d9f69d2f0ebc373c3db47207315d525b2d87fa161a2af4"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd666cebd6ab3b3c0fd348a6202c26e18a401ee34293c3804d3472266bc146f6"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d01f61352c96370febf6c0dbd488dee9183a731fb2702170da9163ae317cded"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0fd40c668b0fa19b8fdb61d9e63d547e2e19cfbfe053a51ef0b6c37070298a8"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa819d7c14c8e4153dcd84671826331538be7cbe460383fc6386f5eea5bd234"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a9ffa8a197a721f07a352d0be6185f5b3e6f9aaebfdb66169ed652108531ae3b"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00119a8fb8c1dc0f1fab023f4d8071fa49e3b0208ee54d589fd463c16ab0124e"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0de02520c11391a026e62987a9aa2c3c2ff01545155059ddf0c4bdf2c5ecbe9f"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4e4558956b6a0fb35e18fba58f7d1810b1f2c0e6b52352572cd5dfb6b4ef33a"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6061a54f39e82a9f2cbcb9c268fc441890e4818a6636473caa4f4063254e0750"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:85fbb01e83967a126d71f679f2b9528ef0912cb0854aa1a4657314c34e255b57"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7aaaffc32905159774a107d3cf33dad59bd41b7a0d1bc9885532186753ee7439"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:08eda88a0f290a36c38cab33df8bf7e35eb95bc802ca5beb2c8fcda471a7d10c"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-win32.whl", hash = "sha256:76cc294246e60a914326b4ca88c6a5ea89c064906614aaf1537ce82f09e9449f"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:43b51e6ebe6549bf21416c3c78ee886147b80875a87cc6f69e303dde0d75be0b"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8df32ad4ff7843734a6c2f067ee974f6d3109ee5a2c3e1a9d2f79347bd282a9a"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:ab924ba260efd7509492f272d4e236d24564033f20c005d7c63a107c6a76fc85"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:a586be418eb70a9f1396cea29ddac8f4b9bf277fb73ea2340db31e218bc00f32"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8532f20916fa3189d4d785ef2a62d93c4d651ec9c5bffda66d2fc36898351f34"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee732ae167e686d1d3c00f98d7d82b23138304694f0441b14d7ddf9c0f8a921c"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75a1c7f46b9c19fc0ae01ca6fd076301628faa2ed7a8edbd55c6353c483946a3"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdf31a0bb85ea2575cc91669f005e6647d2efed491231c4dc1497bc9a5b3aa6"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b78e6fdef3b06c86ed263e1962fee5a7b9d2d158e738b212d13b2c605ee12f5"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:8d62a47714c8bc432b9fabcc29989c815c5da17327d35151f2fd0d85c2a7a5ff"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a5ffa70e76191dcf240d3c43e20c93b3bfd26f54d89148c762d57837f5bcd2c"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:bfbe47a3a7c368f28836e78b2440a3643ac0ec4c67d9fe53588e1448f0a3d35d"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:7f1823275b246f9c7d373be6879e4eec09686948895d4ad083f4b27fd7e4da70"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:57c0f5cb0021a5beb1e5e4d6e840ae2f23a28909703ef4d256a144cc1ad3d437"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:cd320a5c4f1f2742af97eea22954f776379175c5ef2504801e9a155f2ff9a4d7"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:13b13afe32e845c86a573497729e1b7ddeb26c572c78bf50ece51da23b8fad5e"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:9d80a81ec84660422579bdb8e789f656a794b48c7a1ae1261f6bd8bc1897d17d"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win32.whl", hash = "sha256:af8c003ce721b0099dd55cef4ba733500fc3054ea0cc8565d8957aaf7cccdeb4"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:554d117cb916d8032d85007c654d179efbbfd446174c048062778136a922944f"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:d60515335750d431e462af6e722bb55720a5e7827192777bddfd9c4376065a4d"}, + {file = "ast_serialize-0.11.2-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:89499a439955931281986e97ca4dd3c064bf0d2e0027c0017344eb86667733a1"}, + {file = "ast_serialize-0.11.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:daadf1c3e0224621607ffe16f1379e4bd372271ed2e1db8a67878f0bab3ef7e4"}, + {file = "ast_serialize-0.11.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1844ed9a487fb3de7325c52ddb33f2918b66b65cd54d3f8d83d23785ffe99fa4"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b17869f4ba261a5fa468a753328a548f4dbaf74b4eadae9e28aff66df7f1425b"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feb16d9c2a720e0120c58dd5d6e7b3c7c86b43249b60a3bc212bcb8fa031e2dd"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3109fe4805384effc8d0f8e41fbf875aa8f389af91b4348c1cfb60ea6e4cb82"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abdb3e49ba053c3486ac1263bee9f16cc9a4a8abd9f8c90bfc21e3669f3ad9d1"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7004ba572f09be34342ccb98dcd4bad5707d3d81adc8cb4c3f685d2a2c51bbc"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:59c25f47524efa052971b860e128b1add0c94ede7dd16b2962952c85c3582365"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3a367e0e05ed2d1b747ceb07aa728a8c204cc008b589127e9bd4f40053d7575"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:00bbf1f6669f813b48925b759f7ae4591067d456d443924055cab386e7e0a719"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ec1c20f89c3e0d83576e3c06f79375ce936266591fe0d5fd969914af3185cbaa"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c58bb119b73657fdc5569692f316e1e25ca114bd62f7782eb527c6be438ba3a9"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f739e0b601be7300c5697a2573d9200bd1db74b34ab111ef9537b9d5dcd7f106"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:cae5addfbb54cc1d47fe947ef9138e9d83849ed1cbc72b819cf36d96a2315b07"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2fa3be25f7f5351b1b39c9f8a52779b2dbf21199efbae564b4746422e8edca4e"}, + {file = "ast_serialize-0.11.2-cp39-abi3-win32.whl", hash = "sha256:d70556a2f9230a44c99a655774cde823f056efc34466eabfb4085f0cb1ea9f99"}, + {file = "ast_serialize-0.11.2-cp39-abi3-win_amd64.whl", hash = "sha256:b9065dd23131a23b41f5bab3bf4e9b3c350a3fe8e36e8200eded9b729fcea484"}, + {file = "ast_serialize-0.11.2-cp39-abi3-win_arm64.whl", hash = "sha256:dab599cbdcb7b45b18c41fad746645580b3a24357082b7f0e8921cd373804f27"}, + {file = "ast_serialize-0.11.2.tar.gz", hash = "sha256:976a5bd75845d22f4b52905ddf53ab669ef1b14dba7735f5512841a2ef2b5450"}, +] [[package]] name = "astroid" @@ -6,7 +73,7 @@ version = "4.0.4" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.10.0" -groups = ["main"] +groups = ["dev"] files = [ {file = "astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753"}, {file = "astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0"}, @@ -21,7 +88,7 @@ version = "26.3.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2"}, {file = "black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b"}, @@ -189,7 +256,7 @@ version = "8.4.2" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, @@ -204,7 +271,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main"] +groups = ["dev"] markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, @@ -217,7 +284,7 @@ version = "7.10.7" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, @@ -334,7 +401,7 @@ version = "0.4.1" description = "serialize all of Python" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d"}, {file = "dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"}, @@ -362,7 +429,7 @@ version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["dev"] markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, @@ -396,7 +463,7 @@ version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, @@ -408,7 +475,7 @@ version = "8.0.1" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.10.0" -groups = ["main"] +groups = ["dev"] files = [ {file = "isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"}, {file = "isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d"}, @@ -423,7 +490,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["dev"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -435,13 +502,161 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "librt" +version = "0.15.0" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489"}, + {file = "librt-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702"}, + {file = "librt-0.15.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c"}, + {file = "librt-0.15.0-cp310-cp310-win32.whl", hash = "sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e"}, + {file = "librt-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053"}, + {file = "librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22"}, + {file = "librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a"}, + {file = "librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0"}, + {file = "librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb"}, + {file = "librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db"}, + {file = "librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56"}, + {file = "librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e"}, + {file = "librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa"}, + {file = "librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2"}, + {file = "librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1"}, + {file = "librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022"}, + {file = "librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570"}, + {file = "librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26"}, + {file = "librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2"}, + {file = "librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b"}, + {file = "librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab"}, + {file = "librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890"}, + {file = "librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8"}, + {file = "librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad"}, + {file = "librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993"}, + {file = "librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879"}, + {file = "librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65"}, + {file = "librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622"}, + {file = "librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15"}, + {file = "librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28"}, + {file = "librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95"}, + {file = "librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714"}, + {file = "librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc"}, + {file = "librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf"}, + {file = "librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915"}, + {file = "librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605"}, + {file = "librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca"}, + {file = "librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0"}, + {file = "librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d"}, + {file = "librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374"}, + {file = "librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9"}, + {file = "librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8"}, + {file = "librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b"}, + {file = "librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54"}, + {file = "librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b"}, + {file = "librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162"}, + {file = "librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1"}, + {file = "librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc"}, + {file = "librt-0.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0e2d0c0acf5b0ada7d045912b7cf787c21315c95b38b1fa939ef72d45d366b3d"}, + {file = "librt-0.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9ca190fe9edc0eb08eec558a509a16d28d91c35667b8f043cba40ed5e77a959"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80811e1c42386ea95c6fb30571d3250ad43d7863f883f787f70517f441150e59"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:88c2a17815c266e6d8180204ff62cb739ab869ada4a746d4c505331526ac58f1"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a5fa8f1f916988d0bf1afea005bda37f56ac41a18016e813ccf0097a8d460ca4"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:355e3a4c725225a14262004fc1872a552b9d3634b4f791a0dfc80804aafbfd55"}, + {file = "librt-0.15.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc1ed11c4ad0b91af24def2050f2840ea4567828e3dd058fbe608d982f6e5465"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1f4ef2e71db33df4309167ed7f1520c4fae5e611226e159fa9cf33f93e6ddb3d"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1a1a8cd430c7dd0c083f455cb1b328d7fc682b05c31b940906f7845bdff80881"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d5387b908676c0b8d5d2f5fb58373b4ea382d81f7a6f0fab8ea2a462bb4738"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:1172c6ad2a88b646e7fe3b480e3fac4ab4418b3443fd8a4061fdd531e0622fc7"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52e8db01f603f5da0ca30987479acff98769382efc8e142fa3962395dcf3ffdb"}, + {file = "librt-0.15.0-cp39-cp39-win32.whl", hash = "sha256:e4c911f15a1652ca94ae9f1abd92e74cbb1b3597d2d92fdd556202f94e8cd455"}, + {file = "librt-0.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:68242379c9b65a582b6e97318a1e9fbd6d445e58954f2d437991c4804ab11578"}, + {file = "librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162"}, +] + [[package]] name = "markupsafe" version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, @@ -540,19 +755,96 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" -groups = ["main"] +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mypy" +version = "2.3.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "mypy-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:57a936373fc690c43a8cd7e7e12a35148e4ec5aa7698ad7fc0a9f918bdc5be41"}, + {file = "mypy-2.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d00d769056bde2f4e69c175071eba45cfb44fa1ed92bdfbfe64a93e0543b0cf0"}, + {file = "mypy-2.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2166b29228835e1f88ff411e96639e6ca3c7fdde84b62ec211f70f86b4051167"}, + {file = "mypy-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83d36c2924df7426333abe7faf4724a7e1aab0d9fd41625e81b4683034b80c13"}, + {file = "mypy-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:f12fdb70459d0060dea40b29e52163a961b156106d68d57882a6a9f648983a53"}, + {file = "mypy-2.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:e099200a1b1b1223a4951f0a90cbff1b8c91b250ba599dab1f7217a628144d90"}, + {file = "mypy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531"}, + {file = "mypy-2.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8"}, + {file = "mypy-2.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8"}, + {file = "mypy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01"}, + {file = "mypy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1"}, + {file = "mypy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6"}, + {file = "mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff"}, + {file = "mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff"}, + {file = "mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080"}, + {file = "mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355"}, + {file = "mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb"}, + {file = "mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b"}, + {file = "mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d"}, + {file = "mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb"}, + {file = "mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226"}, + {file = "mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74"}, + {file = "mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6"}, + {file = "mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac"}, + {file = "mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d"}, + {file = "mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3"}, + {file = "mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f"}, + {file = "mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82"}, + {file = "mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9"}, + {file = "mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d"}, + {file = "mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595"}, + {file = "mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2"}, + {file = "mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc"}, + {file = "mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045"}, + {file = "mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0"}, + {file = "mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63"}, + {file = "mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4"}, + {file = "mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57"}, + {file = "mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b"}, + {file = "mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561"}, + {file = "mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133"}, + {file = "mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9"}, + {file = "mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3"}, + {file = "mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523"}, + {file = "mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306"}, + {file = "mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021"}, + {file = "mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e"}, + {file = "mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc"}, + {file = "mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4"}, + {file = "mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29"}, + {file = "mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb"}, + {file = "mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419"}, +] + +[package.dependencies] +ast-serialize = ">=0.6.0,<1.0.0" +librt = {version = ">=0.13.0", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.15\""} + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -579,7 +871,7 @@ version = "26.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, @@ -591,7 +883,7 @@ version = "1.1.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, @@ -608,7 +900,7 @@ version = "15.0.4" description = "API Documentation for Python Projects" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "pdoc-15.0.4-py3-none-any.whl", hash = "sha256:f9028e85e7bb8475b054e69bde1f6d26fc4693d25d9fa1b1ce9009bec7f7a5c4"}, {file = "pdoc-15.0.4.tar.gz", hash = "sha256:cf9680f10f5b4863381f44ef084b1903f8f356acb0d4cc6b64576ba9fb712c82"}, @@ -625,7 +917,7 @@ version = "4.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74"}, {file = "platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0"}, @@ -637,7 +929,7 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -653,7 +945,7 @@ version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, @@ -668,7 +960,7 @@ version = "4.0.6" description = "python code static checker" optional = false python-versions = ">=3.10.0" -groups = ["main"] +groups = ["dev"] files = [ {file = "pylint-4.0.6-py3-none-any.whl", hash = "sha256:d11a0e1fdb7b1cd46ec5d6fc78fee8b95f28695b2d6140e5809925f61e32ea54"}, {file = "pylint-4.0.6.tar.gz", hash = "sha256:52f19191bee08bf103f9705ad1a0ece4aa5a0a4ef2bdcbd969375a1e6f6579d5"}, @@ -698,7 +990,7 @@ version = "9.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["dev"] files = [ {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, @@ -722,7 +1014,7 @@ version = "0.4.1" description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, @@ -799,7 +1091,7 @@ version = "2.4.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, @@ -857,20 +1149,31 @@ version = "0.15.1" description = "Style preserving TOML library" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304"}, {file = "tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97"}, ] +[[package]] +name = "types-openpyxl" +version = "3.1.5.20260827" +description = "Typing stubs for openpyxl" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "types_openpyxl-3.1.5.20260827-py3-none-any.whl", hash = "sha256:94e176d871d12e3cbc34f8fb03dc14db2a4245a6690791daf16fc7b08fd67869"}, + {file = "types_openpyxl-3.1.5.20260827.tar.gz", hash = "sha256:be8b605fb99cfd7d5f5576d4a508e8ec44be2dd15b85157c559080de6384be34"}, +] + [[package]] name = "typing-extensions" version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "python_version < \"3.11\"" +groups = ["dev"] files = [ {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, @@ -896,5 +1199,5 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [metadata] lock-version = "2.1" -python-versions = ">=3.10.0,<3.14.0" -content-hash = "ad9836929e66b795af095eaa6914f4d7206719e71ba1878298a735f561ce131a" +python-versions = ">=3.10.0,<3.15.0" +content-hash = "2493128478eda943d5f0c2e8ae0a13cedb26639e4da8ed2c0c0020fd50a3a4f6" diff --git a/public/Canada Test Data.xlsx b/public/Canada Test Data.xlsx deleted file mode 100644 index 1041529..0000000 Binary files a/public/Canada Test Data.xlsx and /dev/null differ diff --git a/public/Mexico Test Data.xlsx b/public/Mexico Test Data.xlsx deleted file mode 100644 index cb2c0fd..0000000 Binary files a/public/Mexico Test Data.xlsx and /dev/null differ diff --git a/public/U.S. Test Data.xlsx b/public/U.S. Test Data.xlsx deleted file mode 100644 index 40c75f1..0000000 Binary files a/public/U.S. Test Data.xlsx and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index f2dddcb..c8ff578 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,27 +1,38 @@ -[tool.poetry] -name = "Geocoder" -version = "0.0.0" -description = "Pangaea IT Geocoder" -authors = ["Pangaea Information Technologies, Ltd."] -readme = "README.md" -repository = "https://github.com/pangaeatech/geocoder" -package-mode = false - -[tool.black] -line-length = 150 -target-version = ['py38'] -include = '\.pyi?$' - -[tool.poetry.dependencies] -black = "26.3.1" -coverage = "7.10.7" -openpyxl = "3.1.5" -pdoc = "15.0.4" -pylint = "4.0.6" -pytest = "9.1.1" -python = ">=3.10.0,<3.14.0" -requests = "2.34.2" - -[tool.pytest.ini_options] -pythonpath = ["src"] -testpaths = ["test"] +[tool.poetry] +name = "Geocoder" +version = "0.0.0" +description = "Pangaea IT Geocoder" +authors = ["Pangaea Information Technologies, Ltd."] +readme = "README.md" +repository = "https://github.com/pangaeatech/geocoder" + +[tool.poetry.scripts] +geocoder = "src.geocoder:main" + +[tool.black] +line-length = 150 +target-version = ['py38'] +include = '\.pyi?$' + +[tool.mypy] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_defs = true + +[tool.poetry.dependencies] +openpyxl = "3.1.5" +python = ">=3.10.0,<3.15.0" +requests = "2.34.2" + +[tool.poetry.group.dev.dependencies] +black = "26.3.1" +coverage = "7.10.7" +pdoc = "15.0.4" +pylint = "4.0.6" +pytest = "9.1.1" +mypy = "^2.3.1" +types-openpyxl = "^3.1.5.20260827" + +[tool.pytest.ini_options] +pythonpath = ["src"] +testpaths = ["test"] diff --git a/src/api.py b/src/api.py index d755ea8..7ab9bc0 100644 --- a/src/api.py +++ b/src/api.py @@ -1,11 +1,8 @@ #!/usr/bin/python3 # -.- coding: utf-8 -.- -# -.- dependencies: Python 3.8+ -.- """ Geocoder — provider base and shared types - -Copyright (c) 2026 Pangaea Information Technologies, Ltd. """ import os @@ -14,7 +11,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field from enum import IntEnum -from typing import Any, Callable, Dict, List, Optional, Type +from typing import Any, Callable, Dict, List, Optional, Type, TypeVar import requests @@ -155,7 +152,9 @@ class AccuracyLevel(IntEnum): COUNTRY = (10, "result_country") NONE = (0, "") - def __new__(cls, score, field_name): + field: str + + def __new__(cls, score: int, field_name: str) -> "AccuracyLevel": """Builds a member valued by its score and tagged with its source field.""" member = int.__new__(cls, score) member._value_ = score @@ -275,12 +274,13 @@ def format_street_address(country: str, street: str, number: str = "", subpremis PROVIDERS: Dict[str, Type["Provider"]] = {} +T = TypeVar("T", bound="Provider") -def register(name: str): +def register(name: str) -> Callable[[Type[T]], Type[T]]: """Class decorator that registers a Provider subclass under the given name.""" - def decorator(cls): + def decorator(cls: Type[T]) -> Type[T]: cls.name = name PROVIDERS[name] = cls return cls @@ -333,7 +333,7 @@ def _request_with_retry(self, send: Callable[[], requests.Response]) -> requests requests.RequestException If every attempt fails. """ - last_error = None + last_error: Optional[requests.RequestException] = None for attempt in range(1, self.MAX_ATTEMPTS + 1): try: response = send() @@ -343,6 +343,7 @@ def _request_with_retry(self, send: Callable[[], requests.Response]) -> requests last_error = error if attempt < self.MAX_ATTEMPTS: time.sleep(self.RETRY_BACKOFF * attempt) + assert last_error is not None raise last_error diff --git a/src/census.py b/src/census.py index 49da47d..9d50fbf 100644 --- a/src/census.py +++ b/src/census.py @@ -1,11 +1,8 @@ #!/usr/bin/python3 # -.- coding: utf-8 -.- -# -.- dependencies: Python 3.8+ -.- """ Geocoder — U.S. Census Bureau provider - -Copyright (c) 2026 Pangaea Information Technologies, Ltd. """ import csv diff --git a/src/geocoder.py b/src/geocoder.py index eed9a1d..2bf410e 100644 --- a/src/geocoder.py +++ b/src/geocoder.py @@ -12,7 +12,7 @@ import json import os from dataclasses import dataclass, replace -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence, Tuple from openpyxl import Workbook, load_workbook @@ -126,7 +126,7 @@ def load_dotenv(path: str = ".env") -> None: os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) -def clean_cell(value) -> str: +def clean_cell(value: object) -> str: """Converts the specified cell value into a trimmed string.""" if value is None: return "" @@ -134,12 +134,12 @@ def clean_cell(value) -> str: return str(value).strip() -def cell_value(row, index: Optional[int]) -> str: +def cell_value(row: Sequence[object], index: Optional[int]) -> str: """Reads one trimmed cell from a row, or a blank when the column is absent.""" return clean_cell(row[index]) if index is not None and index < len(row) else "" -def detect_columns(header_cells) -> Dict[str, int]: +def detect_columns(header_cells: Iterable[object]) -> Dict[str, int]: """ Maps canonical field names to their 0-based column index in the header row. @@ -168,7 +168,7 @@ def detect_columns(header_cells) -> Dict[str, int]: return mapping -def read_sheet_records(rows, column_map: Dict[str, int], sheet_name: str, country_per_sheet: bool) -> List[SourceRecord]: +def read_sheet_records(rows: Iterable[Sequence[object]], column_map: Dict[str, int], sheet_name: str, country_per_sheet: bool) -> List[SourceRecord]: """ Builds SourceRecords from the data rows of a worksheet. @@ -191,7 +191,7 @@ def read_sheet_records(rows, column_map: Dict[str, int], sheet_name: str, countr records : List[SourceRecord] One record per non-blank data row. """ - records = [] + records: List[SourceRecord] = [] for row in rows: values = {field_name: cell_value(row, column_map.get(field_name)) for field_name in CANONICAL_FIELDS} @@ -301,7 +301,7 @@ def report_flags(counts: Dict[str, int]) -> None: print(f" {count:>7} {name}") -def finish_sheet(worksheet) -> None: +def finish_sheet(worksheet: Any) -> None: """ Leaves a written sheet ready to triage: header frozen and filters armed. @@ -429,7 +429,9 @@ def process_workbook( sheet_names = [worksheet] if worksheet else source.sheetnames output = Workbook() - output.remove(output.active) + active_sheet = output.active + assert active_sheet is not None + output.remove(active_sheet) counts: Dict[str, int] = {} processed = 0 @@ -466,7 +468,7 @@ def locate_headers(labels: List[str], wanted: Dict[str, str]) -> Dict[str, int]: return {field_name: labels.index(label) for field_name, label in wanted.items() if label in labels} -def detect_output_columns(header_cells) -> Tuple[Dict[str, int], Dict[str, int], Dict[str, int]]: +def detect_output_columns(header_cells: Iterable[object]) -> Tuple[Dict[str, int], Dict[str, int], Dict[str, int]]: """ Locates the source, result, and match-metadata columns this tool wrote. @@ -488,7 +490,7 @@ def detect_output_columns(header_cells) -> Tuple[Dict[str, int], Dict[str, int], def read_output_row( - row, index: int, source_map: Dict[str, int], result_map: Dict[str, int], meta_map: Dict[str, int] + row: Sequence[object], index: int, source_map: Dict[str, int], result_map: Dict[str, int], meta_map: Dict[str, int] ) -> Tuple[SourceRecord, GeocodeResult]: """ Rebuilds the source record and geocoded result a written row was made from. @@ -520,7 +522,7 @@ def read_output_row( """ record = SourceRecord(internal_key=index, **{name.lower(): cell_value(row, source_map.get(name)) for name in CANONICAL_FIELDS}) - values = {RESULT_ATTRIBUTES[name]: cell_value(row, column) for name, column in result_map.items()} + values: Dict[str, Any] = {RESULT_ATTRIBUTES[name]: cell_value(row, column) for name, column in result_map.items()} values.update({META_ATTRIBUTES[name]: cell_value(row, column) for name, column in meta_map.items()}) accuracy = values.get("accuracy", "") values["accuracy"] = int(accuracy) if accuracy.isdigit() else AccuracyLevel.NONE @@ -590,7 +592,9 @@ def check_cells(record: SourceRecord, result: GeocodeResult, flags: Dict[str, st return cells + (compare_record(record, result) if options.compare else []) -def recheck_sheet(rows, sheet_name: str, options: Options) -> Optional[Tuple[List[str], List[List], List[Dict[str, str]]]]: +def recheck_sheet( + rows: Iterator[Sequence[object]], sheet_name: str, options: Options +) -> Optional[Tuple[List[str], List[List[object]], List[Dict[str, str]]]]: """ Rebuilds one worksheet of written output with its check columns refreshed. @@ -668,7 +672,9 @@ def recheck_workbook(infile: str, outfile: str, worksheet: Optional[str] = None, raise SystemExit(f"error: worksheet '{worksheet}' not found in {infile}") output = Workbook() - output.remove(output.active) + active_sheet = output.active + assert active_sheet is not None + output.remove(active_sheet) counts: Dict[str, int] = {} processed = 0 @@ -724,7 +730,7 @@ def holds_output(infile: str, worksheet: Optional[str]) -> bool: source.close() -def parse_args(argv: Optional[List[str]] = None): +def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace: """Takes and parses the command-line arguments given by the user.""" parser = argparse.ArgumentParser(description="Geocode address rows in an Excel workbook.") @@ -774,7 +780,7 @@ def parse_args(argv: Optional[List[str]] = None): return parser.parse_args(argv) -def select_provider(args) -> Optional[Provider]: +def select_provider(args: argparse.Namespace) -> Optional[Provider]: """ Builds the provider named on the command line, or None when it is 'none'. diff --git a/src/geocodio.py b/src/geocodio.py index 8ecd155..3094c3c 100644 --- a/src/geocodio.py +++ b/src/geocodio.py @@ -1,11 +1,8 @@ #!/usr/bin/python3 # -.- coding: utf-8 -.- -# -.- dependencies: Python 3.8+ -.- """ Geocoder — Geocodio provider - -Copyright (c) 2026 Pangaea Information Technologies, Ltd. """ from typing import Dict, List diff --git a/src/google.py b/src/google.py index e455387..103b3d1 100644 --- a/src/google.py +++ b/src/google.py @@ -1,11 +1,8 @@ #!/usr/bin/python3 # -.- coding: utf-8 -.- -# -.- dependencies: Python 3.8+ -.- """ Geocoder — Google Geocoding API provider - -Copyright (c) 2026 Pangaea Information Technologies, Ltd. """ from typing import Dict, List diff --git a/src/postprocess.py b/src/postprocess.py index 4a4bb93..448072a 100644 --- a/src/postprocess.py +++ b/src/postprocess.py @@ -10,7 +10,7 @@ import math import re -from typing import Dict, List, Optional, Set, Tuple +from typing import Dict, List, Optional, Set, Tuple, Union from .api import AccuracyLevel, GeocodeResult, SourceRecord from .flags import format_flags @@ -307,7 +307,7 @@ def summarize_grades(grades: List[str]) -> str: return max(grades, key=lambda grade: GRADE_SEVERITY.get(grade, 0), default="BLANK") -def flag_result(result: GeocodeResult, grades: Dict[str, str], distance) -> Dict[str, str]: +def flag_result(result: GeocodeResult, grades: Dict[str, str], distance: Union[float, str]) -> Dict[str, str]: """ Flags the results whose own metadata says they are worth a second look. @@ -336,9 +336,10 @@ def flag_result(result: GeocodeResult, grades: Dict[str, str], distance) -> Dict flags = {} if result.accuracy and result.accuracy < LOW_ACCURACY: - flags["LOW_ACCURACY"] = f"resolved no finer than {AccuracyLevel(result.accuracy).name.lower()}" + level = next((candidate for candidate in AccuracyLevel if candidate == result.accuracy), AccuracyLevel.NONE) + flags["LOW_ACCURACY"] = f"resolved no finer than {level.name.lower()}" flags.update({flag: "" for field_name, flag in CHANGE_FLAGS.items() if grades.get(field_name) in CHANGED_GRADES}) - if distance != "" and distance > FAR_DISTANCE_M: + if isinstance(distance, float) and distance > FAR_DISTANCE_M: flags["FAR_FROM_SOURCE"] = f"{round(distance / 1000, 1)} km from the source coordinates" return flags diff --git a/src/preprocess.py b/src/preprocess.py index 2b1a575..cbeaeff 100644 --- a/src/preprocess.py +++ b/src/preprocess.py @@ -267,7 +267,7 @@ def _check_postalcode(record: SourceRecord) -> Dict[str, str]: nine-digit U.S. code is a ZIP+4 that lost its hyphen. """ country = country_code(record.country) - pattern = POSTALCODE_PATTERNS.get(country) + pattern = POSTALCODE_PATTERNS.get(country or "") if not record.postalcode or not pattern or pattern.match(record.postalcode): return {} @@ -314,7 +314,7 @@ def _check_coordinates(record: SourceRecord) -> Dict[str, str]: country = country_code(record.country) if not within_country(country, latitude, longitude): - return {"COORDINATES_OUTSIDE_COUNTRY": f"{latitude}, {longitude} is not in {COUNTRY_NAMES[country]}"} + return {"COORDINATES_OUTSIDE_COUNTRY": f"{latitude}, {longitude} is not in {COUNTRY_NAMES[country or '']}"} return {} diff --git a/test/api_test.py b/test/api_test.py index 6a10538..d103611 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -1,11 +1,8 @@ #!/usr/bin/python3 # -.- coding: utf-8 -.- -# -.- dependencies: Python 3.8+ -.- """ Geocoder API Tests - -Copyright (c) 2026 Pangaea Information Technologies, Ltd. """ import pytest diff --git a/test/census_test.py b/test/census_test.py index 2d15142..0dfa443 100644 --- a/test/census_test.py +++ b/test/census_test.py @@ -1,11 +1,8 @@ #!/usr/bin/python3 # -.- coding: utf-8 -.- -# -.- dependencies: Python 3.8+ -.- """ Geocoder Census Provider Tests - -Copyright (c) 2026 Pangaea Information Technologies, Ltd. """ import pytest diff --git a/test/geocoder_test.py b/test/geocoder_test.py index 9e7d19e..f86cea1 100644 --- a/test/geocoder_test.py +++ b/test/geocoder_test.py @@ -1,11 +1,8 @@ #!/usr/bin/python3 # -.- coding: utf-8 -.- -# -.- dependencies: Python 3.8+ -.- """ Geocoder Tests - -Copyright (c) 2026 Pangaea Information Technologies, Ltd. """ import json diff --git a/test/geocodio_test.py b/test/geocodio_test.py index e2f6b9c..959a3c0 100644 --- a/test/geocodio_test.py +++ b/test/geocodio_test.py @@ -1,11 +1,8 @@ #!/usr/bin/python3 # -.- coding: utf-8 -.- -# -.- dependencies: Python 3.8+ -.- """ Geocoder Geocodio Provider Tests - -Copyright (c) 2026 Pangaea Information Technologies, Ltd. """ from test.helpers import FakeResponse, assert_white_house diff --git a/test/google_test.py b/test/google_test.py index 10777bb..4275802 100644 --- a/test/google_test.py +++ b/test/google_test.py @@ -1,11 +1,8 @@ #!/usr/bin/python3 # -.- coding: utf-8 -.- -# -.- dependencies: Python 3.8+ -.- """ Geocoder Google Provider Tests - -Copyright (c) 2026 Pangaea Information Technologies, Ltd. """ from test.helpers import FakeResponse, assert_white_house diff --git a/test/helpers.py b/test/helpers.py index 51febb4..60342b5 100644 --- a/test/helpers.py +++ b/test/helpers.py @@ -1,11 +1,8 @@ #!/usr/bin/python3 # -.- coding: utf-8 -.- -# -.- dependencies: Python 3.8+ -.- """ Geocoder Shared Provider Test Helpers - -Copyright (c) 2026 Pangaea Information Technologies, Ltd. """