diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e910012b3..efebeb25d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,8 +4,12 @@ updates: directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 7 - package-ecosystem: "pip" directory: "/docs" schedule: interval: "weekly" + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb3b232ce..0b46be871 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,44 +2,41 @@ name: CI on: push: + branches-ignore: + - "wip*" + tags: + - "v*" pull_request: - release: - types: [published] schedule: # Daily at 3:21 - cron: "21 3 * * *" + workflow_dispatch: -env: - PIP_DISABLE_PIP_VERSION_CHECK: "1" - PIP_NO_PYTHON_VERSION_WARNING: "1" +permissions: {} jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - uses: pre-commit/action@v3.0.1 - list: runs-on: ubuntu-latest outputs: noxenvs: ${{ steps.noxenvs-matrix.outputs.noxenvs }} steps: - - uses: actions/checkout@v4 - - name: Set up nox - uses: wntrblm/nox@2024.04.15 + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up uv + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 + with: + enable-cache: ${{ github.ref_type != 'tag' }} # zizmor: ignore[cache-poisoning] - id: noxenvs-matrix run: | echo >>$GITHUB_OUTPUT noxenvs=$( - nox --list-sessions --json | jq '[.[].session]' + uvx nox --list-sessions --json | jq '[.[].session]' ) ci: needs: list runs-on: ${{ matrix.os }} + strategy: fail-fast: false matrix: @@ -48,12 +45,24 @@ jobs: posargs: [""] include: - os: ubuntu-latest - noxenv: "tests-3.11(format)" + noxenv: "tests-3.14(format)" posargs: coverage github - os: ubuntu-latest - noxenv: "tests-3.11(no-extras)" + noxenv: "tests-3.14(no-extras)" posargs: coverage github exclude: + - os: macos-latest + noxenv: "docs(dirhtml)" + - os: macos-latest + noxenv: "docs(doctest)" + - os: macos-latest + noxenv: "docs(linkcheck)" + - os: macos-latest + noxenv: "docs(man)" + - os: macos-latest + noxenv: "docs(spelling)" + - os: macos-latest + noxenv: "docs(style)" - os: windows-latest noxenv: "docs(dirhtml)" - os: windows-latest @@ -64,9 +73,23 @@ jobs: noxenv: "docs(spelling)" - os: windows-latest noxenv: "docs(style)" + - os: windows-latest + noxenv: "tests-3.14(no-extras)" + - os: windows-latest + noxenv: "tests-3.14(format)" + - os: windows-latest + noxenv: "tests-3.14(format-nongpl)" + - os: windows-latest + noxenv: "tests-3.14t(no-extras)" + - os: windows-latest + noxenv: "tests-3.14t(format)" + - os: windows-latest + noxenv: "tests-3.14t(format-nongpl)" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libenchant-2-dev if: runner.os == 'Linux' && startsWith(matrix.noxenv, 'docs') @@ -74,23 +97,28 @@ jobs: run: brew install enchant if: runner.os == 'macOS' && startsWith(matrix.noxenv, 'docs') - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: | - 3.8 - 3.9 3.10 3.11 3.12 - pypy3.10 + 3.13 + 3.14 + 3.14t + pypy3.11 allow-prereleases: true - - name: Set up nox - uses: wntrblm/nox@2024.04.15 - name: Enable UTF-8 on Windows run: echo "PYTHONUTF8=1" >> $env:GITHUB_ENV if: runner.os == 'Windows' && startsWith(matrix.noxenv, 'tests') + + - name: Set up uv + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 + with: + enable-cache: ${{ github.ref_type != 'tag' }} # zizmor: ignore[cache-poisoning] + - name: Run nox - run: nox -s "${{ matrix.noxenv }}" -- ${{ matrix.posargs }} + run: uvx nox -s "${{ matrix.noxenv }}" -- ${{ matrix.posargs }} # zizmor: ignore[template-injection] packaging: needs: ci @@ -98,28 +126,30 @@ jobs: environment: name: PyPI url: https://pypi.org/p/jsonschema + permissions: contents: write id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 + persist-credentials: false + - name: Set up uv + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 with: - python-version: "3.x" - - name: Install dependencies - run: python -m pip install build - - name: Create packages - run: python -m build . + enable-cache: ${{ github.ref_type != 'tag' }} # zizmor: ignore[cache-poisoning] + + - name: Build our distributions + run: uv run --frozen --with 'build[uv]' -m build --installer=uv + - name: Publish to PyPI if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@release/v1 - - name: Create a Release + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e + - name: Create a GitHub Release if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b with: files: | dist/* diff --git a/.github/workflows/documentation-links.yml b/.github/workflows/documentation-links.yml deleted file mode 100644 index 5757faf47..000000000 --- a/.github/workflows/documentation-links.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Read the Docs Pull Request Preview -on: - pull_request_target: - types: - - opened - -permissions: - pull-requests: write - -jobs: - documentation-links: - runs-on: ubuntu-latest - steps: - - uses: readthedocs/actions/preview@v1 - with: - project-slug: "python-jsonschema" diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml deleted file mode 100644 index eaabb4fb5..000000000 --- a/.github/workflows/fuzz.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: CIFuzz - -on: - pull_request: - branches: - - main - -jobs: - Fuzzing: - runs-on: ubuntu-latest - steps: - - name: Build Fuzzers - id: build - uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master - with: - oss-fuzz-project-name: "jsonschema" - language: python - continue-on-error: true - - name: Run Fuzzers - if: steps.build.outcome == 'success' - uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master - with: - oss-fuzz-project-name: "jsonschema" - fuzz-seconds: 30 - - name: Upload Crash - uses: actions/upload-artifact@v4 - if: failure() && steps.build.outcome == 'success' - with: - name: artifacts - path: ./out/artifacts diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 000000000..82556d88e --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,35 @@ +name: GitHub Actions Security Analysis with zizmor 🌈 + +on: + push: + branches: ["main"] + pull_request: + branches: ["**"] + +jobs: + zizmor: + name: Run zizmor + runs-on: ubuntu-latest + permissions: + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 + + - name: Run zizmor 🌈 + run: uvx zizmor --format=sarif .github > results.sarif + + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: results.sarif + category: zizmor diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 50b548389..2eebcd398 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: json/ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - id: check-ast - id: check-json @@ -16,16 +16,7 @@ repos: args: [--fix, lf] - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.4.2" + rev: v0.15.8 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v4.0.0-alpha.8" - hooks: - - id: prettier - exclude: "^jsonschema/benchmarks/issue232/issue.json$" diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 31043883e..0cdedb08f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,11 +1,11 @@ version: 2 build: - os: ubuntu-22.04 + os: ubuntu-24.04 apt_packages: - inkscape tools: - python: "3.11" + python: "3.13" sphinx: builder: dirhtml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0da30a6b5..f169513ce 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,36 @@ +v4.26.0 +======= + +* Decrease import time by delaying importing of ``urllib.request`` (#1416). + +v4.25.1 +======= + +* Fix an incorrect required argument in the ``Validator`` protocol's type annotations (#1396). + +v4.25.0 +======= + +* Add support for the ``iri`` and ``iri-reference`` formats to the ``format-nongpl`` extra via the MIT-licensed ``rfc3987-syntax``. + They were alread supported by the ``format`` extra. (#1388). + +v4.24.1 +======= + +* Properly escape segments in ``ValidationError.json_path`` (#139). + +v4.24.0 +======= + +* Fix improper handling of ``unevaluatedProperties`` in the presence of ``additionalProperties`` (#1351). +* Support for Python 3.8 has been dropped, as it is end-of-life. + +v4.23.0 +======= + +* Do not reorder dictionaries (schemas, instances) that are printed as part of validation errors. +* Declare support for Py3.13 + v4.22.0 ======= diff --git a/README.rst b/README.rst index 4889438ab..29381f41e 100644 --- a/README.rst +++ b/README.rst @@ -134,8 +134,6 @@ I'm Julian Berman. Get in touch, via GitHub or otherwise, if you've got something to contribute, it'd be most welcome! -You can also generally find me on Libera (nick: ``Julian``) in various channels, including ``#python``. - If you feel overwhelmingly grateful, you can also `sponsor me `_. And for companies who appreciate ``jsonschema`` and its continued support and growth, ``jsonschema`` is also now supportable via `TideLift `_. diff --git a/docs/conf.py b/docs/conf.py index 4f8d9f157..ec04c0d3f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -87,6 +87,7 @@ def entire_domain(host): linkcheck_ignore = [ + entire_domain("zenodo.org"), entire_domain("img.shields.io"), "https://github.com/python-jsonschema/jsonschema/actions", "https://github.com/python-jsonschema/jsonschema/workflows/CI/badge.svg", diff --git a/docs/errors.rst b/docs/errors.rst index 79c830e9e..9e8046ee6 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -216,8 +216,8 @@ easier debugging. 3 is not valid under any of the given schemas Failed validating 'anyOf' in schema['items']: - {'anyOf': [{'maxLength': 2, 'type': 'string'}, - {'minimum': 5, 'type': 'integer'}]} + {'anyOf': [{'type': 'string', 'maxLength': 2}, + {'type': 'integer', 'minimum': 5}]} On instance[1]: 3 diff --git a/docs/referencing.rst b/docs/referencing.rst index 425cb13a8..223b03363 100644 --- a/docs/referencing.rst +++ b/docs/referencing.rst @@ -220,6 +220,7 @@ Here for instance we retrieve YAML documents in a way similar to the `above `_ file in the check-jsonschema repository. One could similarly imagine a retrieval function which switches on whether to call ``yaml.safe_load`` or ``json.loads`` by file extension (or some more reliable mechanism) and thereby support retrieving references of various different file formats. diff --git a/docs/requirements.in b/docs/requirements.in index 5a68be72b..a3657146b 100644 --- a/docs/requirements.in +++ b/docs/requirements.in @@ -1,13 +1,10 @@ -file:.#egg=jsonschema +file:. furo lxml -sphinx!=7.2.5 +sphinx<9 # sphinx 9 appears to have issues I haven't diagnosed sphinx-autoapi sphinx-autodoc-typehints sphinx-copybutton sphinx-json-schema-spec sphinxcontrib-spelling sphinxext-opengraph - -# Until pyenchant/pyenchant#302 is released... -pyenchant>=3.3.0rc1 diff --git a/docs/requirements.txt b/docs/requirements.txt index 6b44e8ebb..d27e4ca07 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,76 +1,77 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile --strip-extras docs/requirements.in -# -alabaster==0.7.16 +# This file was autogenerated by uv via the following command: +# uv pip compile --output-file /Users/julian/Development/jsonschema/docs/requirements.txt docs/requirements.in +accessible-pygments==0.0.5 + # via furo +alabaster==1.0.0 # via sphinx -anyascii==0.3.2 - # via sphinx-autoapi -astroid==3.1.0 +astroid==4.1.2 # via sphinx-autoapi -attrs==23.2.0 +attrs==26.1.0 # via # jsonschema # referencing -babel==2.14.0 +babel==2.18.0 # via sphinx -beautifulsoup4==4.12.3 +beautifulsoup4==4.14.3 # via furo -certifi==2024.2.2 +certifi==2026.2.25 # via requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.6 # via requests docutils==0.21.2 # via sphinx -furo==2024.4.27 +furo==2025.12.19 # via -r docs/requirements.in -idna==3.7 +idna==3.11 # via requests -imagesize==1.4.1 +imagesize==2.0.0 # via sphinx -jinja2==3.1.3 +jinja2==3.1.6 # via # sphinx # sphinx-autoapi -file:.#egg=jsonschema +jsonschema @ file:. # via -r docs/requirements.in -jsonschema-specifications==2023.12.1 +jsonschema-specifications==2025.9.1 # via jsonschema -lxml==5.2.1 +lxml==6.0.2 # via # -r docs/requirements.in # sphinx-json-schema-spec -markupsafe==2.1.5 +markupsafe==3.0.3 # via jinja2 -packaging==24.0 +packaging==26.0 # via sphinx -pyenchant==3.3.0rc1 - # via - # -r docs/requirements.in - # sphinxcontrib-spelling -pygments==2.17.2 +pyenchant==3.3.0 + # via sphinxcontrib-spelling +pygments==2.19.2 # via + # accessible-pygments # furo # sphinx -pyyaml==6.0.1 +pyyaml==6.0.3 # via sphinx-autoapi -referencing==0.35.0 +referencing==0.37.0 # via # jsonschema # jsonschema-specifications -requests==2.31.0 +requests==2.33.0 + # via + # sphinx + # sphinxcontrib-spelling +roman-numerals==4.1.0 + # via roman-numerals-py +roman-numerals-py==4.1.0 # via sphinx -rpds-py==0.18.0 +rpds-py==0.30.0 # via # jsonschema # referencing -snowballstemmer==2.2.0 +snowballstemmer==3.0.1 # via sphinx -soupsieve==2.5 +soupsieve==2.8.3 # via beautifulsoup4 -sphinx==7.3.7 +sphinx==8.2.3 # via # -r docs/requirements.in # furo @@ -81,31 +82,33 @@ sphinx==7.3.7 # sphinx-json-schema-spec # sphinxcontrib-spelling # sphinxext-opengraph -sphinx-autoapi==3.0.0 +sphinx-autoapi==3.8.0 # via -r docs/requirements.in -sphinx-autodoc-typehints==2.1.0 +sphinx-autodoc-typehints==3.5.2 # via -r docs/requirements.in sphinx-basic-ng==1.0.0b2 # via furo sphinx-copybutton==0.5.2 # via -r docs/requirements.in -sphinx-json-schema-spec==2024.1.1 +sphinx-json-schema-spec==2025.1.1 # via -r docs/requirements.in -sphinxcontrib-applehelp==1.0.8 +sphinxcontrib-applehelp==2.0.0 # via sphinx -sphinxcontrib-devhelp==1.0.6 +sphinxcontrib-devhelp==2.0.0 # via sphinx -sphinxcontrib-htmlhelp==2.0.5 +sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.7 +sphinxcontrib-qthelp==2.0.0 # via sphinx -sphinxcontrib-serializinghtml==1.1.10 +sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sphinxcontrib-spelling==8.0.0 +sphinxcontrib-spelling==8.0.2 # via -r docs/requirements.in -sphinxext-opengraph==0.9.1 +sphinxext-opengraph==0.13.0 # via -r docs/requirements.in -urllib3==2.2.1 +typing-extensions==4.15.0 + # via beautifulsoup4 +urllib3==2.6.3 # via requests diff --git a/docs/spelling-wordlist.txt b/docs/spelling-wordlist.txt index 640d56f73..cf9eaf0b5 100644 --- a/docs/spelling-wordlist.txt +++ b/docs/spelling-wordlist.txt @@ -37,6 +37,7 @@ recurses regex repr runtime +sandboxing sensical subclassing submodule diff --git a/docs/validate.rst b/docs/validate.rst index 91f0577b9..4e287e4a3 100644 --- a/docs/validate.rst +++ b/docs/validate.rst @@ -11,16 +11,19 @@ Schema Validation If you aren't already comfortable with writing schemas and need an introduction which teaches about JSON Schema the specification, you may find :ujs:`Understanding JSON Schema ` to be a good read! - The Basics ---------- -The simplest way to validate an instance under a given schema is to use the -`validate ` function. +The simplest way to validate an instance under a given schema is to use the `validate ` function. .. autofunction:: validate :noindex: +.. warning:: + + Accepting untrusted schemas as input, especially when combined with untrusted data to validate, can lead to vulnerabilities even when restricting to official JSON Schema dialects and vocabularies. + Never validate data against schemas from untrusted sources without proper sandboxing or input validation. + .. _validator-protocol: The Validator Protocol @@ -206,8 +209,6 @@ Or if you want to avoid GPL dependencies, a second extra is available: $ pip install jsonschema[format-nongpl] -At the moment, it supports all the available checkers except for ``iri`` and ``iri-reference``. - .. warning:: It is your own responsibility ultimately to ensure you are license-compliant, so you should be double checking your own dependencies if you rely on this extra. @@ -230,8 +231,8 @@ Checker Notes ``idn-hostname`` requires idna_ ``ipv4`` ``ipv6`` OS must have `socket.inet_pton` function -``iri`` requires rfc3987_ -``iri-reference`` requires rfc3987_ +``iri`` requires rfc3987_ or rfc3987-syntax_ +``iri-reference`` requires rfc3987_ or rfc3987-syntax_ ``json-pointer`` requires jsonpointer_ ``regex`` ``relative-json-pointer`` requires jsonpointer_ @@ -239,6 +240,7 @@ Checker Notes ``uri`` requires rfc3987_ or rfc3986-validator_ ``uri-reference`` requires rfc3987_ or rfc3986-validator_ ``uri-template`` requires uri-template_ +``uuid`` ========================= ==================== @@ -249,6 +251,7 @@ Checker Notes .. _rfc3339-validator: https://pypi.org/project/rfc3339-validator/ .. _rfc3986-validator: https://pypi.org/project/rfc3986-validator/ .. _rfc3987: https://pypi.org/pypi/rfc3987/ +.. _rfc3987-syntax: https://pypi.org/pypi/rfc3987-syntax/ .. _uri-template: https://pypi.org/pypi/uri-template/ .. _webcolors: https://pypi.org/pypi/webcolors/ diff --git a/json/.github/workflows/annotation-tests.yml b/json/.github/workflows/annotation-tests.yml new file mode 100644 index 000000000..16de7b169 --- /dev/null +++ b/json/.github/workflows/annotation-tests.yml @@ -0,0 +1,21 @@ +name: Validate annotation tests + +on: + pull_request: + paths: + - "annotations/**" + +jobs: + annotate: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Deno + uses: denoland/setup-deno@v2 + with: + deno-version: "2.x" + + - name: Validate annotation tests + run: deno --node-modules-dir=auto --allow-read --no-prompt bin/annotation-tests.ts diff --git a/json/.github/workflows/pr-dependencies.yml b/json/.github/workflows/pr-dependencies.yml new file mode 100644 index 000000000..34a231dcb --- /dev/null +++ b/json/.github/workflows/pr-dependencies.yml @@ -0,0 +1,12 @@ +name: Check PR Dependencies + +on: pull_request + +jobs: + check_dependencies: + runs-on: ubuntu-latest + name: Check Dependencies + steps: + - uses: gregsdennis/dependencies-action@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/json/.github/workflows/show_specification_annotations.yml b/json/.github/workflows/show_specification_annotations.yml new file mode 100644 index 000000000..f7d7b398b --- /dev/null +++ b/json/.github/workflows/show_specification_annotations.yml @@ -0,0 +1,21 @@ +name: Show Specification Annotations + +on: + pull_request: + paths: + - 'tests/**' + +jobs: + annotate: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Generate Annotations + run: pip install uritemplate && bin/annotate-specification-links diff --git a/json/README.md b/json/README.md index bfdcb501c..9f4c516db 100644 --- a/json/README.md +++ b/json/README.md @@ -227,6 +227,7 @@ This suite is being used by: ### C++ +* [Blaze](https://github.com/sourcemeta/blaze) * [Modern C++ JSON schema validator](https://github.com/pboettch/json-schema-validator) * [Valijson](https://github.com/tristanpenman/valijson) @@ -293,7 +294,7 @@ Node-specific support is maintained in a [separate repository](https://github.co ### .NET -* [JsonSchema.Net](https://github.com/gregsdennis/json-everything) +* [JsonSchema.Net](https://github.com/json-everything/json-everything) * [Newtonsoft.Json.Schema](https://github.com/JamesNK/Newtonsoft.Json.Schema) ### Perl @@ -339,6 +340,7 @@ Node-specific support is maintained in a [separate repository](https://github.co ### Swift * [JSONSchema](https://github.com/kylef/JSONSchema.swift) +* [swift-json-schema](https://github.com/ajevans99/swift-json-schema) If you use it as well, please fork and send a pull request adding yourself to the list :). diff --git a/json/annotations/README.md b/json/annotations/README.md new file mode 100644 index 000000000..69cd3dd7e --- /dev/null +++ b/json/annotations/README.md @@ -0,0 +1,116 @@ +# Annotations Tests Suite + +The Annotations Test Suite tests which annotations should appear (or not appear) +on which values of an instance. These tests are agnostic of any output format. + +## Supported Dialects + +Although the annotation terminology of didn't appear in the spec until 2019-09, +the concept is compatible with every version of JSON Schema. Test Cases in this +Test Suite are designed to be compatible with as many releases of JSON Schema as +possible. They do not include `$schema` or `$id`/`id` keywords so +implementations can run the same Test Suite for each dialect they support. + +Since this Test Suite can be used for a variety of dialects, there are a couple +of options that can be used by Test Runners to filter out Test Cases that don't +apply to the dialect under test. + +## Test Case Components + +### description + +A short description of what behavior the Test Case is covering. + +### compatibility + +The `compatibility` option allows you to set which dialects the Test Case is +compatible with. Test Runners can use this value to filter out Test Cases that +don't apply the to dialect currently under test. The terminology for annotations +didn't appear in the spec until 2019-09, but the concept is compatible with +older releases as well. When setting `compatibility`, test authors should take +into account dialects before 2019-09 for implementations that chose to support +annotations for older dialects. + +Dialects are indicated by the number corresponding to their release. Date-based +releases use just the year. If this option isn't present, it means the Test Case +is compatible with any dialect. + +If this option is present with a number, the number indicates the minimum +release the Test Case is compatible with. This example indicates that the Test +Case is compatible with draft-07 and up. + +**Example**: `"compatibility": "7"` + +You can use a `<=` operator to indicate that the Test Case is compatible with +releases less then or equal to the given release. This example indicates that +the Test Case is compatible with 2019-09 and under. + +**Example**: `"compatibility": "<=2019"` + +You can use comma-separated values to indicate multiple constraints if needed. +This example indicates that the Test Case is compatible with releases between +draft-06 and 2019-09. + +**Example**: `"compatibility": "6,<=2019"` + +For convenience, you can use the `=` operator to indicate a Test Case is only +compatible with a single release. This example indicates that the Test Case is +compatible only with 2020-12. + +**Example**: `"compatibility": "=2020"` + +### schema + +The schema that will serve as the subject for the tests. Whenever possible, this +schema shouldn't include `$schema` or `id`/`$id` because Test Cases should be +designed to work with as many releases as possible. + +### externalSchemas + +This allows you to define additional schemas that `schema` makes references to. +The value is an object where the keys are retrieval URIs and values are schemas. +Most external schemas aren't self identifying (using `id`/`$id`) and rely on the +retrieval URI for identification. This is done to increase the number of +dialects that the test is compatible with. Because `id` changed to `$id` in +draft-06, if you use `$id`, the test becomes incompatible with draft-03/4 and in +most cases, that's not necessary. + +### tests + +A collection of Tests to run to verify the Test Case. + +## Test Components + +### instance + +The JSON instance to be annotated. + +### assertions + +A collection of assertions that must be true for the test to pass. + +## Assertions Components + +### location + +The instance location. + +### keyword + +The annotating keyword. + +### expected + +A collection of `keyword` annotations expected on the instance at `location`. +`expected` is an object where the keys are schema locations and the values are +the annotation that schema location contributed for the given `keyword`. + +There can be more than one expected annotation because multiple schema locations +could contribute annotations for a single keyword. + +An empty object is an assertion that the annotation must not appear at the +`location` for the `keyword`. + +As a convention for this Test Suite, the `expected` array should be sorted such +that the most recently encountered value for an annotation given top-down +evaluation of the schema comes before previously encountered values. diff --git a/json/annotations/assertion.schema.json b/json/annotations/assertion.schema.json new file mode 100644 index 000000000..882517887 --- /dev/null +++ b/json/annotations/assertion.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + + "type": "object", + "properties": { + "location": { + "markdownDescription": "The instance location.", + "type": "string", + "format": "json-pointer" + }, + "keyword": { + "markdownDescription": "The annotation keyword.", + "type": "string" + }, + "expected": { + "markdownDescription": "An object of schemaLocation/annotations pairs for `keyword` annotations expected on the instance at `location`.", + "type": "object", + "propertyNames": { + "format": "uri" + } + } + }, + "required": ["location", "keyword", "expected"] +} diff --git a/json/annotations/test-case.schema.json b/json/annotations/test-case.schema.json new file mode 100644 index 000000000..6df5f1098 --- /dev/null +++ b/json/annotations/test-case.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + + "type": "object", + "properties": { + "description": { + "markdownDescription": "A short description of what behavior the Test Case is covering.", + "type": "string" + }, + "compatibility": { + "markdownDescription": "Set which dialects the Test Case is compatible with. Examples:\n- `\"7\"` -- draft-07 and above\n- `\"<=2019\"` -- 2019-09 and previous\n- `\"6,<=2019\"` -- Between draft-06 and 2019-09\n- `\"=2020\"` -- 2020-12 only", + "type": "string", + "pattern": "^(<=|=)?([123467]|2019|2020)(,(<=|=)?([123467]|2019|2020))*$" + }, + "schema": { + "markdownDescription": "This schema shouldn't include `$schema` or `id`/`$id` unless necesary for the test because Test Cases should be designed to work with as many releases as possible.", + "type": ["boolean", "object"] + }, + "externalSchemas": { + "markdownDescription": "The keys are retrieval URIs and values are schemas.", + "type": "object", + "patternProperties": { + "": { + "type": ["boolean", "object"] + } + }, + "propertyNames": { + "format": "uri" + } + }, + "tests": { + "markdownDescription": "A collection of Tests to run to verify the Test Case.", + "type": "array", + "items": { "$ref": "./test.schema.json" } + } + }, + "required": ["description", "schema", "tests"] +} diff --git a/json/annotations/test-suite.schema.json b/json/annotations/test-suite.schema.json new file mode 100644 index 000000000..c8b17f0d5 --- /dev/null +++ b/json/annotations/test-suite.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "suite": { + "type": "array", + "items": { "$ref": "./test-case.schema.json" } + } + }, + "required": ["description", "suite"] +} diff --git a/json/annotations/test.schema.json b/json/annotations/test.schema.json new file mode 100644 index 000000000..3581fbfca --- /dev/null +++ b/json/annotations/test.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + + "type": "object", + "properties": { + "instance": { + "markdownDescription": "The JSON instance to be annotated." + }, + "assertions": { + "markdownDescription": "A collection of assertions that must be true for the test to pass.", + "type": "array", + "items": { "$ref": "./assertion.schema.json" } + } + }, + "required": ["instance", "assertions"] +} diff --git a/json/annotations/tests/applicators.json b/json/annotations/tests/applicators.json new file mode 100644 index 000000000..ceb5044f3 --- /dev/null +++ b/json/annotations/tests/applicators.json @@ -0,0 +1,409 @@ +{ + "$schema": "../test-suite.schema.json", + "description": "The applicator vocabulary", + "suite": [ + { + "description": "`properties`, `patternProperties`, and `additionalProperties`", + "compatibility": "3", + "schema": { + "properties": { + "foo": { + "title": "Foo" + } + }, + "patternProperties": { + "^a": { + "title": "Bar" + } + }, + "additionalProperties": { + "title": "Baz" + } + }, + "tests": [ + { + "instance": {}, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": {} + }, + { + "location": "/apple", + "keyword": "title", + "expected": {} + }, + { + "location": "/bar", + "keyword": "title", + "expected": {} + } + ] + }, + { + "instance": { + "foo": {}, + "apple": {}, + "baz": {} + }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": { + "#/properties/foo": "Foo" + } + }, + { + "location": "/apple", + "keyword": "title", + "expected": { + "#/patternProperties/%5Ea": "Bar" + } + }, + { + "location": "/baz", + "keyword": "title", + "expected": { + "#/additionalProperties": "Baz" + } + } + ] + } + ] + }, + { + "description": "`propertyNames` doesn't annotate property values", + "compatibility": "6", + "schema": { + "propertyNames": { + "const": "foo", + "title": "Foo" + } + }, + "tests": [ + { + "instance": { + "foo": 42 + }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": {} + } + ] + } + ] + }, + { + "description": "`prefixItems` and `items`", + "compatibility": "2020", + "schema": { + "prefixItems": [ + { + "title": "Foo" + } + ], + "items": { + "title": "Bar" + } + }, + "tests": [ + { + "instance": [ + "foo", + "bar" + ], + "assertions": [ + { + "location": "/0", + "keyword": "title", + "expected": { + "#/prefixItems/0": "Foo" + } + }, + { + "location": "/1", + "keyword": "title", + "expected": { + "#/items": "Bar" + } + }, + { + "location": "/2", + "keyword": "title", + "expected": {} + } + ] + } + ] + }, + { + "description": "`contains`", + "compatibility": "6", + "schema": { + "contains": { + "type": "number", + "title": "Foo" + } + }, + "tests": [ + { + "instance": [ + "foo", + 42, + true + ], + "assertions": [ + { + "location": "/0", + "keyword": "title", + "expected": {} + }, + { + "location": "/1", + "keyword": "title", + "expected": { + "#/contains": "Foo" + } + }, + { + "location": "/2", + "keyword": "title", + "expected": {} + }, + { + "location": "/3", + "keyword": "title", + "expected": {} + } + ] + } + ] + }, + { + "description": "`allOf`", + "compatibility": "4", + "schema": { + "allOf": [ + { + "title": "Foo" + }, + { + "title": "Bar" + } + ] + }, + "tests": [ + { + "instance": "foo", + "assertions": [ + { + "location": "", + "keyword": "title", + "expected": { + "#/allOf/1": "Bar", + "#/allOf/0": "Foo" + } + } + ] + } + ] + }, + { + "description": "`anyOf`", + "compatibility": "4", + "schema": { + "anyOf": [ + { + "type": "integer", + "title": "Foo" + }, + { + "type": "number", + "title": "Bar" + } + ] + }, + "tests": [ + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "title", + "expected": { + "#/anyOf/1": "Bar", + "#/anyOf/0": "Foo" + } + } + ] + }, + { + "instance": 4.2, + "assertions": [ + { + "location": "", + "keyword": "title", + "expected": { + "#/anyOf/1": "Bar" + } + } + ] + } + ] + }, + { + "description": "`oneOf`", + "compatibility": "4", + "schema": { + "oneOf": [ + { + "type": "string", + "title": "Foo" + }, + { + "type": "number", + "title": "Bar" + } + ] + }, + "tests": [ + { + "instance": "foo", + "assertions": [ + { + "location": "", + "keyword": "title", + "expected": { + "#/oneOf/0": "Foo" + } + } + ] + }, + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "title", + "expected": { + "#/oneOf/1": "Bar" + } + } + ] + } + ] + }, + { + "description": "`not`", + "compatibility": "4", + "schema": { + "title": "Foo", + "not": { + "not": { + "title": "Bar" + } + } + }, + "tests": [ + { + "instance": {}, + "assertions": [ + { + "location": "", + "keyword": "title", + "expected": { + "#": "Foo" + } + } + ] + } + ] + }, + { + "description": "`dependentSchemas`", + "compatibility": "2019", + "schema": { + "dependentSchemas": { + "foo": { + "title": "Foo" + } + } + }, + "tests": [ + { + "instance": { + "foo": 42 + }, + "assertions": [ + { + "location": "", + "keyword": "title", + "expected": { + "#/dependentSchemas/foo": "Foo" + } + } + ] + }, + { + "instance": { + "foo": 42 + }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": {} + } + ] + } + ] + }, + { + "description": "`if`, `then`, and `else`", + "compatibility": "7", + "schema": { + "if": { + "title": "If", + "type": "string" + }, + "then": { + "title": "Then" + }, + "else": { + "title": "Else" + } + }, + "tests": [ + { + "instance": "foo", + "assertions": [ + { + "location": "", + "keyword": "title", + "expected": { + "#/then": "Then", + "#/if": "If" + } + } + ] + }, + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "title", + "expected": { + "#/else": "Else" + } + } + ] + } + ] + } + ] +} diff --git a/json/annotations/tests/content.json b/json/annotations/tests/content.json new file mode 100644 index 000000000..07c17a691 --- /dev/null +++ b/json/annotations/tests/content.json @@ -0,0 +1,121 @@ +{ + "$schema": "../test-suite.schema.json", + "description": "The content vocabulary", + "suite": [ + { + "description": "`contentMediaType` is an annotation for string instances", + "compatibility": "7", + "schema": { + "contentMediaType": "application/json" + }, + "tests": [ + { + "instance": "{ \"foo\": \"bar\" }", + "assertions": [ + { + "location": "", + "keyword": "contentMediaType", + "expected": { + "#": "application/json" + } + } + ] + }, + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "contentMediaType", + "expected": {} + } + ] + } + ] + }, + { + "description": "`contentEncoding` is an annotation for string instances", + "compatibility": "7", + "schema": { + "contentEncoding": "base64" + }, + "tests": [ + { + "instance": "SGVsbG8gZnJvbSBKU09OIFNjaGVtYQ==", + "assertions": [ + { + "location": "", + "keyword": "contentEncoding", + "expected": { + "#": "base64" + } + } + ] + }, + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "contentEncoding", + "expected": {} + } + ] + } + ] + }, + { + "description": "`contentSchema` is an annotation for string instances", + "compatibility": "2019", + "schema": { + "$id": "https://annotations.json-schema.org/test/contentSchema-is-an-annotation", + "contentMediaType": "application/json", + "contentSchema": { "type": "number" } + }, + "tests": [ + { + "instance": "42", + "assertions": [ + { + "location": "", + "keyword": "contentSchema", + "expected": { + "#": { "type": "number" } + } + } + ] + }, + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "contentSchema", + "expected": {} + } + ] + } + ] + }, + { + "description": "`contentSchema` requires `contentMediaType`", + "compatibility": "2019", + "schema": { + "$id": "https://annotations.json-schema.org/test/contentSchema-is-an-annotation", + "contentSchema": { "type": "number" } + }, + "tests": [ + { + "instance": "42", + "assertions": [ + { + "location": "", + "keyword": "contentSchema", + "expected": {} + } + ] + } + ] + } + ] +} diff --git a/json/annotations/tests/core.json b/json/annotations/tests/core.json new file mode 100644 index 000000000..1d8dee556 --- /dev/null +++ b/json/annotations/tests/core.json @@ -0,0 +1,30 @@ +{ + "$schema": "../test-suite.schema.json", + "description": "The core vocabulary", + "suite": [ + { + "description": "`$ref` and `$defs`", + "compatibility": "2019", + "schema": { + "$ref": "#/$defs/foo", + "$defs": { + "foo": { "title": "Foo" } + } + }, + "tests": [ + { + "instance": "foo", + "assertions": [ + { + "location": "", + "keyword": "title", + "expected": { + "#/$defs/foo": "Foo" + } + } + ] + } + ] + } + ] +} diff --git a/json/annotations/tests/format.json b/json/annotations/tests/format.json new file mode 100644 index 000000000..d8cf9a7af --- /dev/null +++ b/json/annotations/tests/format.json @@ -0,0 +1,26 @@ +{ + "$schema": "../test-suite.schema.json", + "description": "The format vocabulary", + "suite": [ + { + "description": "`format` is an annotation", + "schema": { + "format": "email" + }, + "tests": [ + { + "instance": "foo@bar.com", + "assertions": [ + { + "location": "", + "keyword": "format", + "expected": { + "#": "email" + } + } + ] + } + ] + } + ] +} diff --git a/json/annotations/tests/meta-data.json b/json/annotations/tests/meta-data.json new file mode 100644 index 000000000..be99b652f --- /dev/null +++ b/json/annotations/tests/meta-data.json @@ -0,0 +1,150 @@ +{ + "$schema": "../test-suite.schema.json", + "description": "The meta-data vocabulary", + "suite": [ + { + "description": "`title` is an annotation", + "schema": { + "title": "Foo" + }, + "tests": [ + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "title", + "expected": { + "#": "Foo" + } + } + ] + } + ] + }, + { + "description": "`description` is an annotation", + "schema": { + "description": "Foo" + }, + "tests": [ + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "description", + "expected": { + "#": "Foo" + } + } + ] + } + ] + }, + { + "description": "`default` is an annotation", + "schema": { + "default": "Foo" + }, + "tests": [ + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "default", + "expected": { + "#": "Foo" + } + } + ] + } + ] + }, + { + "description": "`deprecated` is an annotation", + "compatibility": "2019", + "schema": { + "deprecated": true + }, + "tests": [ + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "deprecated", + "expected": { + "#": true + } + } + ] + } + ] + }, + { + "description": "`readOnly` is an annotation", + "compatibility": "7", + "schema": { + "readOnly": true + }, + "tests": [ + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "readOnly", + "expected": { + "#": true + } + } + ] + } + ] + }, + { + "description": "`writeOnly` is an annotation", + "compatibility": "7", + "schema": { + "writeOnly": true + }, + "tests": [ + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "writeOnly", + "expected": { + "#": true + } + } + ] + } + ] + }, + { + "description": "`examples` is an annotation", + "compatibility": "6", + "schema": { + "examples": ["Foo", "Bar"] + }, + "tests": [ + { + "instance": "Foo", + "assertions": [ + { + "location": "", + "keyword": "examples", + "expected": { + "#": ["Foo", "Bar"] + } + } + ] + } + ] + } + ] +} diff --git a/json/annotations/tests/unevaluated.json b/json/annotations/tests/unevaluated.json new file mode 100644 index 000000000..9f2db1158 --- /dev/null +++ b/json/annotations/tests/unevaluated.json @@ -0,0 +1,661 @@ +{ + "$schema": "../test-suite.schema.json", + "description": "The unevaluated vocabulary", + "suite": [ + { + "description": "`unevaluatedProperties` alone", + "compatibility": "2019", + "schema": { + "unevaluatedProperties": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": { "foo": 42, "bar": 24 }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": { + "#/unevaluatedProperties": "Unevaluated" + } + }, + { + "location": "/bar", + "keyword": "title", + "expected": { + "#/unevaluatedProperties": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedProperties` with `properties`", + "compatibility": "2019", + "schema": { + "properties": { + "foo": { "title": "Evaluated" } + }, + "unevaluatedProperties": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": { "foo": 42, "bar": 24 }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": { + "#/properties/foo": "Evaluated" + } + }, + { + "location": "/bar", + "keyword": "title", + "expected": { + "#/unevaluatedProperties": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedProperties` with `patternProperties`", + "compatibility": "2019", + "schema": { + "patternProperties": { + "^a": { "title": "Evaluated" } + }, + "unevaluatedProperties": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": { "apple": 42, "bar": 24 }, + "assertions": [ + { + "location": "/apple", + "keyword": "title", + "expected": { + "#/patternProperties/%5Ea": "Evaluated" + } + }, + { + "location": "/bar", + "keyword": "title", + "expected": { + "#/unevaluatedProperties": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedProperties` with `additionalProperties`", + "compatibility": "2019", + "schema": { + "additionalProperties": { "title": "Evaluated" }, + "unevaluatedProperties": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": { "foo": 42, "bar": 24 }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": { + "#/additionalProperties": "Evaluated" + } + }, + { + "location": "/bar", + "keyword": "title", + "expected": { + "#/additionalProperties": "Evaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedProperties` with `dependentSchemas`", + "compatibility": "2019", + "schema": { + "dependentSchemas": { + "foo": { + "properties": { + "bar": { "title": "Evaluated" } + } + } + }, + "unevaluatedProperties": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": { "foo": 42, "bar": 24 }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": { + "#/unevaluatedProperties": "Unevaluated" + } + }, + { + "location": "/bar", + "keyword": "title", + "expected": { + "#/dependentSchemas/foo/properties/bar": "Evaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedProperties` with `if`, `then`, and `else`", + "compatibility": "2019", + "schema": { + "if": { + "properties": { + "foo": { + "type": "string", + "title": "If" + } + } + }, + "then": { + "properties": { + "foo": { "title": "Then" } + } + }, + "else": { + "properties": { + "foo": { "title": "Else" } + } + }, + "unevaluatedProperties": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": { "foo": "", "bar": 42 }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": { + "#/then/properties/foo": "Then", + "#/if/properties/foo": "If" + } + }, + { + "location": "/bar", + "keyword": "title", + "expected": { + "#/unevaluatedProperties": "Unevaluated" + } + } + ] + }, + { + "instance": { "foo": 42, "bar": "" }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": { + "#/else/properties/foo": "Else" + } + }, + { + "location": "/bar", + "keyword": "title", + "expected": { + "#/unevaluatedProperties": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedProperties` with `allOf`", + "compatibility": "2019", + "schema": { + "allOf": [ + { + "properties": { + "foo": { "title": "Evaluated" } + } + } + ], + "unevaluatedProperties": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": { "foo": 42, "bar": 24 }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": { + "#/allOf/0/properties/foo": "Evaluated" + } + }, + { + "location": "/bar", + "keyword": "title", + "expected": { + "#/unevaluatedProperties": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedProperties` with `anyOf`", + "compatibility": "2019", + "schema": { + "anyOf": [ + { + "properties": { + "foo": { "title": "Evaluated" } + } + } + ], + "unevaluatedProperties": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": { "foo": 42, "bar": 24 }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": { + "#/anyOf/0/properties/foo": "Evaluated" + } + }, + { + "location": "/bar", + "keyword": "title", + "expected": { + "#/unevaluatedProperties": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedProperties` with `oneOf`", + "compatibility": "2019", + "schema": { + "oneOf": [ + { + "properties": { + "foo": { "title": "Evaluated" } + } + } + ], + "unevaluatedProperties": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": { "foo": 42, "bar": 24 }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": { + "#/oneOf/0/properties/foo": "Evaluated" + } + }, + { + "location": "/bar", + "keyword": "title", + "expected": { + "#/unevaluatedProperties": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedProperties` with `not`", + "compatibility": "2019", + "schema": { + "not": { + "not": { + "properties": { + "foo": { "title": "Evaluated" } + } + } + }, + "unevaluatedProperties": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": { "foo": 42, "bar": 24 }, + "assertions": [ + { + "location": "/foo", + "keyword": "title", + "expected": { + "#/unevaluatedProperties": "Unevaluated" + } + }, + { + "location": "/bar", + "keyword": "title", + "expected": { + "#/unevaluatedProperties": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedItems` alone", + "compatibility": "2019", + "schema": { + "unevaluatedItems": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": [42, 24], + "assertions": [ + { + "location": "/0", + "keyword": "title", + "expected": { + "#/unevaluatedItems": "Unevaluated" + } + }, + { + "location": "/1", + "keyword": "title", + "expected": { + "#/unevaluatedItems": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedItems` with `prefixItems`", + "compatibility": "2020", + "schema": { + "prefixItems": [{ "title": "Evaluated" }], + "unevaluatedItems": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": [42, 24], + "assertions": [ + { + "location": "/0", + "keyword": "title", + "expected": { + "#/prefixItems/0": "Evaluated" + } + }, + { + "location": "/1", + "keyword": "title", + "expected": { + "#/unevaluatedItems": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedItems` with `contains`", + "compatibility": "2020", + "schema": { + "contains": { + "type": "string", + "title": "Evaluated" + }, + "unevaluatedItems": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": ["foo", 42], + "assertions": [ + { + "location": "/0", + "keyword": "title", + "expected": { + "#/contains": "Evaluated" + } + }, + { + "location": "/1", + "keyword": "title", + "expected": { + "#/unevaluatedItems": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedItems` with `if`, `then`, and `else`", + "compatibility": "2020", + "schema": { + "if": { + "prefixItems": [ + { + "type": "string", + "title": "If" + } + ] + }, + "then": { + "prefixItems": [ + { "title": "Then" } + ] + }, + "else": { + "prefixItems": [ + { "title": "Else" } + ] + }, + "unevaluatedItems": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": ["", 42], + "assertions": [ + { + "location": "/0", + "keyword": "title", + "expected": { + "#/then/prefixItems/0": "Then", + "#/if/prefixItems/0": "If" + } + }, + { + "location": "/1", + "keyword": "title", + "expected": { + "#/unevaluatedItems": "Unevaluated" + } + } + ] + }, + { + "instance": [42, ""], + "assertions": [ + { + "location": "/0", + "keyword": "title", + "expected": { + "#/else/prefixItems/0": "Else" + } + }, + { + "location": "/1", + "keyword": "title", + "expected": { + "#/unevaluatedItems": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedItems` with `allOf`", + "compatibility": "2020", + "schema": { + "allOf": [ + { + "prefixItems": [ + { "title": "Evaluated" } + ] + } + ], + "unevaluatedItems": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": [42, 24], + "assertions": [ + { + "location": "/0", + "keyword": "title", + "expected": { + "#/allOf/0/prefixItems/0": "Evaluated" + } + }, + { + "location": "/1", + "keyword": "title", + "expected": { + "#/unevaluatedItems": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedItems` with `anyOf`", + "compatibility": "2020", + "schema": { + "anyOf": [ + { + "prefixItems": [ + { "title": "Evaluated" } + ] + } + ], + "unevaluatedItems": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": [42, 24], + "assertions": [ + { + "location": "/0", + "keyword": "title", + "expected": { + "#/anyOf/0/prefixItems/0": "Evaluated" + } + }, + { + "location": "/1", + "keyword": "title", + "expected": { + "#/unevaluatedItems": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedItems` with `oneOf`", + "compatibility": "2020", + "schema": { + "oneOf": [ + { + "prefixItems": [ + { "title": "Evaluated" } + ] + } + ], + "unevaluatedItems": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": [42, 24], + "assertions": [ + { + "location": "/0", + "keyword": "title", + "expected": { + "#/oneOf/0/prefixItems/0": "Evaluated" + } + }, + { + "location": "/1", + "keyword": "title", + "expected": { + "#/unevaluatedItems": "Unevaluated" + } + } + ] + } + ] + }, + { + "description": "`unevaluatedItems` with `not`", + "compatibility": "2020", + "schema": { + "not": { + "not": { + "prefixItems": [ + { "title": "Evaluated" } + ] + } + }, + "unevaluatedItems": { "title": "Unevaluated" } + }, + "tests": [ + { + "instance": [42, 24], + "assertions": [ + { + "location": "/0", + "keyword": "title", + "expected": { + "#/unevaluatedItems": "Unevaluated" + } + }, + { + "location": "/1", + "keyword": "title", + "expected": { + "#/unevaluatedItems": "Unevaluated" + } + } + ] + } + ] + } + ] +} diff --git a/json/annotations/tests/unknown.json b/json/annotations/tests/unknown.json new file mode 100644 index 000000000..b0c89003c --- /dev/null +++ b/json/annotations/tests/unknown.json @@ -0,0 +1,27 @@ +{ + "$schema": "../test-suite.schema.json", + "description": "Unknown keywords", + "suite": [ + { + "description": "`unknownKeyword` is an annotation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "x-unknownKeyword": "Foo" + }, + "tests": [ + { + "instance": 42, + "assertions": [ + { + "location": "", + "keyword": "x-unknownKeyword", + "expected": { + "#": "Foo" + } + } + ] + } + ] + } + ] +} diff --git a/json/bin/annotate-specification-links b/json/bin/annotate-specification-links new file mode 100755 index 000000000..963768b43 --- /dev/null +++ b/json/bin/annotate-specification-links @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +Annotate pull requests to the GitHub repository with links to specifications. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any +import json +import re +import sys + +from uritemplate import URITemplate + + +BIN_DIR = Path(__file__).parent +TESTS = BIN_DIR.parent / "tests" +URLS = json.loads(BIN_DIR.joinpath("specification_urls.json").read_text()) + + +def urls(version: str) -> dict[str, URITemplate]: + """ + Retrieve the version-specific URLs for specifications. + """ + for_version = {**URLS["json-schema"][version], **URLS["external"]} + return {k: URITemplate(v) for k, v in for_version.items()} + + +def annotation( + path: Path, + message: str, + line: int = 1, + level: str = "notice", + **kwargs: Any, +) -> str: + """ + Format a GitHub annotation. + + See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions + for full syntax. + """ + + if kwargs: + additional = "," + ",".join(f"{k}={v}" for k, v in kwargs.items()) + else: + additional = "" + + relative = path.relative_to(TESTS.parent) + return f"::{level} file={relative},line={line}{additional}::{message}\n" + + +def line_number_of(path: Path, case: dict[str, Any]) -> int: + """ + Crudely find the line number of a test case. + """ + with path.open() as file: + description = case["description"] + return next( + (i + 1 for i, line in enumerate(file, 1) if description in line), + 1, + ) + +def extract_kind_and_spec(key: str) -> (str, str): + """ + Extracts specification number and kind from the defined key + """ + can_have_spec = ["rfc", "iso"] + if not any(key.startswith(el) for el in can_have_spec): + return key, "" + number = re.search(r"\d+", key) + spec = "" if number is None else number.group(0) + kind = key.removesuffix(spec) + return kind, spec + + +def main(): + # Clear annotations which may have been emitted by a previous run. + sys.stdout.write("::remove-matcher owner=me::\n") + + for version in TESTS.iterdir(): + if version.name in {"draft-next", "latest"}: + continue + + version_urls = urls(version.name) + + for path in version.rglob("*.json"): + try: + contents = json.loads(path.read_text()) + except json.JSONDecodeError as error: + error = annotation( + level="error", + path=path, + line=error.lineno, + col=error.pos + 1, + title=str(error), + message=f"cannot load {path}" + ) + sys.stdout.write(error) + continue + + for test_case in contents: + specifications = test_case.get("specification") + if specifications is not None: + for each in specifications: + quote = each.pop("quote", "") + (key, section), = each.items() + + (kind, spec) = extract_kind_and_spec(key) + + url_template = version_urls[kind] + if url_template is None: + error = annotation( + level="error", + path=path, + line=line_number_of(path, test_case), + title=f"unsupported template '{kind}'", + message=f"cannot find a URL template for '{kind}'" + ) + sys.stdout.write(error) + continue + + url = url_template.expand( + spec=spec, + section=section, + ) + + message = f"{url}\n\n{quote}" if quote else url + sys.stdout.write( + annotation( + path=path, + line=line_number_of(path, test_case), + title="Specification Link", + message=message, + ), + ) + + +if __name__ == "__main__": + main() diff --git a/json/bin/annotation-tests.ts b/json/bin/annotation-tests.ts new file mode 100755 index 000000000..2d3d19326 --- /dev/null +++ b/json/bin/annotation-tests.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env deno +import { validate } from "npm:@hyperjump/json-schema/draft-07"; +import { BASIC } from "npm:@hyperjump/json-schema/experimental"; + +const validateTestSuite = await validate("./annotations/test-suite.schema.json"); + +console.log("Validating annotation tests ..."); + +let isValid = true; +for await (const entry of Deno.readDir("./annotations/tests")) { + if (entry.isFile) { + const json = await Deno.readTextFile(`./annotations/tests/${entry.name}`); + const suite = JSON.parse(json); + + const output = validateTestSuite(suite, BASIC); + + if (output.valid) { + console.log(`\x1b[32m✔\x1b[0m ${entry.name}`); + } else { + isValid = false; + console.log(`\x1b[31m✖\x1b[0m ${entry.name}`); + console.log(output); + } + } +} + +console.log("Done."); + +if (!isValid) { + Deno.exit(1); +} diff --git a/json/bin/specification_urls.json b/json/bin/specification_urls.json new file mode 100644 index 000000000..e1824999a --- /dev/null +++ b/json/bin/specification_urls.json @@ -0,0 +1,34 @@ +{ + "json-schema": { + "draft2020-12": { + "core": "https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#section-{section}", + "validation": "https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-{section}" + }, + "draft2019-09": { + "core": "https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02#rfc.section.{section}", + "validation": "https://json-schema.org/draft/2019-09/draft-handrews-json-schema-validation-02#rfc.section.{section}" + }, + "draft7": { + "core": "https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.{section}", + "validation": "https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01#rfc.section.{section}" + }, + "draft6": { + "core": "https://json-schema.org/draft-06/draft-wright-json-schema-01#rfc.section.{section}", + "validation": "https://json-schema.org/draft-06/draft-wright-json-schema-validation-01#rfc.section.{section}" + }, + "draft4": { + "core": "https://json-schema.org/draft-04/draft-zyp-json-schema-04#rfc.section.{section}", + "validation": "https://json-schema.org/draft-04/draft-fge-json-schema-validation-00#rfc.section.{section}" + }, + "draft3": { + "core": "https://json-schema.org/draft-03/draft-zyp-json-schema-03.pdf" + } + }, + + "external": { + "ecma262": "https://262.ecma-international.org/{section}", + "perl5": "https://perldoc.perl.org/perlre#{section}", + "rfc": "https://www.rfc-editor.org/rfc/rfc{spec}.html#section-{section}", + "iso": "https://www.iso.org/obp/ui" + } +} diff --git a/json/remotes/subSchemas.json b/json/remotes/draft3/subSchemas.json similarity index 100% rename from json/remotes/subSchemas.json rename to json/remotes/draft3/subSchemas.json diff --git a/json/remotes/locationIndependentIdentifierDraft4.json b/json/remotes/draft4/locationIndependentIdentifier.json similarity index 100% rename from json/remotes/locationIndependentIdentifierDraft4.json rename to json/remotes/draft4/locationIndependentIdentifier.json diff --git a/json/remotes/name.json b/json/remotes/draft4/name.json similarity index 100% rename from json/remotes/name.json rename to json/remotes/draft4/name.json diff --git a/json/remotes/draft4/subSchemas.json b/json/remotes/draft4/subSchemas.json new file mode 100644 index 000000000..6e9b3de35 --- /dev/null +++ b/json/remotes/draft4/subSchemas.json @@ -0,0 +1,10 @@ +{ + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } + } +} diff --git a/json/remotes/locationIndependentIdentifierPre2019.json b/json/remotes/draft6/locationIndependentIdentifier.json similarity index 100% rename from json/remotes/locationIndependentIdentifierPre2019.json rename to json/remotes/draft6/locationIndependentIdentifier.json diff --git a/json/remotes/draft6/name.json b/json/remotes/draft6/name.json new file mode 100644 index 000000000..fceacb809 --- /dev/null +++ b/json/remotes/draft6/name.json @@ -0,0 +1,15 @@ +{ + "definitions": { + "orNull": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#" + } + ] + } + }, + "type": "string" +} diff --git a/json/remotes/ref-and-definitions.json b/json/remotes/draft6/ref-and-definitions.json similarity index 74% rename from json/remotes/ref-and-definitions.json rename to json/remotes/draft6/ref-and-definitions.json index e0ee802a9..b80deeb7b 100644 --- a/json/remotes/ref-and-definitions.json +++ b/json/remotes/draft6/ref-and-definitions.json @@ -1,5 +1,5 @@ { - "$id": "http://localhost:1234/ref-and-definitions.json", + "$id": "http://localhost:1234/draft6/ref-and-definitions.json", "definitions": { "inner": { "properties": { diff --git a/json/remotes/draft6/subSchemas.json b/json/remotes/draft6/subSchemas.json new file mode 100644 index 000000000..6e9b3de35 --- /dev/null +++ b/json/remotes/draft6/subSchemas.json @@ -0,0 +1,10 @@ +{ + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } + } +} diff --git a/json/remotes/draft7/locationIndependentIdentifier.json b/json/remotes/draft7/locationIndependentIdentifier.json new file mode 100644 index 000000000..e72815cd5 --- /dev/null +++ b/json/remotes/draft7/locationIndependentIdentifier.json @@ -0,0 +1,11 @@ +{ + "definitions": { + "refToInteger": { + "$ref": "#foo" + }, + "A": { + "$id": "#foo", + "type": "integer" + } + } +} diff --git a/json/remotes/draft7/name.json b/json/remotes/draft7/name.json new file mode 100644 index 000000000..fceacb809 --- /dev/null +++ b/json/remotes/draft7/name.json @@ -0,0 +1,15 @@ +{ + "definitions": { + "orNull": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#" + } + ] + } + }, + "type": "string" +} diff --git a/json/remotes/draft7/ref-and-definitions.json b/json/remotes/draft7/ref-and-definitions.json new file mode 100644 index 000000000..d5929380c --- /dev/null +++ b/json/remotes/draft7/ref-and-definitions.json @@ -0,0 +1,11 @@ +{ + "$id": "http://localhost:1234/draft7/ref-and-definitions.json", + "definitions": { + "inner": { + "properties": { + "bar": { "type": "string" } + } + } + }, + "allOf": [ { "$ref": "#/definitions/inner" } ] +} diff --git a/json/remotes/draft7/subSchemas.json b/json/remotes/draft7/subSchemas.json new file mode 100644 index 000000000..6e9b3de35 --- /dev/null +++ b/json/remotes/draft7/subSchemas.json @@ -0,0 +1,10 @@ +{ + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } + } +} diff --git a/json/tests/draft-next/optional/ecmascript-regex.json b/json/tests/draft-next/optional/ecmascript-regex.json index 272114503..a1a4f9638 100644 --- a/json/tests/draft-next/optional/ecmascript-regex.json +++ b/json/tests/draft-next/optional/ecmascript-regex.json @@ -405,20 +405,6 @@ } ] }, - { - "description": "\\a is not an ECMA 262 control escape", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "$ref": "https://json-schema.org/draft/next/schema" - }, - "tests": [ - { - "description": "when used as a pattern", - "data": { "pattern": "\\a" }, - "valid": false - } - ] - }, { "description": "pattern with non-ASCII digits", "schema": { diff --git a/json/tests/draft-next/optional/format/duration.json b/json/tests/draft-next/optional/format/duration.json index d5adca206..c4aa66bae 100644 --- a/json/tests/draft-next/optional/format/duration.json +++ b/json/tests/draft-next/optional/format/duration.json @@ -46,6 +46,11 @@ "data": "PT1D", "valid": false }, + { + "description": "must start with P", + "data": "4DT12H30M5S", + "valid": false + }, { "description": "no elements present", "data": "P", diff --git a/json/tests/draft-next/optional/format/ecmascript-regex.json b/json/tests/draft-next/optional/format/ecmascript-regex.json new file mode 100644 index 000000000..1e19c2729 --- /dev/null +++ b/json/tests/draft-next/optional/format/ecmascript-regex.json @@ -0,0 +1,16 @@ +[ + { + "description": "\\a is not an ECMA 262 control escape", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "format": "regex" + }, + "tests": [ + { + "description": "when used as a pattern", + "data": "\\a", + "valid": false + } + ] + } +] diff --git a/json/tests/draft-next/optional/format/hostname.json b/json/tests/draft-next/optional/format/hostname.json index bfb306363..bc3a60dcc 100644 --- a/json/tests/draft-next/optional/format/hostname.json +++ b/json/tests/draft-next/optional/format/hostname.json @@ -120,6 +120,16 @@ "description": "single label ending with digit", "data": "hostnam3", "valid": true + }, + { + "description": "empty string", + "data": "", + "valid": false + }, + { + "description": "single dot", + "data": ".", + "valid": false } ] } diff --git a/json/tests/draft-next/optional/format/idn-hostname.json b/json/tests/draft-next/optional/format/idn-hostname.json index 109bf73c9..1061f4243 100644 --- a/json/tests/draft-next/optional/format/idn-hostname.json +++ b/json/tests/draft-next/optional/format/idn-hostname.json @@ -257,7 +257,7 @@ { "description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", - "data": "\u0660\u06f0", + "data": "\u0628\u0660\u06f0", "valid": false }, { @@ -326,6 +326,63 @@ "description": "single label ending with digit", "data": "hostnam3", "valid": true + }, + { + "description": "empty string", + "data": "", + "valid": false + } + ] + }, + { + "description": "validation of separators in internationalized host names", + "specification": [ + {"rfc3490": "3.1", "quote": "Whenever dots are used as label separators, the following characters MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61(halfwidth ideographic full stop)"} + ], + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "format": "idn-hostname" + }, + "tests": [ + { + "description": "single dot", + "data": ".", + "valid": false + }, + { + "description": "single ideographic full stop", + "data": "\u3002", + "valid": false + }, + { + "description": "single fullwidth full stop", + "data": "\uff0e", + "valid": false + }, + { + "description": "single halfwidth ideographic full stop", + "data": "\uff61", + "valid": false + }, + { + "description": "dot as label separator", + "data": "a.b", + "valid": true + }, + { + "description": "ideographic full stop as label separator", + "data": "a\u3002b", + "valid": true + }, + { + "description": "fullwidth full stop as label separator", + "data": "a\uff0eb", + "valid": true + }, + { + "description": "halfwidth ideographic full stop as label separator", + "data": "a\uff61b", + "valid": true } ] } diff --git a/json/tests/draft2019-09/optional/format/duration.json b/json/tests/draft2019-09/optional/format/duration.json index 00d5f47ae..2d515a64a 100644 --- a/json/tests/draft2019-09/optional/format/duration.json +++ b/json/tests/draft2019-09/optional/format/duration.json @@ -46,6 +46,11 @@ "data": "PT1D", "valid": false }, + { + "description": "must start with P", + "data": "4DT12H30M5S", + "valid": false + }, { "description": "no elements present", "data": "P", diff --git a/json/tests/draft2019-09/optional/format/hostname.json b/json/tests/draft2019-09/optional/format/hostname.json index f3b7181c8..24bfdfc5a 100644 --- a/json/tests/draft2019-09/optional/format/hostname.json +++ b/json/tests/draft2019-09/optional/format/hostname.json @@ -120,6 +120,16 @@ "description": "single label ending with digit", "data": "hostnam3", "valid": true + }, + { + "description": "empty string", + "data": "", + "valid": false + }, + { + "description": "single dot", + "data": ".", + "valid": false } ] } diff --git a/json/tests/draft2019-09/optional/format/idn-hostname.json b/json/tests/draft2019-09/optional/format/idn-hostname.json index 072a6b08e..348c504c8 100644 --- a/json/tests/draft2019-09/optional/format/idn-hostname.json +++ b/json/tests/draft2019-09/optional/format/idn-hostname.json @@ -257,7 +257,7 @@ { "description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", - "data": "\u0660\u06f0", + "data": "\u0628\u0660\u06f0", "valid": false }, { @@ -326,6 +326,63 @@ "description": "single label ending with digit", "data": "hostnam3", "valid": true + }, + { + "description": "empty string", + "data": "", + "valid": false + } + ] + }, + { + "description": "validation of separators in internationalized host names", + "specification": [ + {"rfc3490": "3.1", "quote": "Whenever dots are used as label separators, the following characters MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61(halfwidth ideographic full stop)"} + ], + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "format": "idn-hostname" + }, + "tests": [ + { + "description": "single dot", + "data": ".", + "valid": false + }, + { + "description": "single ideographic full stop", + "data": "\u3002", + "valid": false + }, + { + "description": "single fullwidth full stop", + "data": "\uff0e", + "valid": false + }, + { + "description": "single halfwidth ideographic full stop", + "data": "\uff61", + "valid": false + }, + { + "description": "dot as label separator", + "data": "a.b", + "valid": true + }, + { + "description": "ideographic full stop as label separator", + "data": "a\u3002b", + "valid": true + }, + { + "description": "fullwidth full stop as label separator", + "data": "a\uff0eb", + "valid": true + }, + { + "description": "halfwidth ideographic full stop as label separator", + "data": "a\uff61b", + "valid": true } ] } diff --git a/json/tests/draft2019-09/propertyNames.json b/json/tests/draft2019-09/propertyNames.json index b7fecbf71..3b2bb23bb 100644 --- a/json/tests/draft2019-09/propertyNames.json +++ b/json/tests/draft2019-09/propertyNames.json @@ -111,5 +111,58 @@ "valid": true } ] + }, + { + "description": "propertyNames with const", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "propertyNames": {"const": "foo"} + }, + "tests": [ + { + "description": "object with property foo is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with any other property is invalid", + "data": {"bar": 1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames with enum", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "propertyNames": {"enum": ["foo", "bar"]} + }, + "tests": [ + { + "description": "object with property foo is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with property foo and bar is valid", + "data": {"foo": 1, "bar": 1}, + "valid": true + }, + { + "description": "object with any other property is invalid", + "data": {"baz": 1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] } ] diff --git a/json/tests/draft2020-12/optional/ecmascript-regex.json b/json/tests/draft2020-12/optional/ecmascript-regex.json index 23b962e4b..a4d62e0cf 100644 --- a/json/tests/draft2020-12/optional/ecmascript-regex.json +++ b/json/tests/draft2020-12/optional/ecmascript-regex.json @@ -405,20 +405,6 @@ } ] }, - { - "description": "\\a is not an ECMA 262 control escape", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "https://json-schema.org/draft/2020-12/schema" - }, - "tests": [ - { - "description": "when used as a pattern", - "data": { "pattern": "\\a" }, - "valid": false - } - ] - }, { "description": "pattern with non-ASCII digits", "schema": { diff --git a/json/tests/draft2020-12/optional/format/duration.json b/json/tests/draft2020-12/optional/format/duration.json index a3af56ef0..a09fec5ef 100644 --- a/json/tests/draft2020-12/optional/format/duration.json +++ b/json/tests/draft2020-12/optional/format/duration.json @@ -46,6 +46,11 @@ "data": "PT1D", "valid": false }, + { + "description": "must start with P", + "data": "4DT12H30M5S", + "valid": false + }, { "description": "no elements present", "data": "P", diff --git a/json/tests/draft2020-12/optional/format/ecmascript-regex.json b/json/tests/draft2020-12/optional/format/ecmascript-regex.json new file mode 100644 index 000000000..b0648084a --- /dev/null +++ b/json/tests/draft2020-12/optional/format/ecmascript-regex.json @@ -0,0 +1,16 @@ +[ + { + "description": "\\a is not an ECMA 262 control escape", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "format": "regex" + }, + "tests": [ + { + "description": "when used as a pattern", + "data": "\\a", + "valid": false + } + ] + } +] \ No newline at end of file diff --git a/json/tests/draft2020-12/optional/format/hostname.json b/json/tests/draft2020-12/optional/format/hostname.json index 41418dd4a..57827c4d4 100644 --- a/json/tests/draft2020-12/optional/format/hostname.json +++ b/json/tests/draft2020-12/optional/format/hostname.json @@ -120,6 +120,16 @@ "description": "single label ending with digit", "data": "hostnam3", "valid": true + }, + { + "description": "empty string", + "data": "", + "valid": false + }, + { + "description": "single dot", + "data": ".", + "valid": false } ] } diff --git a/json/tests/draft2020-12/optional/format/idn-hostname.json b/json/tests/draft2020-12/optional/format/idn-hostname.json index bc7d92f66..f42ae969b 100644 --- a/json/tests/draft2020-12/optional/format/idn-hostname.json +++ b/json/tests/draft2020-12/optional/format/idn-hostname.json @@ -257,7 +257,7 @@ { "description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", - "data": "\u0660\u06f0", + "data": "\u0628\u0660\u06f0", "valid": false }, { @@ -326,6 +326,63 @@ "description": "single label ending with digit", "data": "hostnam3", "valid": true + }, + { + "description": "empty string", + "data": "", + "valid": false + } + ] + }, + { + "description": "validation of separators in internationalized host names", + "specification": [ + {"rfc3490": "3.1", "quote": "Whenever dots are used as label separators, the following characters MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61(halfwidth ideographic full stop)"} + ], + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "format": "idn-hostname" + }, + "tests": [ + { + "description": "single dot", + "data": ".", + "valid": false + }, + { + "description": "single ideographic full stop", + "data": "\u3002", + "valid": false + }, + { + "description": "single fullwidth full stop", + "data": "\uff0e", + "valid": false + }, + { + "description": "single halfwidth ideographic full stop", + "data": "\uff61", + "valid": false + }, + { + "description": "dot as label separator", + "data": "a.b", + "valid": true + }, + { + "description": "ideographic full stop as label separator", + "data": "a\u3002b", + "valid": true + }, + { + "description": "fullwidth full stop as label separator", + "data": "a\uff0eb", + "valid": true + }, + { + "description": "halfwidth ideographic full stop as label separator", + "data": "a\uff61b", + "valid": true } ] } diff --git a/json/tests/draft2020-12/propertyNames.json b/json/tests/draft2020-12/propertyNames.json index 7ecfb7ec3..b4780088a 100644 --- a/json/tests/draft2020-12/propertyNames.json +++ b/json/tests/draft2020-12/propertyNames.json @@ -44,6 +44,36 @@ } ] }, + { + "description": "propertyNames validation with pattern", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "propertyNames": { "pattern": "^a+$" } + }, + "tests": [ + { + "description": "matching property names valid", + "data": { + "a": {}, + "aa": {}, + "aaa": {} + }, + "valid": true + }, + { + "description": "non-matching property name is invalid", + "data": { + "aaA": {} + }, + "valid": false + }, + { + "description": "object without properties is valid", + "data": {}, + "valid": true + } + ] + }, { "description": "propertyNames with boolean schema true", "schema": { @@ -81,5 +111,58 @@ "valid": true } ] + }, + { + "description": "propertyNames with const", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "propertyNames": {"const": "foo"} + }, + "tests": [ + { + "description": "object with property foo is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with any other property is invalid", + "data": {"bar": 1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames with enum", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "propertyNames": {"enum": ["foo", "bar"]} + }, + "tests": [ + { + "description": "object with property foo is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with property foo and bar is valid", + "data": {"foo": 1, "bar": 1}, + "valid": true + }, + { + "description": "object with any other property is invalid", + "data": {"baz": 1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] } ] diff --git a/json/tests/draft2020-12/unevaluatedProperties.json b/json/tests/draft2020-12/unevaluatedProperties.json index ae29c9eb3..0da38f679 100644 --- a/json/tests/draft2020-12/unevaluatedProperties.json +++ b/json/tests/draft2020-12/unevaluatedProperties.json @@ -3,7 +3,6 @@ "description": "unevaluatedProperties true", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "unevaluatedProperties": true }, "tests": [ @@ -25,7 +24,6 @@ "description": "unevaluatedProperties schema", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "unevaluatedProperties": { "type": "string", "minLength": 3 @@ -57,7 +55,6 @@ "description": "unevaluatedProperties false", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "unevaluatedProperties": false }, "tests": [ @@ -79,7 +76,6 @@ "description": "unevaluatedProperties with adjacent properties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -107,7 +103,6 @@ "description": "unevaluatedProperties with adjacent patternProperties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "patternProperties": { "^foo": { "type": "string" } }, @@ -132,13 +127,9 @@ ] }, { - "description": "unevaluatedProperties with adjacent additionalProperties", + "description": "unevaluatedProperties with adjacent bool additionalProperties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, "additionalProperties": true, "unevaluatedProperties": false }, @@ -160,11 +151,35 @@ } ] }, + { + "description": "unevaluatedProperties with adjacent non-bool additionalProperties", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": { "type": "string" }, + "unevaluatedProperties": false + }, + "tests": [ + { + "description": "with only valid additional properties", + "data": { + "foo": "foo" + }, + "valid": true + }, + { + "description": "with invalid additional properties", + "data": { + "foo": "foo", + "bar": 1 + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties with nested properties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -201,7 +216,6 @@ "description": "unevaluatedProperties with nested patternProperties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -238,7 +252,6 @@ "description": "unevaluatedProperties with nested additionalProperties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -271,7 +284,6 @@ "description": "unevaluatedProperties with nested unevaluatedProperties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -307,7 +319,6 @@ "description": "unevaluatedProperties with anyOf", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -376,7 +387,6 @@ "description": "unevaluatedProperties with oneOf", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -420,7 +430,6 @@ "description": "unevaluatedProperties with not", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -449,7 +458,6 @@ "description": "unevaluatedProperties with if/then/else", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "if": { "properties": { "foo": { "const": "then" } @@ -509,7 +517,6 @@ "description": "unevaluatedProperties with if/then/else, then not defined", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "if": { "properties": { "foo": { "const": "then" } @@ -563,7 +570,6 @@ "description": "unevaluatedProperties with if/then/else, else not defined", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "if": { "properties": { "foo": { "const": "then" } @@ -617,7 +623,6 @@ "description": "unevaluatedProperties with dependentSchemas", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -653,7 +658,6 @@ "description": "unevaluatedProperties with boolean schemas", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -681,7 +685,6 @@ "description": "unevaluatedProperties with $ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "$ref": "#/$defs/bar", "properties": { "foo": { "type": "string" } @@ -719,7 +722,6 @@ "description": "unevaluatedProperties before $ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "unevaluatedProperties": false, "properties": { "foo": { "type": "string" } @@ -773,7 +775,6 @@ "$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", "unevaluatedProperties": false, - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -862,7 +863,6 @@ "description": "nested unevaluatedProperties, outer false, inner true, properties outside", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -895,7 +895,6 @@ "description": "nested unevaluatedProperties, outer false, inner true, properties inside", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "allOf": [ { "properties": { @@ -928,7 +927,6 @@ "description": "nested unevaluatedProperties, outer true, inner false, properties outside", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { "type": "string" } }, @@ -961,7 +959,6 @@ "description": "nested unevaluatedProperties, outer true, inner false, properties inside", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "allOf": [ { "properties": { @@ -994,7 +991,6 @@ "description": "cousin unevaluatedProperties, true and false, true with properties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "allOf": [ { "properties": { @@ -1029,7 +1025,6 @@ "description": "cousin unevaluatedProperties, true and false, false with properties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "allOf": [ { "unevaluatedProperties": true @@ -1065,10 +1060,8 @@ "comment": "see https://stackoverflow.com/questions/66936884/deeply-nested-unevaluatedproperties-and-their-expectations", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "foo": { - "type": "object", "properties": { "bar": { "type": "string" @@ -1117,7 +1110,6 @@ "description": "in-place applicator siblings, allOf has unevaluated", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "allOf": [ { "properties": { @@ -1163,7 +1155,6 @@ "description": "in-place applicator siblings, anyOf has unevaluated", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "allOf": [ { "properties": { @@ -1209,7 +1200,6 @@ "description": "unevaluatedProperties + single cyclic ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": { "x": { "$ref": "#" } }, diff --git a/json/tests/draft3/optional/ecmascript-regex.json b/json/tests/draft3/optional/format/ecmascript-regex.json similarity index 100% rename from json/tests/draft3/optional/ecmascript-regex.json rename to json/tests/draft3/optional/format/ecmascript-regex.json diff --git a/json/tests/draft3/optional/format/host-name.json b/json/tests/draft3/optional/format/host-name.json index d418f3763..9a75c3c20 100644 --- a/json/tests/draft3/optional/format/host-name.json +++ b/json/tests/draft3/optional/format/host-name.json @@ -57,6 +57,11 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "empty string", + "data": "", + "valid": false } ] } diff --git a/json/tests/draft3/refRemote.json b/json/tests/draft3/refRemote.json index 0e4ab53e0..81a6c5116 100644 --- a/json/tests/draft3/refRemote.json +++ b/json/tests/draft3/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, + "schema": {"$ref": "http://localhost:1234/draft3/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" + "$ref": "http://localhost:1234/draft3/subSchemas.json#/definitions/refToInteger" }, "tests": [ { diff --git a/json/tests/draft4/optional/format/hostname.json b/json/tests/draft4/optional/format/hostname.json index a8ecd194f..866a61788 100644 --- a/json/tests/draft4/optional/format/hostname.json +++ b/json/tests/draft4/optional/format/hostname.json @@ -112,6 +112,16 @@ "description": "single label ending with digit", "data": "hostnam3", "valid": true + }, + { + "description": "empty string", + "data": "", + "valid": false + }, + { + "description": "single dot", + "data": ".", + "valid": false } ] } diff --git a/json/tests/draft4/refRemote.json b/json/tests/draft4/refRemote.json index 64a618b89..65e45190c 100644 --- a/json/tests/draft4/refRemote.json +++ b/json/tests/draft4/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, + "schema": {"$ref": "http://localhost:1234/draft4/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" + "$ref": "http://localhost:1234/draft4/subSchemas.json#/definitions/refToInteger" }, "tests": [ { @@ -139,7 +139,7 @@ "id": "http://localhost:1234/object", "type": "object", "properties": { - "name": {"$ref": "name.json#/definitions/orNull"} + "name": {"$ref": "draft4/name.json#/definitions/orNull"} } }, "tests": [ @@ -171,7 +171,7 @@ { "description": "Location-independent identifier in remote ref", "schema": { - "$ref": "http://localhost:1234/locationIndependentIdentifierDraft4.json#/definitions/refToInteger" + "$ref": "http://localhost:1234/draft4/locationIndependentIdentifier.json#/definitions/refToInteger" }, "tests": [ { diff --git a/json/tests/draft6/optional/format/hostname.json b/json/tests/draft6/optional/format/hostname.json index a8ecd194f..866a61788 100644 --- a/json/tests/draft6/optional/format/hostname.json +++ b/json/tests/draft6/optional/format/hostname.json @@ -112,6 +112,16 @@ "description": "single label ending with digit", "data": "hostnam3", "valid": true + }, + { + "description": "empty string", + "data": "", + "valid": false + }, + { + "description": "single dot", + "data": ".", + "valid": false } ] } diff --git a/json/tests/draft6/propertyNames.json b/json/tests/draft6/propertyNames.json index f0788e649..7c7b80006 100644 --- a/json/tests/draft6/propertyNames.json +++ b/json/tests/draft6/propertyNames.json @@ -103,5 +103,52 @@ "valid": true } ] + }, + { + "description": "propertyNames with const", + "schema": {"propertyNames": {"const": "foo"}}, + "tests": [ + { + "description": "object with property foo is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with any other property is invalid", + "data": {"bar": 1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames with enum", + "schema": {"propertyNames": {"enum": ["foo", "bar"]}}, + "tests": [ + { + "description": "object with property foo is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with property foo and bar is valid", + "data": {"foo": 1, "bar": 1}, + "valid": true + }, + { + "description": "object with any other property is invalid", + "data": {"baz": 1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] } ] diff --git a/json/tests/draft6/refRemote.json b/json/tests/draft6/refRemote.json index 28459c4a0..49ead6d1f 100644 --- a/json/tests/draft6/refRemote.json +++ b/json/tests/draft6/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, + "schema": {"$ref": "http://localhost:1234/draft6/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" + "$ref": "http://localhost:1234/draft6/subSchemas.json#/definitions/refToInteger" }, "tests": [ { @@ -139,7 +139,7 @@ "$id": "http://localhost:1234/object", "type": "object", "properties": { - "name": {"$ref": "name.json#/definitions/orNull"} + "name": {"$ref": "draft6/name.json#/definitions/orNull"} } }, "tests": [ @@ -173,7 +173,7 @@ "schema": { "$id": "http://localhost:1234/schema-remote-ref-ref-defs1.json", "allOf": [ - { "$ref": "ref-and-definitions.json" } + { "$ref": "draft6/ref-and-definitions.json" } ] }, "tests": [ @@ -196,7 +196,7 @@ { "description": "Location-independent identifier in remote ref", "schema": { - "$ref": "http://localhost:1234/locationIndependentIdentifierPre2019.json#/definitions/refToInteger" + "$ref": "http://localhost:1234/draft6/locationIndependentIdentifier.json#/definitions/refToInteger" }, "tests": [ { diff --git a/json/tests/draft7/optional/format/hostname.json b/json/tests/draft7/optional/format/hostname.json index a8ecd194f..866a61788 100644 --- a/json/tests/draft7/optional/format/hostname.json +++ b/json/tests/draft7/optional/format/hostname.json @@ -112,6 +112,16 @@ "description": "single label ending with digit", "data": "hostnam3", "valid": true + }, + { + "description": "empty string", + "data": "", + "valid": false + }, + { + "description": "single dot", + "data": ".", + "valid": false } ] } diff --git a/json/tests/draft7/optional/format/idn-hostname.json b/json/tests/draft7/optional/format/idn-hostname.json index dc47f7b5c..5c8cdc77b 100644 --- a/json/tests/draft7/optional/format/idn-hostname.json +++ b/json/tests/draft7/optional/format/idn-hostname.json @@ -254,7 +254,7 @@ { "description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", - "data": "\u0660\u06f0", + "data": "\u0628\u0660\u06f0", "valid": false }, { @@ -318,6 +318,60 @@ "description": "single label ending with digit", "data": "hostnam3", "valid": true + }, + { + "description": "empty string", + "data": "", + "valid": false + } + ] + }, + { + "description": "validation of separators in internationalized host names", + "specification": [ + {"rfc3490": "3.1", "quote": "Whenever dots are used as label separators, the following characters MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61(halfwidth ideographic full stop)"} + ], + "schema": { "format": "idn-hostname" }, + "tests": [ + { + "description": "single dot", + "data": ".", + "valid": false + }, + { + "description": "single ideographic full stop", + "data": "\u3002", + "valid": false + }, + { + "description": "single fullwidth full stop", + "data": "\uff0e", + "valid": false + }, + { + "description": "single halfwidth ideographic full stop", + "data": "\uff61", + "valid": false + }, + { + "description": "dot as label separator", + "data": "a.b", + "valid": true + }, + { + "description": "ideographic full stop as label separator", + "data": "a\u3002b", + "valid": true + }, + { + "description": "fullwidth full stop as label separator", + "data": "a\uff0eb", + "valid": true + }, + { + "description": "halfwidth ideographic full stop as label separator", + "data": "a\uff61b", + "valid": true } ] } diff --git a/json/tests/draft7/propertyNames.json b/json/tests/draft7/propertyNames.json index f0788e649..7c7b80006 100644 --- a/json/tests/draft7/propertyNames.json +++ b/json/tests/draft7/propertyNames.json @@ -103,5 +103,52 @@ "valid": true } ] + }, + { + "description": "propertyNames with const", + "schema": {"propertyNames": {"const": "foo"}}, + "tests": [ + { + "description": "object with property foo is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with any other property is invalid", + "data": {"bar": 1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames with enum", + "schema": {"propertyNames": {"enum": ["foo", "bar"]}}, + "tests": [ + { + "description": "object with property foo is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with property foo and bar is valid", + "data": {"foo": 1, "bar": 1}, + "valid": true + }, + { + "description": "object with any other property is invalid", + "data": {"baz": 1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] } ] diff --git a/json/tests/draft7/refRemote.json b/json/tests/draft7/refRemote.json index 22185d678..450787af6 100644 --- a/json/tests/draft7/refRemote.json +++ b/json/tests/draft7/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, + "schema": {"$ref": "http://localhost:1234/draft7/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" + "$ref": "http://localhost:1234/draft7/subSchemas.json#/definitions/refToInteger" }, "tests": [ { @@ -139,7 +139,7 @@ "$id": "http://localhost:1234/object", "type": "object", "properties": { - "name": {"$ref": "name.json#/definitions/orNull"} + "name": {"$ref": "draft7/name.json#/definitions/orNull"} } }, "tests": [ @@ -173,7 +173,7 @@ "schema": { "$id": "http://localhost:1234/schema-remote-ref-ref-defs1.json", "allOf": [ - { "$ref": "ref-and-definitions.json" } + { "$ref": "draft7/ref-and-definitions.json" } ] }, "tests": [ @@ -196,7 +196,7 @@ { "description": "Location-independent identifier in remote ref", "schema": { - "$ref": "http://localhost:1234/locationIndependentIdentifierPre2019.json#/definitions/refToInteger" + "$ref": "http://localhost:1234/draft7/locationIndependentIdentifier.json#/definitions/refToInteger" }, "tests": [ { diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py index 79924cf7e..d8dec8cfa 100644 --- a/jsonschema/__init__.py +++ b/jsonschema/__init__.py @@ -106,12 +106,12 @@ def __getattr__(name): __all__ = [ - "Draft201909Validator", - "Draft202012Validator", "Draft3Validator", "Draft4Validator", "Draft6Validator", "Draft7Validator", + "Draft201909Validator", + "Draft202012Validator", "FormatChecker", "SchemaError", "TypeChecker", diff --git a/jsonschema/_format.py b/jsonschema/_format.py index 9e4827e08..5efda86e6 100644 --- a/jsonschema/_format.py +++ b/jsonschema/_format.py @@ -13,9 +13,7 @@ _FormatCheckCallable = typing.Callable[[object], bool] #: A format checker callable. _F = typing.TypeVar("_F", bound=_FormatCheckCallable) -_RaisesType = typing.Union[ - typing.Type[Exception], typing.Tuple[typing.Type[Exception], ...], -] +_RaisesType = type[Exception] | tuple[type[Exception], ...] _RE_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$", re.ASCII) @@ -274,6 +272,10 @@ def is_ipv6(instance: object) -> bool: draft7="hostname", draft201909="hostname", draft202012="hostname", + # fqdn.FQDN("") raises a ValueError due to a bug + # however, it's not clear when or if that will be fixed, so catch it + # here for now + raises=ValueError, ) def is_host_name(instance: object) -> bool: if not isinstance(instance, str): @@ -322,6 +324,31 @@ def is_uri_reference(instance: object) -> bool: return True return validate_rfc3986(instance, rule="URI_reference") + with suppress(ImportError): + from rfc3987_syntax import is_valid_syntax as _rfc3987_is_valid_syntax + + @_checks_drafts( + draft7="iri", + draft201909="iri", + draft202012="iri", + raises=ValueError, + ) + def is_iri(instance: object) -> bool: + if not isinstance(instance, str): + return True + return _rfc3987_is_valid_syntax("iri", instance) + + @_checks_drafts( + draft7="iri-reference", + draft201909="iri-reference", + draft202012="iri-reference", + raises=ValueError, + ) + def is_iri_reference(instance: object) -> bool: + if not isinstance(instance, str): + return True + return _rfc3987_is_valid_syntax("iri_reference", instance) + else: @_checks_drafts( @@ -413,20 +440,16 @@ def is_draft3_time(instance: object) -> bool: with suppress(ImportError): - from webcolors import CSS21_NAMES_TO_HEX import webcolors - def is_css_color_code(instance: object) -> bool: - return webcolors.normalize_hex(instance) - @_checks_drafts(draft3="color", raises=(ValueError, TypeError)) def is_css21_color(instance: object) -> bool: - if ( - not isinstance(instance, str) - or instance.lower() in CSS21_NAMES_TO_HEX - ): - return True - return is_css_color_code(instance) + if isinstance(instance, str): + try: + webcolors.name_to_hex(instance) + except ValueError: + webcolors.normalize_hex(instance.lower()) + return True with suppress(ImportError): diff --git a/jsonschema/_types.py b/jsonschema/_types.py index bf25e7e6f..fcfe3f09c 100644 --- a/jsonschema/_types.py +++ b/jsonschema/_types.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Callable, Mapping +from typing import TYPE_CHECKING import numbers from attrs import evolve, field, frozen @@ -8,6 +8,10 @@ from jsonschema.exceptions import UndefinedTypeCheck +if TYPE_CHECKING: + from collections.abc import Callable, Mapping + from typing import Any + # unfortunately, the type of HashTrieMap is generic, and if used as an attrs # converter, the generic type is presented to mypy, which then fails to match @@ -192,7 +196,7 @@ def remove(self, *types) -> TypeChecker: "integer", lambda checker, instance: ( is_integer(checker, instance) - or isinstance(instance, float) and instance.is_integer() + or (isinstance(instance, float) and instance.is_integer()) ), ) draft7_type_checker = draft6_type_checker diff --git a/jsonschema/_typing.py b/jsonschema/_typing.py index d283dc48d..f8dda63a7 100644 --- a/jsonschema/_typing.py +++ b/jsonschema/_typing.py @@ -1,7 +1,8 @@ """ Some (initially private) typing helpers for jsonschema's types. """ -from typing import Any, Callable, Iterable, Protocol, Tuple, Union +from collections.abc import Callable, Iterable +from typing import Any, Protocol import referencing.jsonschema @@ -19,10 +20,10 @@ def __call__( ... -id_of = Callable[[referencing.jsonschema.Schema], Union[str, None]] +id_of = Callable[[referencing.jsonschema.Schema], str | None] ApplicableValidators = Callable[ [referencing.jsonschema.Schema], - Iterable[Tuple[str, Any]], + Iterable[tuple[str, Any]], ] diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index 54d28c041..84a0965e5 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -298,18 +298,18 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): ), ) - for keyword in [ - "properties", "additionalProperties", "unevaluatedProperties", - ]: - if keyword in schema: - schema_value = schema[keyword] - if validator.is_type(schema_value, "boolean") and schema_value: - evaluated_keys += instance.keys() - - elif validator.is_type(schema_value, "object"): - for property in schema_value: - if property in instance: - evaluated_keys.append(property) + properties = schema.get("properties") + if validator.is_type(properties, "object"): + evaluated_keys += properties.keys() & instance.keys() + + for keyword in ["additionalProperties", "unevaluatedProperties"]: + if (subschema := schema.get(keyword)) is None: + continue + evaluated_keys += ( + key + for key, value in instance.items() + if is_valid(validator.descend(value, subschema)) + ) if "patternProperties" in schema: for property in instance: @@ -326,13 +326,12 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): ) for keyword in ["allOf", "oneOf", "anyOf"]: - if keyword in schema: - for subschema in schema[keyword]: - errs = next(validator.descend(instance, subschema), None) - if errs is None: - evaluated_keys += find_evaluated_property_keys_by_schema( - validator, instance, subschema, - ) + for subschema in schema.get(keyword, []): + if not is_valid(validator.descend(instance, subschema)): + continue + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, subschema, + ) if "if" in schema: if validator.evolve(schema=schema["if"]).is_valid(instance): @@ -349,3 +348,8 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): ) return evaluated_keys + + +def is_valid(errs_it): + """Whether there are no errors in the given iterator.""" + return next(errs_it, None) is None diff --git a/jsonschema/benchmarks/import_benchmark.py b/jsonschema/benchmarks/import_benchmark.py new file mode 100644 index 000000000..e3aac70ba --- /dev/null +++ b/jsonschema/benchmarks/import_benchmark.py @@ -0,0 +1,31 @@ +""" +A benchmark which measures the import time of jsonschema +""" + +import subprocess +import sys + + +def import_time(loops): + total_us = 0 + for _ in range(loops): + p = subprocess.run( + [sys.executable, "-X", "importtime", "-c", "import jsonschema"], + stderr=subprocess.PIPE, + stdout=subprocess.DEVNULL, + check=True, + ) + + line = p.stderr.splitlines()[-1] + field = line.split(b"|")[-2].strip() + us = int(field) # microseconds + total_us += us + + # pyperf expects seconds + return total_us / 1_000_000.0 + +if __name__ == "__main__": + from pyperf import Runner + runner = Runner() + + runner.bench_time_func("Import time (cumulative)", import_time) diff --git a/jsonschema/cli.py b/jsonschema/cli.py index cf6298eb0..f3ca4d6ad 100644 --- a/jsonschema/cli.py +++ b/jsonschema/cli.py @@ -4,6 +4,7 @@ from importlib import metadata from json import JSONDecodeError +from pkgutil import resolve_name from textwrap import dedent import argparse import json @@ -11,11 +12,6 @@ import traceback import warnings -try: - from pkgutil import resolve_name -except ImportError: - from pkgutil_resolve_name import resolve_name # type: ignore[no-redef] - from attrs import define, field from jsonschema.exceptions import SchemaError diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index 82d53da6f..2e5d4ca08 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -6,9 +6,9 @@ from collections import defaultdict, deque from pprint import pformat from textwrap import dedent, indent -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar import heapq -import itertools +import re import warnings from attrs import define @@ -17,14 +17,25 @@ from jsonschema import _utils if TYPE_CHECKING: - from collections.abc import Iterable, Mapping, MutableMapping + from collections.abc import Iterable, Mapping, MutableMapping, Sequence + + from jsonschema import _types WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"]) STRONG_MATCHES: frozenset[str] = frozenset() +_JSON_PATH_COMPATIBLE_PROPERTY_PATTERN = re.compile("^[a-zA-Z][a-zA-Z0-9_]*$") + _unset = _utils.Unset() +def _pretty(thing: Any, prefix: str): + """ + Format something for an error message as prettily as we currently can. + """ + return indent(pformat(thing, width=72, sort_dicts=False), prefix).lstrip() + + def __getattr__(name): if name == "RefResolutionError": warnings.warn( @@ -44,17 +55,17 @@ class _Error(Exception): def __init__( self, message: str, - validator=_unset, - path=(), - cause=None, + validator: str = _unset, # type: ignore[assignment] + path: Iterable[str | int] = (), + cause: Exception | None = None, context=(), - validator_value=_unset, - instance=_unset, - schema=_unset, - schema_path=(), - parent=None, - type_checker=_unset, - ): + validator_value: Any = _unset, + instance: Any = _unset, + schema: Mapping[str, Any] | bool = _unset, # type: ignore[assignment] + schema_path: Iterable[str | int] = (), + parent: _Error | None = None, + type_checker: _types.TypeChecker = _unset, # type: ignore[assignment] + ) -> None: super().__init__( message, validator, @@ -82,10 +93,10 @@ def __init__( for error in context: error.parent = self - def __repr__(self): + def __repr__(self) -> str: return f"<{self.__class__.__name__}: {self.message!r}>" - def __str__(self): + def __str__(self) -> str: essential_for_verbose = ( self.validator, self.validator_value, self.instance, self.schema, ) @@ -107,19 +118,19 @@ def __str__(self): {self.message} Failed validating {self.validator!r} in {schema_path}: - {indent(pformat(self.schema, width=72), prefix).lstrip()} + {_pretty(self.schema, prefix=prefix)} On {instance_path}: - {indent(pformat(self.instance, width=72), prefix).lstrip()} + {_pretty(self.instance, prefix=prefix)} """.rstrip(), ) @classmethod - def create_from(cls, other): + def create_from(cls, other: _Error): return cls(**other._contents()) @property - def absolute_path(self): + def absolute_path(self) -> Sequence[str | int]: parent = self.parent if parent is None: return self.relative_path @@ -129,7 +140,7 @@ def absolute_path(self): return path @property - def absolute_schema_path(self): + def absolute_schema_path(self) -> Sequence[str | int]: parent = self.parent if parent is None: return self.relative_schema_path @@ -139,16 +150,23 @@ def absolute_schema_path(self): return path @property - def json_path(self): + def json_path(self) -> str: path = "$" for elem in self.absolute_path: if isinstance(elem, int): path += "[" + str(elem) + "]" - else: + elif _JSON_PATH_COMPATIBLE_PROPERTY_PATTERN.match(elem): path += "." + elem + else: + escaped_elem = elem.replace("\\", "\\\\").replace("'", r"\'") + path += "['" + escaped_elem + "']" return path - def _set(self, type_checker=None, **kwargs): + def _set( + self, + type_checker: _types.TypeChecker | None = None, + **kwargs: Any, + ) -> None: if type_checker is not None and self._type_checker is _unset: self._type_checker = type_checker @@ -163,9 +181,10 @@ def _contents(self): ) return {attr: getattr(self, attr) for attr in attrs} - def _matches_type(self): + def _matches_type(self) -> bool: try: - expected = self.schema["type"] + # We ignore this as we want to simply crash if this happens + expected = self.schema["type"] # type: ignore[index] except (KeyError, TypeError): return False @@ -197,7 +216,7 @@ class SchemaError(_Error): @define(slots=False) -class _RefResolutionError(Exception): +class _RefResolutionError(Exception): # noqa: PLW1641 """ A ref could not be resolved. """ @@ -215,7 +234,7 @@ def __eq__(self, other): return NotImplemented # pragma: no cover -- uncovered but deprecated # noqa: E501 return self._cause == other._cause - def __str__(self): + def __str__(self) -> str: return str(self._cause) @@ -248,10 +267,10 @@ class UndefinedTypeCheck(Exception): A type checker was asked to check a type it did not have registered. """ - def __init__(self, type): + def __init__(self, type: str) -> None: self.type = type - def __str__(self): + def __str__(self) -> str: return f"Type {self.type!r} is unknown to this type checker" @@ -271,10 +290,10 @@ def __str__(self): return dedent( f"""\ Unknown type {self.type!r} for validator with schema: - {indent(pformat(self.schema, width=72), prefix).lstrip()} + {_pretty(self.schema, prefix=prefix)} While checking instance: - {indent(pformat(self.instance, width=72), prefix).lstrip()} + {_pretty(self.instance, prefix=prefix)} """.rstrip(), ) @@ -396,7 +415,7 @@ def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES): def relevance(error): validator = error.validator return ( # prefer errors which are ... - -len(error.path), # 'deeper' and thereby more specific + -len(error.path), # shorter path thereby more general error.path, # earlier (for sibling errors) validator not in weak, # for a non-low-priority keyword validator in strong, # for a high priority keyword @@ -457,11 +476,9 @@ def best_match(errors, key=relevance): set of inputs from version to version if better heuristics are added. """ - errors = iter(errors) - best = next(errors, None) + best = max(errors, key=key, default=None) if best is None: return - best = max(itertools.chain([best], errors), key=key) while best.context: # Calculate the minimum via nsmallest, because we don't recurse if diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py index 39e56d0fa..b6288dcc2 100644 --- a/jsonschema/protocols.py +++ b/jsonschema/protocols.py @@ -7,21 +7,14 @@ from __future__ import annotations -from typing import ( - TYPE_CHECKING, - Any, - ClassVar, - Iterable, - Protocol, - runtime_checkable, -) +from typing import TYPE_CHECKING, Any, ClassVar, Protocol, runtime_checkable # in order for Sphinx to resolve references accurately from type annotations, # it needs to see names like `jsonschema.TypeChecker` # therefore, only import at type-checking time (to avoid circular references), # but use `jsonschema` for any types which will otherwise not be resolvable if TYPE_CHECKING: - from collections.abc import Mapping + from collections.abc import Iterable, Mapping import referencing.jsonschema @@ -115,10 +108,11 @@ class Validator(Protocol): def __init__( self, schema: Mapping | bool, - registry: referencing.jsonschema.SchemaRegistry, + resolver: Any = None, # deprecated format_checker: jsonschema.FormatChecker | None = None, - ) -> None: - ... + *, + registry: referencing.jsonschema.SchemaRegistry = ..., + ) -> None: ... @classmethod def check_schema(cls, schema: Mapping | bool) -> None: diff --git a/jsonschema/tests/_suite.py b/jsonschema/tests/_suite.py index 0da6503c1..d61d38277 100644 --- a/jsonschema/tests/_suite.py +++ b/jsonschema/tests/_suite.py @@ -10,7 +10,6 @@ import json import os import re -import subprocess import sys import unittest @@ -21,11 +20,14 @@ if TYPE_CHECKING: from collections.abc import Iterable, Mapping, Sequence + from referencing.jsonschema import Schema import pyperf from jsonschema.validators import _VALIDATORS import jsonschema +MAGIC_REMOTE_URL = "http://localhost:1234" + _DELIMITERS = re.compile(r"[\W\- ]+") @@ -51,38 +53,7 @@ def _find_suite(): class Suite: _root: Path = field(factory=_find_suite) - _remotes: referencing.jsonschema.SchemaRegistry = field(init=False) - - def __attrs_post_init__(self): - jsonschema_suite = self._root.joinpath("bin", "jsonschema_suite") - argv = [sys.executable, str(jsonschema_suite), "remotes"] - remotes = subprocess.check_output(argv).decode("utf-8") - - resources = json.loads(remotes) - li = "http://localhost:1234/locationIndependentIdentifierPre2019.json" - li4 = "http://localhost:1234/locationIndependentIdentifierDraft4.json" - - registry = Registry().with_resources( - [ - ( - li, - referencing.jsonschema.DRAFT7.create_resource( - contents=resources.pop(li), - ), - ), - ( - li4, - referencing.jsonschema.DRAFT4.create_resource( - contents=resources.pop(li4), - ), - ), - ], - ).with_contents( - resources.items(), - default_specification=referencing.jsonschema.DRAFT202012, - ) - object.__setattr__(self, "_remotes", registry) def benchmark(self, runner: pyperf.Runner): # pragma: no cover for name, Validator in _VALIDATORS.items(): @@ -92,10 +63,18 @@ def benchmark(self, runner: pyperf.Runner): # pragma: no cover ) def version(self, name) -> Version: + Validator = _VALIDATORS[name] + uri: str = Validator.ID_OF(Validator.META_SCHEMA) # type: ignore[assignment] + specification = referencing.jsonschema.specification_with(uri) + + registry = Registry().with_contents( + remotes_in(root=self._root / "remotes", name=name, uri=uri), + default_specification=specification, + ) return Version( name=name, path=self._root / "tests" / name, - remotes=self._remotes, + remotes=registry, ) @@ -187,6 +166,36 @@ def benchmark(self, runner: pyperf.Runner, **kwargs): # pragma: no cover ) +def remotes_in( + root: Path, + name: str, + uri: str, +) -> Iterable[tuple[str, Schema]]: + # This messy logic is because the test suite is terrible at indicating + # what remotes are needed for what drafts, and mixes in schemas which + # have no $schema and which are invalid under earlier versions, in with + # other schemas which are needed for tests. + + for each in root.rglob("*.json"): + schema = json.loads(each.read_text()) + + relative = str(each.relative_to(root)).replace("\\", "/") + + if ( + ( # invalid boolean schema + name in {"draft3", "draft4"} + and each.stem == "tree" + ) or + ( # draft/*.json + "$schema" not in schema + and relative.startswith("draft") + and not relative.startswith(name) + ) + ): + continue + yield f"{MAGIC_REMOTE_URL}/{relative}", schema + + @frozen(repr=False) class _Test: diff --git a/jsonschema/tests/test_cli.py b/jsonschema/tests/test_cli.py index 79d2a1584..bed9f3e4c 100644 --- a/jsonschema/tests/test_cli.py +++ b/jsonschema/tests/test_cli.py @@ -690,7 +690,7 @@ def test_successful_validation_of_just_the_schema_pretty_output(self): ) def test_successful_validation_via_explicit_base_uri(self): - ref_schema_file = tempfile.NamedTemporaryFile(delete=False) + ref_schema_file = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115 ref_schema_file.close() self.addCleanup(os.remove, ref_schema_file.name) @@ -711,7 +711,7 @@ def test_successful_validation_via_explicit_base_uri(self): ) def test_unsuccessful_validation_via_explicit_base_uri(self): - ref_schema_file = tempfile.NamedTemporaryFile(delete=False) + ref_schema_file = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115 ref_schema_file.close() self.addCleanup(os.remove, ref_schema_file.name) @@ -881,11 +881,8 @@ def test_useless_error_format(self): class TestCLIIntegration(TestCase): def test_license(self): - output = subprocess.check_output( - [sys.executable, "-m", "pip", "show", "jsonschema"], - stderr=subprocess.STDOUT, - ) - self.assertIn(b"License: MIT", output) + our_metadata = metadata.metadata("jsonschema") + self.assertEqual(our_metadata.get("License-Expression"), "MIT") def test_version(self): version = subprocess.check_output( diff --git a/jsonschema/tests/test_deprecations.py b/jsonschema/tests/test_deprecations.py index aea922d23..a54b02f38 100644 --- a/jsonschema/tests/test_deprecations.py +++ b/jsonschema/tests/test_deprecations.py @@ -183,7 +183,7 @@ def test_RefResolver(self): self.assertEqual(w.filename, __file__) with self.assertWarnsRegex(DeprecationWarning, message) as w: - from jsonschema.validators import RefResolver # noqa: F401, F811 + from jsonschema.validators import RefResolver # noqa: F401 self.assertEqual(w.filename, __file__) def test_RefResolutionError(self): diff --git a/jsonschema/tests/test_exceptions.py b/jsonschema/tests/test_exceptions.py index 5b3b43621..358b92425 100644 --- a/jsonschema/tests/test_exceptions.py +++ b/jsonschema/tests/test_exceptions.py @@ -1,6 +1,8 @@ from unittest import TestCase import textwrap +import jsonpath_ng + from jsonschema import exceptions from jsonschema.validators import _LATEST_VERSION @@ -319,7 +321,7 @@ def test_boolean_schemas(self): def test_one_error(self): validator = _LATEST_VERSION({"minProperties": 2}) - error, = validator.iter_errors({}) + validator.iter_errors({}) self.assertEqual( exceptions.best_match(validator.iter_errors({})).validator, "minProperties", @@ -648,6 +650,29 @@ def test_uses_pprint(self): validator="maxLength", ) + def test_does_not_reorder_dicts(self): + self.assertShows( + """ + Failed validating 'type' in schema: + {'do': 3, 'not': 7, 'sort': 37, 'me': 73} + + On instance: + {'here': 73, 'too': 37, 'no': 7, 'sorting': 3} + """, + schema={ + "do": 3, + "not": 7, + "sort": 37, + "me": 73, + }, + instance={ + "here": 73, + "too": 37, + "no": 7, + "sorting": 3, + }, + ) + def test_str_works_with_instances_having_overriden_eq_operator(self): """ Check for #164 which rendered exceptions unusable when a @@ -677,3 +702,58 @@ class TestHashable(TestCase): def test_hashable(self): {exceptions.ValidationError("")} {exceptions.SchemaError("")} + + +class TestJsonPathRendering(TestCase): + def validate_json_path_rendering(self, property_name, expected_path): + error = exceptions.ValidationError( + path=[property_name], + message="1", + validator="foo", + instance="i1", + ) + + rendered_json_path = error.json_path + self.assertEqual(rendered_json_path, expected_path) + + re_parsed_name = jsonpath_ng.parse(rendered_json_path).right.fields[0] + self.assertEqual(re_parsed_name, property_name) + + def test_basic(self): + self.validate_json_path_rendering("x", "$.x") + + def test_empty(self): + self.validate_json_path_rendering("", "$['']") + + def test_number(self): + self.validate_json_path_rendering("1", "$['1']") + + def test_period(self): + self.validate_json_path_rendering(".", "$['.']") + + def test_single_quote(self): + self.validate_json_path_rendering("'", r"$['\'']") + + def test_space(self): + self.validate_json_path_rendering(" ", "$[' ']") + + def test_backslash(self): + self.validate_json_path_rendering("\\", r"$['\\']") + + def test_backslash_single_quote(self): + self.validate_json_path_rendering(r"\'", r"$['\\\'']") + + def test_underscore(self): + self.validate_json_path_rendering("_", r"$['_']") + + def test_double_quote(self): + self.validate_json_path_rendering('"', """$['"']""") + + def test_hyphen(self): + self.validate_json_path_rendering("-", "$['-']") + + def test_json_path_injection(self): + self.validate_json_path_rendering("a[0]", "$['a[0]']") + + def test_open_bracket(self): + self.validate_json_path_rendering("[", "$['[']") diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py index 282c1369c..41c982553 100644 --- a/jsonschema/tests/test_jsonschema_test_suite.py +++ b/jsonschema/tests/test_jsonschema_test_suite.py @@ -6,7 +6,6 @@ See https://github.com/json-schema-org/JSON-Schema-Test-Suite for details. """ -import sys from jsonschema.tests._suite import Suite import jsonschema @@ -27,6 +26,11 @@ def skipper(test): return skipper +def ecmascript_regex(test): + if test.subject == "ecmascript-regex": + return "ECMA regex support will be added in #1142." + + def missing_format(Validator): def missing_format(test): # pragma: no cover schema = test.schema @@ -66,18 +70,6 @@ def complex_email_validation(test): )(test) -if sys.version_info < (3, 9): # pragma: no cover - message = "Rejecting leading zeros is 3.9+" - allowed_leading_zeros = skip( - message=message, - subject="ipv4", - description="invalid leading zeroes, as they are treated as octals", - ) -else: - def allowed_leading_zeros(test): # pragma: no cover - return - - def leap_second(test): message = "Leap seconds are unsupported." return skip( @@ -132,7 +124,8 @@ def leap_second(test): Validator=jsonschema.Draft3Validator, format_checker=jsonschema.Draft3Validator.FORMAT_CHECKER, skip=lambda test: ( - missing_format(jsonschema.Draft3Validator)(test) + ecmascript_regex(test) + or missing_format(jsonschema.Draft3Validator)(test) or complex_email_validation(test) ), ) @@ -149,7 +142,7 @@ def leap_second(test): Validator=jsonschema.Draft4Validator, format_checker=jsonschema.Draft4Validator.FORMAT_CHECKER, skip=lambda test: ( - allowed_leading_zeros(test) + ecmascript_regex(test) or leap_second(test) or missing_format(jsonschema.Draft4Validator)(test) or complex_email_validation(test) @@ -167,7 +160,7 @@ def leap_second(test): Validator=jsonschema.Draft6Validator, format_checker=jsonschema.Draft6Validator.FORMAT_CHECKER, skip=lambda test: ( - allowed_leading_zeros(test) + ecmascript_regex(test) or leap_second(test) or missing_format(jsonschema.Draft6Validator)(test) or complex_email_validation(test) @@ -187,7 +180,7 @@ def leap_second(test): Validator=jsonschema.Draft7Validator, format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER, skip=lambda test: ( - allowed_leading_zeros(test) + ecmascript_regex(test) or leap_second(test) or missing_format(jsonschema.Draft7Validator)(test) or complex_email_validation(test) @@ -224,7 +217,7 @@ def leap_second(test): format_checker=jsonschema.Draft201909Validator.FORMAT_CHECKER, skip=lambda test: ( complex_email_validation(test) - or allowed_leading_zeros(test) + or ecmascript_regex(test) or leap_second(test) or missing_format(jsonschema.Draft201909Validator)(test) or complex_email_validation(test) @@ -261,7 +254,7 @@ def leap_second(test): format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER, skip=lambda test: ( complex_email_validation(test) - or allowed_leading_zeros(test) + or ecmascript_regex(test) or leap_second(test) or missing_format(jsonschema.Draft202012Validator)(test) or complex_email_validation(test) diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index 28cc40273..7d8a4c5cd 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -839,9 +839,9 @@ def test_anyOf(self): self.assertEqual(e.schema, schema) self.assertIsNone(e.parent) - self.assertEqual(e.path, deque([])) - self.assertEqual(e.relative_path, deque([])) - self.assertEqual(e.absolute_path, deque([])) + self.assertEqual(e.path, deque()) + self.assertEqual(e.relative_path, deque()) + self.assertEqual(e.absolute_path, deque()) self.assertEqual(e.json_path, "$") self.assertEqual(e.schema_path, deque(["anyOf"])) @@ -858,9 +858,9 @@ def test_anyOf(self): self.assertEqual(e1.schema, schema["anyOf"][0]) self.assertIs(e1.parent, e) - self.assertEqual(e1.path, deque([])) - self.assertEqual(e1.absolute_path, deque([])) - self.assertEqual(e1.relative_path, deque([])) + self.assertEqual(e1.path, deque()) + self.assertEqual(e1.absolute_path, deque()) + self.assertEqual(e1.relative_path, deque()) self.assertEqual(e1.json_path, "$") self.assertEqual(e1.schema_path, deque([0, "minimum"])) @@ -877,9 +877,9 @@ def test_anyOf(self): self.assertEqual(e2.schema, schema["anyOf"][1]) self.assertIs(e2.parent, e) - self.assertEqual(e2.path, deque([])) - self.assertEqual(e2.relative_path, deque([])) - self.assertEqual(e2.absolute_path, deque([])) + self.assertEqual(e2.path, deque()) + self.assertEqual(e2.relative_path, deque()) + self.assertEqual(e2.absolute_path, deque()) self.assertEqual(e2.json_path, "$") self.assertEqual(e2.schema_path, deque([1, "type"])) @@ -911,9 +911,9 @@ def test_type(self): self.assertEqual(e.schema, schema) self.assertIsNone(e.parent) - self.assertEqual(e.path, deque([])) - self.assertEqual(e.relative_path, deque([])) - self.assertEqual(e.absolute_path, deque([])) + self.assertEqual(e.path, deque()) + self.assertEqual(e.relative_path, deque()) + self.assertEqual(e.absolute_path, deque()) self.assertEqual(e.json_path, "$") self.assertEqual(e.schema_path, deque(["type"])) @@ -930,9 +930,9 @@ def test_type(self): self.assertEqual(e1.schema, schema["type"][0]) self.assertIs(e1.parent, e) - self.assertEqual(e1.path, deque([])) - self.assertEqual(e1.relative_path, deque([])) - self.assertEqual(e1.absolute_path, deque([])) + self.assertEqual(e1.path, deque()) + self.assertEqual(e1.relative_path, deque()) + self.assertEqual(e1.absolute_path, deque()) self.assertEqual(e1.json_path, "$") self.assertEqual(e1.schema_path, deque([0, "type"])) @@ -1027,7 +1027,7 @@ def test_multiple_nesting(self): errors = validator.iter_errors(instance) e1, e2, e3, e4, e5, e6 = sorted_errors(errors) - self.assertEqual(e1.path, deque([])) + self.assertEqual(e1.path, deque()) self.assertEqual(e2.path, deque([0])) self.assertEqual(e3.path, deque([1, "bar"])) self.assertEqual(e4.path, deque([1, "bar", "bar"])) @@ -1257,7 +1257,7 @@ def test_propertyNames(self): error.message, "'foo' should not be valid under {'const': 'foo'}", ) - self.assertEqual(error.path, deque([])) + self.assertEqual(error.path, deque()) self.assertEqual(error.json_path, "$") self.assertEqual(error.schema_path, deque(["propertyNames", "not"])) @@ -1272,7 +1272,7 @@ def test_if_then(self): self.assertEqual(error.validator, "const") self.assertEqual(error.message, "13 was expected") - self.assertEqual(error.path, deque([])) + self.assertEqual(error.path, deque()) self.assertEqual(error.json_path, "$") self.assertEqual(error.schema_path, deque(["then", "const"])) @@ -1287,7 +1287,7 @@ def test_if_else(self): self.assertEqual(error.validator, "const") self.assertEqual(error.message, "13 was expected") - self.assertEqual(error.path, deque([])) + self.assertEqual(error.path, deque()) self.assertEqual(error.json_path, "$") self.assertEqual(error.schema_path, deque(["else", "const"])) @@ -1311,7 +1311,7 @@ def test_boolean_schema_False(self): None, 12, False, - deque([]), + deque(), "$", ), ) @@ -1472,7 +1472,7 @@ def test_contains_too_many(self): "maxContains", 2, ["foo", 2, "bar", 4, "baz", "quux"], - deque([]), + deque(), {"contains": {"type": "string"}, "maxContains": 2}, deque(["contains"]), "$", @@ -1502,7 +1502,7 @@ def test_contains_too_few(self): "minContains", 2, ["foo", 2, 4], - deque([]), + deque(), {"contains": {"type": "string"}, "minContains": 2}, deque(["contains"]), "$", @@ -1529,7 +1529,7 @@ def test_contains_none(self): "contains", {"type": "string"}, [2, 4], - deque([]), + deque(), {"contains": {"type": "string"}, "minContains": 2}, deque(["contains"]), "$", @@ -2379,11 +2379,9 @@ def fake_urlopen(url): self.assertEqual(url, "http://bar") yield BytesIO(json.dumps(schema).encode("utf8")) - self.addCleanup(setattr, validators, "urlopen", validators.urlopen) - validators.urlopen = fake_urlopen - - with self.resolver.resolving(ref) as resolved: - pass + with mock.patch("urllib.request.urlopen", new=fake_urlopen): # noqa: SIM117 + with self.resolver.resolving(ref) as resolved: + pass self.assertEqual(resolved, 12) def test_it_retrieves_local_refs_via_urlopen(self): diff --git a/jsonschema/tests/typing/__init__.py b/jsonschema/tests/typing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py b/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py new file mode 100644 index 000000000..63e8bd405 --- /dev/null +++ b/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py @@ -0,0 +1,38 @@ +""" +This module acts as a test that type checkers will allow each validator +class to be assigned to a variable of type `type[Validator]` + +The assignation is only valid if type checkers recognize each Validator +implementation as a valid implementer of the protocol. +""" +from jsonschema.protocols import Validator +from jsonschema.validators import ( + Draft3Validator, + Draft4Validator, + Draft6Validator, + Draft7Validator, + Draft201909Validator, + Draft202012Validator, +) + +my_validator: type[Validator] + +my_validator = Draft3Validator +my_validator = Draft4Validator +my_validator = Draft6Validator +my_validator = Draft7Validator +my_validator = Draft201909Validator +my_validator = Draft202012Validator + + +# in order to confirm that none of the above were incorrectly typed as 'Any' +# ensure that each of these assignments to a non-validator variable requires an +# ignore +none_var: None + +none_var = Draft3Validator # type: ignore[assignment] +none_var = Draft4Validator # type: ignore[assignment] +none_var = Draft6Validator # type: ignore[assignment] +none_var = Draft7Validator # type: ignore[assignment] +none_var = Draft201909Validator # type: ignore[assignment] +none_var = Draft202012Validator # type: ignore[assignment] diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 85c39160d..98ddf6bfb 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -9,7 +9,6 @@ from operator import methodcaller from typing import TYPE_CHECKING from urllib.parse import unquote, urldefrag, urljoin, urlsplit -from urllib.request import urlopen from warnings import warn import contextlib import json @@ -147,7 +146,7 @@ def create( applicable_validators: _typing.ApplicableValidators = methodcaller( "items", ), -): +) -> type[Validator]: """ Create a new validator class. @@ -511,7 +510,7 @@ def is_valid(self, instance, _schema=None): Validator.__name__ = Validator.__qualname__ = f"{safe}Validator" Validator = validates(version)(Validator) # type: ignore[misc] - return Validator + return Validator # type: ignore[return-value] def extend( @@ -857,7 +856,7 @@ def extend( version="draft2020-12", ) -_LATEST_VERSION = Draft202012Validator +_LATEST_VERSION: type[Validator] = Draft202012Validator class _RefResolver: @@ -1225,6 +1224,7 @@ def resolve_remote(self, uri): result = requests.get(uri).json() else: # Otherwise, pass off to urllib and assume utf-8 + from urllib.request import urlopen with urlopen(uri) as url: # noqa: S310 result = json.loads(url.read().decode("utf-8")) @@ -1334,7 +1334,7 @@ def validate(instance, schema, cls=None, *args, **kwargs): # noqa: D417 def validator_for( schema, - default: Validator | _utils.Unset = _UNSET, + default: type[Validator] | _utils.Unset = _UNSET, ) -> type[Validator]: """ Retrieve the validator class appropriate for validating the given schema. @@ -1396,7 +1396,7 @@ class is returned: DefaultValidator = _LATEST_VERSION if default is _UNSET else default if schema is True or schema is False or "$schema" not in schema: - return DefaultValidator + return DefaultValidator # type: ignore[return-value] if schema["$schema"] not in _META_SCHEMAS and default is _UNSET: warn( ( diff --git a/noxfile.py b/noxfile.py index 2c611278b..0817c2389 100644 --- a/noxfile.py +++ b/noxfile.py @@ -6,6 +6,7 @@ ROOT = Path(__file__).parent PACKAGE = ROOT / "jsonschema" +TYPING_TESTS= ROOT / "jsonschema" / "tests" / "typing" BENCHMARKS = PACKAGE / "benchmarks" PYPROJECT = ROOT / "pyproject.toml" CHANGELOG = ROOT / "CHANGELOG.rst" @@ -22,26 +23,29 @@ docs=DOCS / "requirements.txt", ) REQUIREMENTS_IN = [ # this is actually ordered, as files depend on each other - path.parent / f"{path.stem}.in" for path in REQUIREMENTS.values() + (path.parent / f"{path.stem}.in", path) for path in REQUIREMENTS.values() ] NONGPL_LICENSES = [ "Apache Software License", + "Apache-2.0", "BSD License", "ISC License (ISCL)", + "MIT", "MIT License", "Mozilla Public License 2.0 (MPL 2.0)", "Python Software Foundation License", "The Unlicense (Unlicense)", ] -SUPPORTED = ["3.8", "3.9", "3.10", "pypy3.10", "3.11", "3.12"] -LATEST = SUPPORTED[-1] +SUPPORTED = ["3.10", "pypy3.11", "3.11", "3.12", "3.13", "3.14t", "3.14"] +LATEST_STABLE = SUPPORTED[-1] +nox.options.default_venv_backend = "uv|virtualenv" nox.options.sessions = [] -def session(default=True, python=LATEST, **kwargs): # noqa: D103 +def session(default=True, python=LATEST_STABLE, **kwargs): # noqa: D103 def _session(fn): if default: nox.options.sessions.append(kwargs.get("name", fn.__name__)) @@ -58,7 +62,7 @@ def tests(session, installable): """ env = dict(JSON_SCHEMA_TEST_SUITE=str(ROOT / "json")) - session.install("virtue", installable) + session.install("--group=test", installable) if session.posargs and session.posargs[0] == "coverage": if len(session.posargs) > 1 and session.posargs[1] == "github": @@ -94,16 +98,6 @@ def tests(session, installable): session.run("virtue", *session.posargs, PACKAGE, env=env) -@session() -@nox.parametrize("installable", INSTALLABLE) -def audit(session, installable): - """ - Audit dependencies for vulnerabilities. - """ - session.install("pip-audit", installable) - session.run("python", "-m", "pip_audit") - - @session() def license_check(session): """ @@ -115,9 +109,21 @@ def license_check(session): "-m", "piplicenses", "--ignore-packages", + + # because they're not our deps "pip-requirements-parser", "pip_audit", "pip-api", + + # because pip-licenses doesn't yet support PEP 639 :/ + "attrs", + "idna", + "jsonschema", + "jsonschema-specifications", + "referencing", + "rpds-py", + "types-python-dateutil", + "--allow-only", ";".join(NONGPL_LICENSES), ) @@ -128,9 +134,15 @@ def build(session): """ Build a distribution suitable for PyPI and check its validity. """ - session.install("build", "docutils", "twine") + session.install("build[uv]", "docutils", "twine") with TemporaryDirectory() as tmpdir: - session.run("python", "-m", "build", ROOT, "--outdir", tmpdir) + session.run( + "pyproject-build", + "--installer=uv", + ROOT, + "--outdir", + tmpdir, + ) session.run("twine", "check", "--strict", tmpdir + "/*") session.run( "python", "-m", "docutils", "--strict", CHANGELOG, os.devnull, @@ -143,7 +155,7 @@ def secrets(session): Check for accidentally committed secrets. """ session.install("detect-secrets") - session.run("detect-secrets", "scan", ROOT) + session.run("detect-secrets", "scan", ROOT, "--exclude-files", "json/") @session(tags=["style"]) @@ -162,6 +174,9 @@ def typing(session): """ session.install("mypy", "types-requests", ROOT) session.run("mypy", "--config", PYPROJECT, PACKAGE) + session.run( + "mypy", "--config", PYPROJECT, "--warn-unused-ignores", TYPING_TESTS, + ) @session(tags=["docs"]) @@ -215,6 +230,7 @@ def docs_style(session): @session(default=False) +@nox.parametrize("installable", INSTALLABLE) @nox.parametrize( "benchmark", [ @@ -222,11 +238,11 @@ def docs_style(session): for each in BENCHMARKS.glob("[!_]*.py") ], ) -def bench(session, benchmark): +def bench(session, installable, benchmark): """ Run a performance benchmark. """ - session.install("pyperf", f"{ROOT}[format]") + session.install("pyperf", installable) tmpdir = Path(session.create_tmp()) output = tmpdir / f"bench-{benchmark}.json" session.run("python", BENCHMARKS / f"{benchmark}.py", "--output", output) @@ -239,13 +255,13 @@ def requirements(session): You should commit the result afterwards. """ - session.install("pip-tools") - for each in REQUIREMENTS_IN: - session.run( - "pip-compile", - "--resolver", - "backtracking", - "--strip-extras", - "-U", - each.relative_to(ROOT), - ) + if session.venv_backend == "uv": + cmd = ["uv", "pip", "compile"] + else: + session.install("pip-tools") + cmd = ["pip-compile", "--resolver", "backtracking", "--strip-extras"] + + for each, out in REQUIREMENTS_IN: + # otherwise output files end up with silly absolute path comments... + relative = each.relative_to(ROOT) + session.run(*cmd, "--upgrade", "--output-file", out, relative) diff --git a/pyproject.toml b/pyproject.toml index 45dbc8c4f..acc243121 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,14 +8,15 @@ source = "vcs" [project] name = "jsonschema" description = "An implementation of JSON Schema validation for Python" -requires-python = ">=3.8" -license = {text = "MIT"} +requires-python = ">=3.10" +license = "MIT" +license-files = ["COPYING"] keywords = [ - "validation", - "data validation", - "jsonschema", - "json", - "json schema", + "validation", + "data validation", + "jsonschema", + "json", + "json schema", ] authors = [ { name = "Julian Berman", email = "Julian+jsonschema@GrayVines.com" }, @@ -23,14 +24,13 @@ authors = [ classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: File Formats :: JSON", @@ -41,10 +41,7 @@ dependencies = [ "attrs>=22.2.0", "jsonschema-specifications>=2023.03.6", "referencing>=0.28.4", - "rpds-py>=0.7.1", - - "importlib_resources>=1.4.0;python_version<'3.9'", - "pkgutil_resolve_name>=1.3.10;python_version<'3.9'", + "rpds-py>=0.25.0", ] [project.optional-dependencies] @@ -65,8 +62,9 @@ format-nongpl = [ "jsonpointer>1.13", "rfc3339-validator", "rfc3986-validator>0.1.0", + "rfc3987-syntax>=1.1.0", "uri_template", - "webcolors>=1.11", + "webcolors>=24.6.0", ] [project.scripts] @@ -81,6 +79,9 @@ Tidelift = "https://tidelift.com/subscription/pkg/pypi-jsonschema?utm_source=pyp Changelog = "https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst" Source = "https://github.com/python-jsonschema/jsonschema" +[dependency-groups] +test = ["virtue", "jsonpath-ng"] + [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/x-rst" @@ -129,17 +130,10 @@ skip_covered = true [tool.doc8] ignore = [ - "D000", # see PyCQA/doc8#125 - "D001", # one sentence per line, so max length doesn't make sense + "D000", # see PyCQA/doc8#125 + "D001", # one sentence per line, so max length doesn't make sense ] -[tool.isort] -combine_as_imports = true -ensure_newline_before_comments = true -from_first = true -include_trailing_comma = true -multi_line_output = 3 - [tool.mypy] ignore_missing_imports = true show_error_codes = true @@ -152,51 +146,53 @@ extend-exclude = ["json"] [tool.ruff.lint] select = ["ALL"] ignore = [ - "A001", # It's fine to shadow builtins + "A001", # It's fine to shadow builtins "A002", "A003", - "ARG", # This is all wrong whenever an interface is involved - "ANN", # Just let the type checker do this - "B006", # Mutable arguments require care but are OK if you don't abuse them - "B008", # It's totally OK to call functions for default arguments. - "B904", # raise SomeException(...) is fine. - "B905", # No need for explicit strict, this is simply zip's default behavior - "C408", # Calling dict is fine when it saves quoting the keys - "C901", # Not really something to focus on - "D105", # It's fine to not have docstrings for magic methods. - "D107", # __init__ especially doesn't need a docstring - "D200", # This rule makes diffs uglier when expanding docstrings - "D203", # No blank lines before docstrings. - "D212", # Start docstrings on the second line. - "D400", # This rule misses sassy docstrings ending with ! or ? - "D401", # This rule is too flaky. - "D406", # Section headers should end with a colon not a newline - "D407", # Underlines aren't needed - "D412", # Plz spaces after section headers - "EM101", # These don't bother me. + "A005", + "ARG", # This is all wrong whenever an interface is involved + "ANN", # Just let the type checker do this + "B006", # Mutable arguments require care but are OK if you don't abuse them + "B008", # It's totally OK to call functions for default arguments. + "B904", # raise SomeException(...) is fine. + "B905", # No need for explicit strict, this is simply zip's default behavior + "C408", # Calling dict is fine when it saves quoting the keys + "C901", # Not really something to focus on + "D105", # It's fine to not have docstrings for magic methods. + "D107", # __init__ especially doesn't need a docstring + "D200", # This rule makes diffs uglier when expanding docstrings + "D203", # No blank lines before docstrings. + "D212", # Start docstrings on the second line. + "D400", # This rule misses sassy docstrings ending with ! or ? + "D401", # This rule is too flaky. + "D406", # Section headers should end with a colon not a newline + "D407", # Underlines aren't needed + "D412", # Plz spaces after section headers + "EM101", # These don't bother me, it's fine there's some duplication. "EM102", - "FBT", # It's worth avoiding boolean args but I don't care to enforce it - "FIX", # Yes thanks, if I could it wouldn't be there - "N", # These naming rules are silly - "PERF203", # try/excepts in loops are sometimes needed - "PLR0911", # These metrics are fine to be aware of but not to enforce + "FBT", # It's worth avoiding boolean args but I don't care to enforce it + "FIX", # Yes thanks, if I could it wouldn't be there + "N", # These naming rules are silly + "PERF203", # try/excepts in loops are sometimes needed + "PLC0415", # too noisy, there are too many cases this is fine + "PLR0911", # These metrics are fine to be aware of but not to enforce "PLR0912", "PLR0913", "PLR0915", - "PLR1714", # This makes for uglier comparisons sometimes - "PLW2901", # Shadowing for loop variables is occasionally fine. - "PT", # We use unittest + "PLR1714", # This makes for uglier comparisons sometimes + "PLW0642", # Shadowing self also isn't a big deal. + "PLW2901", # Shadowing for loop variables is occasionally fine. + "PT", # We use unittest "PYI025", # wat, I'm not confused, thanks. "RET502", # Returning None implicitly is fine "RET503", "RET505", # These push you to use `if` instead of `elif`, but for no reason "RET506", "RSE102", # Ha, what, who even knew you could leave the parens off. But no. - "SIM300", # Not sure what heuristic this uses, but it's easily incorrect + "SIM300", # Not sure what heuristic this uses, but it's easily incorrect "SLF001", # Private usage within this package itself is fine - "TD", # These TODO style rules are also silly + "TD", # These TODO style rules are also silly "TRY003", # Some exception classes are essentially intended for free-form - "UP007", # We support 3.8 + 3.9 ] [tool.ruff.lint.flake8-pytest-style] @@ -212,6 +208,15 @@ from-first = true [tool.ruff.lint.per-file-ignores] "noxfile.py" = ["ANN", "D100", "S101", "T201"] "docs/*" = ["ANN", "D", "INP001"] -"jsonschema/tests/*" = ["ANN", "D", "RUF012", "S", "PLR", "PYI024", "TRY"] +"jsonschema/tests/*" = [ + "ANN", + "D", + "RUF012", + "S", + "PLR", + "PLW1641", + "PYI024", + "TRY", +] "jsonschema/tests/test_format.py" = ["ERA001"] "jsonschema/benchmarks/*" = ["ANN", "D", "INP001", "S101"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..e450c3f57 --- /dev/null +++ b/uv.lock @@ -0,0 +1,626 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "automat" +version = "25.4.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/0f/d40bbe294bbf004d436a8bcbcfaadca8b5140d39ad0ad3d73d1a8ba15f14/automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0", size = 129977, upload-time = "2025-04-16T20:12:16.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "constantly" +version = "23.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/cb2a94494ff74aa9528a36c5b1422756330a75a8367bf20bd63171fc324d/constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd", size = 13300, upload-time = "2023-10-28T23:18:24.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547, upload-time = "2023-10-28T23:18:23.038Z" }, +] + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "incremental" +version = "24.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/3c/82e84109e02c492f382c711c58a3dd91badda6d746def81a1465f74dc9f5/incremental-24.11.0.tar.gz", hash = "sha256:87d3480dbb083c1d736222511a8cf380012a8176c2456d01ef483242abbbcf8c", size = 24000, upload-time = "2025-11-28T02:30:17.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/55/0f4df2a44053867ea9cbea73fc588b03c55605cd695cee0a3d86f0029cb2/incremental-24.11.0-py3-none-any.whl", hash = "sha256:a34450716b1c4341fe6676a0598e88a39e04189f4dce5dc96f656e040baa10b3", size = 21109, upload-time = "2025-11-28T02:30:16.442Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "jsonpath-ng" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/32/58/250751940d75c8019659e15482d548a4aa3b6ce122c515102a4bfdac50e3/jsonpath_ng-1.8.0.tar.gz", hash = "sha256:54252968134b5e549ea5b872f1df1168bd7defe1a52fed5a358c194e1943ddc3", size = 74513, upload-time = "2026-02-24T14:42:06.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/99/33c7d78a3fb70d545fd5411ac67a651c81602cc09c9cf0df383733f068c5/jsonpath_ng-1.8.0-py3-none-any.whl", hash = "sha256:b8dde192f8af58d646fc031fac9c99fe4d00326afc4148f1f043c601a8cfe138", size = 67844, upload-time = "2026-02-28T00:53:19.637Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + +[[package]] +name = "jsonschema" +source = { editable = "." } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] + +[package.optional-dependencies] +format = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3987" }, + { name = "uri-template" }, + { name = "webcolors" }, +] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[package.dev-dependencies] +test = [ + { name = "jsonpath-ng" }, + { name = "virtue" }, +] + +[package.metadata] +requires-dist = [ + { name = "attrs", specifier = ">=22.2.0" }, + { name = "fqdn", marker = "extra == 'format'" }, + { name = "fqdn", marker = "extra == 'format-nongpl'" }, + { name = "idna", marker = "extra == 'format'" }, + { name = "idna", marker = "extra == 'format-nongpl'" }, + { name = "isoduration", marker = "extra == 'format'" }, + { name = "isoduration", marker = "extra == 'format-nongpl'" }, + { name = "jsonpointer", marker = "extra == 'format'", specifier = ">1.13" }, + { name = "jsonpointer", marker = "extra == 'format-nongpl'", specifier = ">1.13" }, + { name = "jsonschema-specifications", specifier = ">=2023.3.6" }, + { name = "referencing", specifier = ">=0.28.4" }, + { name = "rfc3339-validator", marker = "extra == 'format'" }, + { name = "rfc3339-validator", marker = "extra == 'format-nongpl'" }, + { name = "rfc3986-validator", marker = "extra == 'format-nongpl'", specifier = ">0.1.0" }, + { name = "rfc3987", marker = "extra == 'format'" }, + { name = "rfc3987-syntax", marker = "extra == 'format-nongpl'", specifier = ">=1.1.0" }, + { name = "rpds-py", specifier = ">=0.25.0" }, + { name = "uri-template", marker = "extra == 'format'" }, + { name = "uri-template", marker = "extra == 'format-nongpl'" }, + { name = "webcolors", marker = "extra == 'format'", specifier = ">=1.11" }, + { name = "webcolors", marker = "extra == 'format-nongpl'", specifier = ">=24.6.0" }, +] +provides-extras = ["format", "format-nongpl"] + +[package.metadata.requires-dev] +test = [ + { name = "jsonpath-ng" }, + { name = "virtue" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pyrsistent" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/3a/5031723c09068e9c8c2f0bc25c3a9245f2b1d1aea8396c787a408f2b95ca/pyrsistent-0.20.0.tar.gz", hash = "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", size = 103642, upload-time = "2023-10-25T21:06:56.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/19/c343b14061907b629b765444b6436b160e2bd4184d17d4804bbe6381f6be/pyrsistent-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", size = 83416, upload-time = "2023-10-25T21:06:04.579Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4f/8342079ea331031ef9ed57edd312a9ad283bcc8adfaf268931ae356a09a6/pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", size = 118021, upload-time = "2023-10-25T21:06:06.953Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b7/64a125c488243965b7c5118352e47c6f89df95b4ac306d31cee409153d57/pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", size = 117747, upload-time = "2023-10-25T21:06:08.5Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/43c67bd5f80df9e7583042398d12113263ec57f27c0607abe9d78395d18f/pyrsistent-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", size = 114524, upload-time = "2023-10-25T21:06:10.728Z" }, + { url = "https://files.pythonhosted.org/packages/8a/98/b382a87e89ca839106d874f7bf78d226b3eedb26735eb6f751f1a3375f21/pyrsistent-0.20.0-cp310-cp310-win32.whl", hash = "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", size = 60780, upload-time = "2023-10-25T21:06:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/37/8a/23e2193f7adea6901262e3cf39c7fe18ac0c446176c0ff0e19aeb2e9681e/pyrsistent-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", size = 63310, upload-time = "2023-10-25T21:06:13.598Z" }, + { url = "https://files.pythonhosted.org/packages/df/63/7544dc7d0953294882a5c587fb1b10a26e0c23d9b92281a14c2514bac1f7/pyrsistent-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", size = 83481, upload-time = "2023-10-25T21:06:15.238Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a0/49249bc14d71b1bf2ffe89703acfa86f2017c25cfdabcaea532b8c8a5810/pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", size = 120222, upload-time = "2023-10-25T21:06:17.144Z" }, + { url = "https://files.pythonhosted.org/packages/a1/94/9808e8c9271424120289b9028a657da336ad7e43da0647f62e4f6011d19b/pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", size = 120002, upload-time = "2023-10-25T21:06:18.727Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f6/9ecfb78b2fc8e2540546db0fe19df1fae0f56664a5958c21ff8861b0f8da/pyrsistent-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", size = 116850, upload-time = "2023-10-25T21:06:20.424Z" }, + { url = "https://files.pythonhosted.org/packages/83/c8/e6d28bc27a0719f8eaae660357df9757d6e9ca9be2691595721de9e8adfc/pyrsistent-0.20.0-cp311-cp311-win32.whl", hash = "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", size = 60775, upload-time = "2023-10-25T21:06:21.815Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/c6ef52ff30388f357922d08de012abdd3dc61e09311d88967bdae23ab657/pyrsistent-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", size = 63306, upload-time = "2023-10-25T21:06:22.874Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/ff2ed52032ac1ce2e7ba19e79bd5b05d152ebfb77956cf08fcd6e8d760ea/pyrsistent-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", size = 83537, upload-time = "2023-10-25T21:06:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/f1/338d0050b24c3132bcfc79b68c3a5f54bce3d213ecef74d37e988b971d8a/pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", size = 122615, upload-time = "2023-10-25T21:06:25.815Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/e56d6431b713518094fae6ff833a04a6f49ad0fbe25fb7c0dc7408e19d20/pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", size = 122335, upload-time = "2023-10-25T21:06:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/4a/bb/5f40a4d5e985a43b43f607250e766cdec28904682c3505eb0bd343a4b7db/pyrsistent-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", size = 118510, upload-time = "2023-10-25T21:06:30.718Z" }, + { url = "https://files.pythonhosted.org/packages/1c/13/e6a22f40f5800af116c02c28e29f15c06aa41cb2036f6a64ab124647f28b/pyrsistent-0.20.0-cp312-cp312-win32.whl", hash = "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", size = 60865, upload-time = "2023-10-25T21:06:32.742Z" }, + { url = "https://files.pythonhosted.org/packages/75/ef/2fa3b55023ec07c22682c957808f9a41836da4cd006b5f55ec76bf0fbfa6/pyrsistent-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", size = 63239, upload-time = "2023-10-25T21:06:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/23/88/0acd180010aaed4987c85700b7cc17f9505f3edb4e5873e4dc67f613e338/pyrsistent-0.20.0-py3-none-any.whl", hash = "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", size = 58106, upload-time = "2023-10-25T21:06:54.387Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987" +version = "1.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/bb/f1395c4b62f251a1cb503ff884500ebd248eed593f41b469f89caa3547bd/rfc3987-1.3.8.tar.gz", hash = "sha256:d3c4d257a560d544e9826b38bc81db676890c79ab9d7ac92b39c7a253d5ca733", size = 20700, upload-time = "2018-07-29T17:23:47.954Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/d4/f7407c3d15d5ac779c3dd34fbbc6ea2090f77bd7dd12f207ccf881551208/rfc3987-1.3.8-py2.py3-none-any.whl", hash = "sha256:10702b1e51e5658843460b189b185c0366d2cf4cff716f13111b0ea9fd2dce53", size = 13377, upload-time = "2018-07-29T17:23:45.313Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "twisted" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "automat" }, + { name = "constantly" }, + { name = "hyperlink" }, + { name = "incremental" }, + { name = "typing-extensions" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/0f/82716ed849bf7ea4984c21385597c949944f0f9b428b5710f79d0afc084d/twisted-25.5.0.tar.gz", hash = "sha256:1deb272358cb6be1e3e8fc6f9c8b36f78eb0fa7c2233d2dbe11ec6fee04ea316", size = 3545725, upload-time = "2025-06-07T09:52:24.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/66/ab7efd8941f0bc7b2bd555b0f0471bff77df4c88e0cc31120c82737fec77/twisted-25.5.0-py3-none-any.whl", hash = "sha256:8559f654d01a54a8c3efe66d533d43f383531ebf8d81d9f9ab4769d91ca15df7", size = 3204767, upload-time = "2025-06-07T09:52:21.428Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + +[[package]] +name = "virtue" +version = "2025.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "colorama" }, + { name = "pyrsistent" }, + { name = "twisted" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/be/12ac287b9a2d8af43cc5b1e2b23be25c4c1d32b1c0ee1db162ed7c592023/virtue-2025.7.1.tar.gz", hash = "sha256:7af7e000de8629279f7dcdad43a851a833aa5019c02b277e737d544600ca319b", size = 34972, upload-time = "2025-07-01T15:39:15.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/7d/699d5077881131666596ce3eafd33a23c0228198f465d50ea42713b9f8af/virtue-2025.7.1-py3-none-any.whl", hash = "sha256:37c55eb88e8ac74041389cc07a7191cac0584f9ee8a76df36842ea367cd6ffee", size = 22815, upload-time = "2025-07-01T15:39:14.028Z" }, +] + +[[package]] +name = "webcolors" +version = "25.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, +] + +[[package]] +name = "zope-interface" +version = "8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/a4/77daa5ba398996d16bb43fc721599d27d03eae68fe3c799de1963c72e228/zope_interface-8.2.tar.gz", hash = "sha256:afb20c371a601d261b4f6edb53c3c418c249db1a9717b0baafc9a9bb39ba1224", size = 254019, upload-time = "2026-01-09T07:51:07.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fa/6d9eb3a33998a3019d7eb4fa1802d01d6602fad90e0aea443e6e0fe8e49a/zope_interface-8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:788c293f3165964ec6527b2d861072c68eef53425213f36d3893ebee89a89623", size = 207541, upload-time = "2026-01-09T08:04:55.378Z" }, + { url = "https://files.pythonhosted.org/packages/19/8c/ad23c96fdee84cb1f768f6695dac187cc26e9038e01c69713ba0f7dc46ab/zope_interface-8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9a4e785097e741a1c953b3970ce28f2823bd63c00adc5d276f2981dd66c96c15", size = 208075, upload-time = "2026-01-09T08:04:57.118Z" }, + { url = "https://files.pythonhosted.org/packages/dd/35/1bfd5fec31a307f0cf4065ee74ade63858ded3e2a71e248f1508118fcc95/zope_interface-8.2-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:16c69da19a06566664ddd4785f37cad5693a51d48df1515d264c20d005d322e2", size = 249528, upload-time = "2026-01-09T08:04:59.074Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3a/5d50b5fdb0f8226a2edff6adb7efdd3762ec95dff827dbab1761cb9a9e85/zope_interface-8.2-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c31acfa3d7cde48bec45701b0e1f4698daffc378f559bfb296837d8c834732f6", size = 254646, upload-time = "2026-01-09T08:05:00.964Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2a/ee7d675e151578eaf77828b8faac2b7ed9a69fead350bf5cf0e4afe7c73d/zope_interface-8.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0723507127f8269b8f3f22663168f717e9c9742107d1b6c9f419df561b71aa6d", size = 255083, upload-time = "2026-01-09T08:05:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/5d/07/99e2342f976c3700e142eddc01524e375a9e9078869a6885d9c72f3a3659/zope_interface-8.2-cp310-cp310-win_amd64.whl", hash = "sha256:3bf73a910bb27344def2d301a03329c559a79b308e1e584686b74171d736be4e", size = 211924, upload-time = "2026-01-09T08:05:04.702Z" }, + { url = "https://files.pythonhosted.org/packages/98/97/9c2aa8caae79915ed64eb114e18816f178984c917aa9adf2a18345e4f2e5/zope_interface-8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c65ade7ea85516e428651048489f5e689e695c79188761de8c622594d1e13322", size = 208081, upload-time = "2026-01-09T08:05:06.623Z" }, + { url = "https://files.pythonhosted.org/packages/34/86/4e2fcb01a8f6780ac84923748e450af0805531f47c0956b83065c99ab543/zope_interface-8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1ef4b43659e1348f35f38e7d1a6bbc1682efde239761f335ffc7e31e798b65b", size = 208522, upload-time = "2026-01-09T08:05:07.986Z" }, + { url = "https://files.pythonhosted.org/packages/f6/eb/08e277da32ddcd4014922854096cf6dcb7081fad415892c2da1bedefbf02/zope_interface-8.2-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dfc4f44e8de2ff4eba20af4f0a3ca42d3c43ab24a08e49ccd8558b7a4185b466", size = 255198, upload-time = "2026-01-09T08:05:09.532Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a1/b32484f3281a5dc83bc713ad61eca52c543735cdf204543172087a074a74/zope_interface-8.2-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8f094bfb49179ec5dc9981cb769af1275702bd64720ef94874d9e34da1390d4c", size = 259970, upload-time = "2026-01-09T08:05:11.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/bca0e8ae1e487d4093a8a7cfed2118aa2d4758c8cfd66e59d2af09d71f1c/zope_interface-8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d2bb8e7364e18f083bf6744ccf30433b2a5f236c39c95df8514e3c13007098ce", size = 261153, upload-time = "2026-01-09T08:05:13.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/e3ff2a708011e56b10b271b038d4cb650a8ad5b7d24352fe2edf6d6b187a/zope_interface-8.2-cp311-cp311-win_amd64.whl", hash = "sha256:6f4b4dfcfdfaa9177a600bb31cebf711fdb8c8e9ed84f14c61c420c6aa398489", size = 212330, upload-time = "2026-01-09T08:05:15.267Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a0/1e1fabbd2e9c53ef92b69df6d14f4adc94ec25583b1380336905dc37e9a0/zope_interface-8.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:624b6787fc7c3e45fa401984f6add2c736b70a7506518c3b537ffaacc4b29d4c", size = 208785, upload-time = "2026-01-09T08:05:17.348Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2a/88d098a06975c722a192ef1fb7d623d1b57c6a6997cf01a7aabb45ab1970/zope_interface-8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc9ded9e97a0ed17731d479596ed1071e53b18e6fdb2fc33af1e43f5fd2d3aaa", size = 208976, upload-time = "2026-01-09T08:05:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e8/757398549fdfd2f8c89f32c82ae4d2f0537ae2a5d2f21f4a2f711f5a059f/zope_interface-8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:532367553e4420c80c0fc0cabcc2c74080d495573706f66723edee6eae53361d", size = 259411, upload-time = "2026-01-09T08:05:20.567Z" }, + { url = "https://files.pythonhosted.org/packages/91/af/502601f0395ce84dff622f63cab47488657a04d0065547df42bee3a680ff/zope_interface-8.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2bf9cf275468bafa3c72688aad8cfcbe3d28ee792baf0b228a1b2d93bd1d541a", size = 264859, upload-time = "2026-01-09T08:05:22.234Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/d2f765b9b4814a368a7c1b0ac23b68823c6789a732112668072fe596945d/zope_interface-8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0009d2d3c02ea783045d7804da4fd016245e5c5de31a86cebba66dd6914d59a2", size = 264398, upload-time = "2026-01-09T08:05:23.853Z" }, + { url = "https://files.pythonhosted.org/packages/4a/81/2f171fbc4222066957e6b9220c4fb9146792540102c37e6d94e5d14aad97/zope_interface-8.2-cp312-cp312-win_amd64.whl", hash = "sha256:845d14e580220ae4544bd4d7eb800f0b6034fe5585fc2536806e0a26c2ee6640", size = 212444, upload-time = "2026-01-09T08:05:25.148Z" }, + { url = "https://files.pythonhosted.org/packages/66/47/45188fb101fa060b20e6090e500682398ab415e516a0c228fbb22bc7def2/zope_interface-8.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:6068322004a0158c80dfd4708dfb103a899635408c67c3b10e9acec4dbacefec", size = 209170, upload-time = "2026-01-09T08:05:26.616Z" }, + { url = "https://files.pythonhosted.org/packages/09/03/f6b9336c03c2b48403c4eb73a1ec961d94dc2fb5354c583dfb5fa05fd41f/zope_interface-8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2499de92e8275d0dd68f84425b3e19e9268cd1fa8507997900fa4175f157733c", size = 209229, upload-time = "2026-01-09T08:05:28.521Z" }, + { url = "https://files.pythonhosted.org/packages/07/b1/65fe1dca708569f302ade02e6cdca309eab6752bc9f80105514f5b708651/zope_interface-8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f777e68c76208503609c83ca021a6864902b646530a1a39abb9ed310d1100664", size = 259393, upload-time = "2026-01-09T08:05:29.897Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a5/97b49cfceb6ed53d3dcfb3f3ebf24d83b5553194f0337fbbb3a9fec6cf78/zope_interface-8.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b05a919fdb0ed6ea942e5a7800e09a8b6cdae6f98fee1bef1c9d1a3fc43aaa0", size = 264863, upload-time = "2026-01-09T08:05:31.501Z" }, + { url = "https://files.pythonhosted.org/packages/cb/02/0b7a77292810efe3a0586a505b077ebafd5114e10c6e6e659f0c8e387e1f/zope_interface-8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ccc62b5712dd7bd64cfba3ee63089fb11e840f5914b990033beeae3b2180b6cb", size = 264369, upload-time = "2026-01-09T08:05:32.941Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1d/0d1ff3846302ed1b5bbf659316d8084b30106770a5f346b7ff4e9f540f80/zope_interface-8.2-cp313-cp313-win_amd64.whl", hash = "sha256:34f877d1d3bb7565c494ed93828fa6417641ca26faf6e8f044e0d0d500807028", size = 212447, upload-time = "2026-01-09T08:05:35.064Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/3c89de3917751446728b8898b4d53318bc2f8f6bf8196e150a063c59905e/zope_interface-8.2-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:46c7e4e8cbc698398a67e56ca985d19cb92365b4aafbeb6a712e8c101090f4cb", size = 209223, upload-time = "2026-01-09T08:05:36.449Z" }, + { url = "https://files.pythonhosted.org/packages/00/7f/62d00ec53f0a6e5df0c984781e6f3999ed265129c4c3413df8128d1e0207/zope_interface-8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a87fc7517f825a97ff4a4ca4c8a950593c59e0f8e7bfe1b6f898a38d5ba9f9cf", size = 209366, upload-time = "2026-01-09T08:05:38.197Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/f241986315174be8e00aabecfc2153cf8029c1327cab8ed53a9d979d7e08/zope_interface-8.2-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:ccf52f7d44d669203c2096c1a0c2c15d52e36b2e7a9413df50f48392c7d4d080", size = 261037, upload-time = "2026-01-09T08:05:39.568Z" }, + { url = "https://files.pythonhosted.org/packages/02/cc/b321c51d6936ede296a1b8860cf173bee2928357fe1fff7f97234899173f/zope_interface-8.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aae807efc7bd26302eb2fea05cd6de7d59269ed6ae23a6de1ee47add6de99b8c", size = 264219, upload-time = "2026-01-09T08:05:41.624Z" }, + { url = "https://files.pythonhosted.org/packages/ab/fb/5f5e7b40a2f4efd873fe173624795ca47eaa22e29051270c981361b45209/zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05a0e42d6d830f547e114de2e7cd15750dc6c0c78f8138e6c5035e51ddfff37c", size = 264390, upload-time = "2026-01-09T08:05:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/f9/82/3f2bc594370bc3abd58e5f9085d263bf682a222f059ed46275cde0570810/zope_interface-8.2-cp314-cp314-win_amd64.whl", hash = "sha256:561ce42390bee90bae51cf1c012902a8033b2aaefbd0deed81e877562a116d48", size = 212585, upload-time = "2026-01-09T08:05:44.419Z" }, +]