diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9af30b..055eee2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,6 +26,16 @@ jobs: - name: Run checks run: make check + - name: Confirm rfc3987 never enters the lock file + # jsonschema[format] pulls in rfc3987, which is GPLv3+, into a package that is + # Apache-2.0; only jsonschema[format-nongpl] (see the `validation` extra) should ever + # be used. This is the one place that would catch someone typing the wrong extra. + run: | + if grep -q '^name = "rfc3987"$' uv.lock; then + echo "rfc3987 (GPLv3+) is present in uv.lock - use jsonschema[format-nongpl], not jsonschema[format]" >&2 + exit 1 + fi + tests-and-type-check: runs-on: ${{ matrix.platform }} strategy: diff --git a/pyproject.toml b/pyproject.toml index dd5bcaf..fb16544 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,14 +49,17 @@ oold-validate = "oold.cli:validate_main" [project.optional-dependencies] # The validator needs a JSON Schema implementation and a CLI framework. pyld and rdflib are # already core dependencies, so the whole JSON-LD half of it costs nothing extra. +# +# jsonschema's `format-nongpl` extra, not its `format` extra, supplies the format checkers: the +# `format` extra pulls in rfc3987, which is GPLv3+, into a package that is Apache-2.0. validation = [ - "jsonschema>=4.20", + "jsonschema[format-nongpl]>=4.20", "referencing>=0.30", "click>=8.1", ] mcp = [ "mcp>=1.2", - "jsonschema>=4.20", + "jsonschema[format-nongpl]>=4.20", "referencing>=0.30", "click>=8.1", ] @@ -93,7 +96,7 @@ all = [ "traitlets", "ipython", "nicegui", - "jsonschema>=4.20", + "jsonschema[format-nongpl]>=4.20", "referencing>=0.30", "click>=8.1", "mcp>=1.2", @@ -103,7 +106,7 @@ all = [ dev = [ # The validation extras are dev dependencies too, so `make test` and `make check` see them # without `--all-extras`. - "jsonschema>=4.20", + "jsonschema[format-nongpl]>=4.20", "referencing>=0.30", "click>=8.1", "mcp>=1.2", diff --git a/src/oold/validation/formats.py b/src/oold/validation/formats.py index 72dbd13..81decce 100644 --- a/src/oold/validation/formats.py +++ b/src/oold/validation/formats.py @@ -4,39 +4,65 @@ declared ``format`` is not an error. This module asserts it instead, so a generated instance that violates a declared ``format`` is a failure rather than a silent pass. -Two reasons it implements the formats itself instead of relying on ``jsonschema[format]``: - -* the stock :data:`jsonschema.Draft202012Validator.FORMAT_CHECKER` asserts only eight formats - without optional packages installed, and the ones OO-LD leans on most (``iri``, ``uri``, - ``date-time``, ``duration``, ``time``) are not among them; -* ``iri`` and ``iri-reference`` need a deliberate *override* rather than a strict RFC 3987 - implementation. See :func:`is_iri_reference`. +Most formats are asserted by the checkers ``jsonschema[format-nongpl]`` registers on +``Draft202012Validator.FORMAT_CHECKER`` (see the ``validation`` extra in ``pyproject.toml``). That +extra, not ``jsonschema[format]``, is deliberate: ``jsonschema[format]`` pulls in ``rfc3987``, +which is GPLv3+, into a package that is Apache-2.0. ``format-nongpl`` covers the same formats this +module needs through ``fqdn`` (MPL-2.0), ``rfc3986-validator`` (MIT) and ``rfc3987-syntax`` +(Apache-2.0) instead, none of which carry that obligation. + +Five formats stay hand-written because the library's own checker disagrees with the reference +toolchain (ajv-formats in *full* mode, which is what ``tests/data/format_parity.json`` records) on +cases this project's schemas rely on: + +* ``date-time`` and ``time`` - ``rfc3339_validator`` rejects the space date/time separator, an + offset without a colon (``+0200``) and the leap second ``23:59:60``, all of which the reference + toolchain accepts. +* ``email`` and ``idn-email`` - the library's checker for both is not conditional on any optional + package; it is always just ``"@" in instance``, far looser than ajv-formats' full email pattern. +* ``uuid`` - the library's checker parses with :class:`uuid.UUID`, which strips a leading + ``urn:uuid:``, and then range-checks dash positions assuming no prefix was there, so a prefixed + UUID such as ``urn:uuid:...`` is rejected even though the reference toolchain accepts it. Formats not implemented here stay annotations: an unrecognized ``format`` value is not an error. """ from __future__ import annotations -import ipaddress import re from datetime import date as _date from typing import Any -from jsonschema import FormatChecker +from jsonschema import Draft202012Validator, FormatChecker + +# ---------------------------------------------------------------------------- checker -# ---------------------------------------------------------------------------- IRI +#: The format checker used everywhere in this package. Kept as a module-level singleton so a +#: validator built in one check behaves identically to one built in another. +OOLD_FORMAT_CHECKER = FormatChecker() -# IRI formats (RFC 3987). -# -# jsonschema's stock Draft202012Validator.FORMAT_CHECKER asserts only eight formats without -# optional packages installed (date, email, idn-email, idn-hostname, ipv4, ipv6, regex, uuid), -# and none of them is iri or iri-reference, so this module implements both against RFC 3987 -# itself: an IRI reference excludes ASCII controls, space and the delimiters RFC 3987 disallows, -# while non-ASCII ucschar stays allowed; an absolute IRI additionally begins with a scheme. A -# compact IRI such as `ex:alice` is accepted, since any URI/IRI grammar accepts one - an IRI is a -# superset of a URI. -_IRI_EXCLUDED = re.compile(r"[\s<>\"{}|\\^`]") -_SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.\-]*:") +# Formats where jsonschema[format-nongpl]'s own checker matches the reference toolchain on every +# case in tests/data/format_parity.json, plus this project's own edge cases - a compact IRI such +# as `ex:alice` is accepted by `iri`/`iri-reference`/`uri`/`uri-reference` alike, since an IRI is +# a superset of a URI. Copied from the draft this project targets rather than relying on +# jsonschema's ambient global registry, which also carries formats this module does not use +# (e.g. `color`, registered only for draft 3). +for _name in ( + "date", + "duration", + "hostname", + "idn-hostname", + "ipv4", + "ipv6", + "iri", + "iri-reference", + "json-pointer", + "regex", + "relative-json-pointer", + "uri", + "uri-reference", +): + OOLD_FORMAT_CHECKER.checkers[_name] = Draft202012Validator.FORMAT_CHECKER.checkers[_name] #: ``format`` values that constrain a string to IRI/URI shape. Used by the pattern lint. IRI_FORMATS = frozenset({"iri-reference", "iri", "uri-reference", "uri"}) @@ -44,24 +70,24 @@ def is_iri_reference(value: str) -> bool: """True for an IRI reference: absolute, compact or relative.""" - return isinstance(value, str) and not _IRI_EXCLUDED.search(value) + return isinstance(value, str) and OOLD_FORMAT_CHECKER.conforms(value, "iri-reference") def is_iri(value: str) -> bool: """True for an absolute IRI, meaning an IRI reference that carries a scheme.""" - return is_iri_reference(value) and bool(_SCHEME.match(value)) + return isinstance(value, str) and OOLD_FORMAT_CHECKER.conforms(value, "iri") -# ---------------------------------------------------------------------------- patterns +# ---------------------------------------------------------------------------- hand-written formats # Ported from ajv-formats so the two toolchains agree on edge cases. ajv matches a loose regex # and then range-checks the fields numerically, which is what stops `25:00:00` being accepted; # the same split is used here. `date-time` accepts a space separator and requires an offset, -# `time` leaves the offset optional, and both allow the leap second 23:59:60. +# `time` leaves the offset optional in its grammar (though not by default here, see +# `_valid_time`), and both allow the leap second 23:59:60. _DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$") _TIME = re.compile(r"^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|[+-]\d\d(?::?\d\d)?)?$", re.IGNORECASE) _DATE_TIME_SPLIT = re.compile(r"^(.+?)[t ](.+)$", re.IGNORECASE) -_DURATION = re.compile(r"^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$") # ajv-formats' *full* email pattern, the variant `addFormats(ajv)` installs by default. It # differs from the fast variant by requiring a dotted domain, so `a@b` is rejected, and by only # allowing dots between local-part atoms. @@ -70,35 +96,10 @@ def is_iri(value: str) -> bool: r"@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$", re.IGNORECASE, ) -_HOSTNAME = re.compile( - r"^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?" - r"(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$", - re.IGNORECASE, -) _UUID = re.compile( r"^(?:urn:uuid:)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE, ) -_JSON_POINTER = re.compile(r"^(?:/(?:[^~/]|~0|~1)*)*$") -_RELATIVE_JSON_POINTER = re.compile(r"^(?:0|[1-9][0-9]*)(?:#|(?:/(?:[^~/]|~0|~1)*)*)$") - -#: An ASCII-only counterpart of the IRI rule, which is what separates ``uri`` from ``iri``. -_NON_ASCII = re.compile(r"[^\x00-\x7f]") - - -def _is_uri_reference(value: str) -> bool: - return is_iri_reference(value) and not _NON_ASCII.search(value) - - -def _is_uri(value: str) -> bool: - return _is_uri_reference(value) and bool(_SCHEME.match(value)) - - -# ---------------------------------------------------------------------------- checker - -#: The format checker used everywhere in this package. Kept as a module-level singleton so a -#: validator built in one check behaves identically to one built in another. -OOLD_FORMAT_CHECKER = FormatChecker() def _string_check(func): @@ -159,56 +160,15 @@ def _valid_date_time(value: str) -> bool: return _valid_date(parts[1]) and _valid_time(parts[2], require_offset=True) -def _valid_ipv4(value: str) -> bool: - try: - ipaddress.IPv4Address(value) - except ValueError: - return False - return True - - -def _valid_ipv6(value: str) -> bool: - # A scoped address such as `fe80::1%eth0` is accepted by `ipaddress` but not by ajv, and a - # zone identifier is not part of the JSON Schema `ipv6` format. - if "%" in value: - return False - try: - ipaddress.IPv6Address(value) - except ValueError: - return False - return True - - -def _valid_regex(value: str) -> bool: - try: - re.compile(value) - except re.error: - return False - return True - - -_register("date", _valid_date) _register("date-time", _valid_date_time) _register("time", _valid_time) -_register("duration", lambda v: bool(_DURATION.match(v))) _register("email", lambda v: bool(_EMAIL.match(v))) -_register("hostname", lambda v: bool(_HOSTNAME.match(v))) -_register("ipv4", _valid_ipv4) -_register("ipv6", _valid_ipv6) _register("uuid", lambda v: bool(_UUID.match(v))) -_register("regex", _valid_regex) -_register("json-pointer", lambda v: bool(_JSON_POINTER.match(v))) -_register("relative-json-pointer", lambda v: bool(_RELATIVE_JSON_POINTER.match(v))) -_register("uri", _is_uri) -_register("uri-reference", _is_uri_reference) -_register("iri", is_iri) -_register("iri-reference", is_iri_reference) - -# The internationalised variants differ from their ASCII counterparts only by permitting -# non-ASCII, which the IRI rules already do. Neither OO-LD corpus uses them; they are registered -# so a schema that declares one still gets a shape check rather than silently no check at all. + +# `idn-email` differs from `email` only by permitting non-ASCII, which the IRI rules already do. +# Neither OO-LD corpus uses it; it is registered so a schema that declares it still gets a shape +# check rather than silently no check at all. _register("idn-email", lambda v: bool(_EMAIL.match(v)) or ("@" in v and is_iri_reference(v))) -_register("idn-hostname", lambda v: is_iri_reference(v) and "/" not in v) #: Deterministic, format-valid sample values, used by the instance generator. Every entry must #: satisfy the corresponding checker above; :mod:`tests` asserts exactly that. diff --git a/uv.lock b/uv.lock index c3c4514..5996d8f 100644 --- a/uv.lock +++ b/uv.lock @@ -243,6 +243,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, ] +[[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 = "asttokens" version = "3.0.1" @@ -1119,6 +1132,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/8f/b61d427c4f49a8bdadc93f4e7e74df8a6df6f77ee6e26bf0df53d3925363/filelock-3.29.3-py3-none-any.whl", hash = "sha256:e58333029cc9b925f39aad59b1d8f0a1ad836af4e60d7217f4a4dba87461261d", size = 42324, upload-time = "2026-06-10T17:37:10.37Z" }, ] +[[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 = "frozendict" version = "2.4.7" @@ -1702,6 +1724,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] +[[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 = "isort" version = "8.0.1" @@ -1756,6 +1790,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/94/a8066f84d62ab666d61ef97deba1a33126e3e5c0c0da2c458ada17053ed6/jsondiff-2.2.1-py3-none-any.whl", hash = "sha256:b1f0f7e2421881848b1d556d541ac01a91680cfcc14f51a9b62cdf4da0e56722", size = 13440, upload-time = "2024-08-29T04:09:04.955Z" }, ] +[[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" version = "4.26.0" @@ -1772,6 +1815,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] +[package.optional-dependencies] +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]] name = "jsonschema-specifications" version = "2025.9.1" @@ -1836,6 +1892,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, ] +[[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 = "linkify-it-py" version = "2.1.0" @@ -2685,7 +2750,7 @@ all = [ { name = "ipykernel" }, { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jsonschema" }, + { name = "jsonschema", extra = ["format-nongpl"] }, { name = "jupyter-bokeh" }, { name = "mcp" }, { name = "nicegui" }, @@ -2696,7 +2761,7 @@ all = [ ] mcp = [ { name = "click" }, - { name = "jsonschema" }, + { name = "jsonschema", extra = ["format-nongpl"] }, { name = "mcp" }, { name = "referencing" }, ] @@ -2728,7 +2793,7 @@ ui-panel = [ ] validation = [ { name = "click" }, - { name = "jsonschema" }, + { name = "jsonschema", extra = ["format-nongpl"] }, { name = "referencing" }, ] @@ -2737,7 +2802,7 @@ dev = [ { name = "click" }, { name = "deptry" }, { name = "jsondiff" }, - { name = "jsonschema" }, + { name = "jsonschema", extra = ["format-nongpl"] }, { name = "mcp" }, { name = "mkdocstrings-python" }, { name = "panel" }, @@ -2770,9 +2835,9 @@ requires-dist = [ { name = "ipython", marker = "extra == 'ui'" }, { name = "ipython", marker = "extra == 'ui-jupyter'" }, { name = "jsondiff" }, - { name = "jsonschema", marker = "extra == 'all'", specifier = ">=4.20" }, - { name = "jsonschema", marker = "extra == 'mcp'", specifier = ">=4.20" }, - { name = "jsonschema", marker = "extra == 'validation'", specifier = ">=4.20" }, + { name = "jsonschema", extras = ["format-nongpl"], marker = "extra == 'all'", specifier = ">=4.20" }, + { name = "jsonschema", extras = ["format-nongpl"], marker = "extra == 'mcp'", specifier = ">=4.20" }, + { name = "jsonschema", extras = ["format-nongpl"], marker = "extra == 'validation'", specifier = ">=4.20" }, { name = "jupyter-bokeh", marker = "extra == 'all'" }, { name = "jupyter-bokeh", marker = "extra == 'ui'" }, { name = "jupyter-bokeh", marker = "extra == 'ui-panel'" }, @@ -2807,7 +2872,7 @@ dev = [ { name = "click", specifier = ">=8.1" }, { name = "deptry", specifier = ">=0.25.1" }, { name = "jsondiff" }, - { name = "jsonschema", specifier = ">=4.20" }, + { name = "jsonschema", extras = ["format-nongpl"], specifier = ">=4.20" }, { name = "mcp", specifier = ">=1.2" }, { name = "mkdocstrings-python", specifier = ">=1.0.3" }, { name = "panel" }, @@ -4241,6 +4306,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/60/50fbb6ffb35f733654466f1a90d162bcbea358adc3b0871339254fbc37b2/requirements_parser-0.13.0-py3-none-any.whl", hash = "sha256:2b3173faecf19ec5501971b7222d38f04cb45bb9d87d0ad629ca71e2e62ded14", size = 14782, upload-time = "2025-05-21T13:42:04.007Z" }, ] +[[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-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 = "rich" version = "14.3.4" @@ -4891,6 +4989,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, ] +[[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 = "urllib3" version = "2.7.0" @@ -5169,6 +5276,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl", hash = "sha256:f453740b1e4a4f3291faa37944c555d71056c4da08d59809b307ef4feba695c8", size = 323092, upload-time = "2026-06-08T05:57:21.413Z" }, ] +[[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 = "webencodings" version = "0.5.1"