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" },
+]
]