Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +2 -0
- .venv/lib/python3.11/site-packages/__pycache__/pynvml.cpython-311.pyc +3 -0
- .venv/lib/python3.11/site-packages/__pycache__/typing_extensions.cpython-311.pyc +3 -0
- .venv/lib/python3.11/site-packages/prometheus_client/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/__pycache__/core.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/__pycache__/gc_collector.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/__pycache__/metrics.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/__pycache__/multiprocess.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/__pycache__/process_collector.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/__pycache__/samples.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/__pycache__/utils.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/bridge/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/bridge/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/bridge/__pycache__/graphite.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/bridge/graphite.py +94 -0
- .venv/lib/python3.11/site-packages/prometheus_client/twisted/__init__.py +3 -0
- .venv/lib/python3.11/site-packages/prometheus_client/twisted/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/twisted/__pycache__/_exposition.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/prometheus_client/twisted/_exposition.py +8 -0
- .venv/lib/python3.11/site-packages/referencing/__init__.py +7 -0
- .venv/lib/python3.11/site-packages/referencing/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/__pycache__/_attrs.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/__pycache__/_core.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/__pycache__/exceptions.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/__pycache__/jsonschema.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/__pycache__/retrieval.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/__pycache__/typing.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/_attrs.py +31 -0
- .venv/lib/python3.11/site-packages/referencing/_attrs.pyi +20 -0
- .venv/lib/python3.11/site-packages/referencing/_core.py +739 -0
- .venv/lib/python3.11/site-packages/referencing/exceptions.py +165 -0
- .venv/lib/python3.11/site-packages/referencing/jsonschema.py +642 -0
- .venv/lib/python3.11/site-packages/referencing/py.typed +0 -0
- .venv/lib/python3.11/site-packages/referencing/retrieval.py +92 -0
- .venv/lib/python3.11/site-packages/referencing/tests/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/referencing/tests/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/tests/__pycache__/test_core.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/tests/__pycache__/test_exceptions.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/tests/__pycache__/test_jsonschema.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/tests/__pycache__/test_referencing_suite.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/tests/__pycache__/test_retrieval.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/referencing/tests/test_core.py +1057 -0
- .venv/lib/python3.11/site-packages/referencing/tests/test_exceptions.py +34 -0
- .venv/lib/python3.11/site-packages/referencing/tests/test_jsonschema.py +382 -0
- .venv/lib/python3.11/site-packages/referencing/tests/test_referencing_suite.py +66 -0
- .venv/lib/python3.11/site-packages/referencing/tests/test_retrieval.py +106 -0
- .venv/lib/python3.11/site-packages/referencing/typing.py +61 -0
- .venv/lib/python3.11/site-packages/starlette/_exception_handler.py +65 -0
- .venv/lib/python3.11/site-packages/starlette/applications.py +249 -0
- .venv/lib/python3.11/site-packages/starlette/concurrency.py +62 -0
.gitattributes
CHANGED
|
@@ -205,3 +205,5 @@ tuning-competition-baseline/.venv/lib/python3.11/site-packages/torch/_inductor/_
|
|
| 205 |
.venv/lib/python3.11/site-packages/vllm/__pycache__/utils.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 206 |
.venv/lib/python3.11/site-packages/grpc/_cython/cygrpc.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 207 |
.venv/lib/python3.11/site-packages/wrapt/_wrappers.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 205 |
.venv/lib/python3.11/site-packages/vllm/__pycache__/utils.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 206 |
.venv/lib/python3.11/site-packages/grpc/_cython/cygrpc.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 207 |
.venv/lib/python3.11/site-packages/wrapt/_wrappers.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 208 |
+
.venv/lib/python3.11/site-packages/__pycache__/typing_extensions.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
| 209 |
+
.venv/lib/python3.11/site-packages/__pycache__/pynvml.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
.venv/lib/python3.11/site-packages/__pycache__/pynvml.cpython-311.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8ec8fbd54c733bec6399caf5cd7da39d61df25426c6376b9e78be8d375f12722
|
| 3 |
+
size 285515
|
.venv/lib/python3.11/site-packages/__pycache__/typing_extensions.cpython-311.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4270657b146f00b5210a7cbf963ff4b514a08a0e7303eef5ba0a9e3a6c9a5e5b
|
| 3 |
+
size 151467
|
.venv/lib/python3.11/site-packages/prometheus_client/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (2.54 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/prometheus_client/__pycache__/core.cpython-311.pyc
ADDED
|
Binary file (1.11 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/prometheus_client/__pycache__/gc_collector.cpython-311.pyc
ADDED
|
Binary file (2.51 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/prometheus_client/__pycache__/metrics.cpython-311.pyc
ADDED
|
Binary file (43.2 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/prometheus_client/__pycache__/multiprocess.cpython-311.pyc
ADDED
|
Binary file (10.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/prometheus_client/__pycache__/process_collector.cpython-311.pyc
ADDED
|
Binary file (6.88 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/prometheus_client/__pycache__/samples.cpython-311.pyc
ADDED
|
Binary file (3.89 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/prometheus_client/__pycache__/utils.cpython-311.pyc
ADDED
|
Binary file (1.13 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/prometheus_client/bridge/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/prometheus_client/bridge/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (197 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/prometheus_client/bridge/__pycache__/graphite.cpython-311.pyc
ADDED
|
Binary file (5.35 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/prometheus_client/bridge/graphite.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import re
|
| 5 |
+
import socket
|
| 6 |
+
import threading
|
| 7 |
+
import time
|
| 8 |
+
from timeit import default_timer
|
| 9 |
+
from typing import Callable, Tuple
|
| 10 |
+
|
| 11 |
+
from ..registry import CollectorRegistry, REGISTRY
|
| 12 |
+
|
| 13 |
+
# Roughly, have to keep to what works as a file name.
|
| 14 |
+
# We also remove periods, so labels can be distinguished.
|
| 15 |
+
|
| 16 |
+
_INVALID_GRAPHITE_CHARS = re.compile(r"[^a-zA-Z0-9_-]")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _sanitize(s):
|
| 20 |
+
return _INVALID_GRAPHITE_CHARS.sub('_', s)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class _RegularPush(threading.Thread):
|
| 24 |
+
def __init__(self, pusher, interval, prefix):
|
| 25 |
+
super().__init__()
|
| 26 |
+
self._pusher = pusher
|
| 27 |
+
self._interval = interval
|
| 28 |
+
self._prefix = prefix
|
| 29 |
+
|
| 30 |
+
def run(self):
|
| 31 |
+
wait_until = default_timer()
|
| 32 |
+
while True:
|
| 33 |
+
while True:
|
| 34 |
+
now = default_timer()
|
| 35 |
+
if now >= wait_until:
|
| 36 |
+
# May need to skip some pushes.
|
| 37 |
+
while wait_until < now:
|
| 38 |
+
wait_until += self._interval
|
| 39 |
+
break
|
| 40 |
+
# time.sleep can return early.
|
| 41 |
+
time.sleep(wait_until - now)
|
| 42 |
+
try:
|
| 43 |
+
self._pusher.push(prefix=self._prefix)
|
| 44 |
+
except OSError:
|
| 45 |
+
logging.exception("Push failed")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class GraphiteBridge:
|
| 49 |
+
def __init__(self,
|
| 50 |
+
address: Tuple[str, int],
|
| 51 |
+
registry: CollectorRegistry = REGISTRY,
|
| 52 |
+
timeout_seconds: float = 30,
|
| 53 |
+
_timer: Callable[[], float] = time.time,
|
| 54 |
+
tags: bool = False,
|
| 55 |
+
):
|
| 56 |
+
self._address = address
|
| 57 |
+
self._registry = registry
|
| 58 |
+
self._tags = tags
|
| 59 |
+
self._timeout = timeout_seconds
|
| 60 |
+
self._timer = _timer
|
| 61 |
+
|
| 62 |
+
def push(self, prefix: str = '') -> None:
|
| 63 |
+
now = int(self._timer())
|
| 64 |
+
output = []
|
| 65 |
+
|
| 66 |
+
prefixstr = ''
|
| 67 |
+
if prefix:
|
| 68 |
+
prefixstr = prefix + '.'
|
| 69 |
+
|
| 70 |
+
for metric in self._registry.collect():
|
| 71 |
+
for s in metric.samples:
|
| 72 |
+
if s.labels:
|
| 73 |
+
if self._tags:
|
| 74 |
+
sep = ';'
|
| 75 |
+
fmt = '{0}={1}'
|
| 76 |
+
else:
|
| 77 |
+
sep = '.'
|
| 78 |
+
fmt = '{0}.{1}'
|
| 79 |
+
labelstr = sep + sep.join(
|
| 80 |
+
[fmt.format(
|
| 81 |
+
_sanitize(k), _sanitize(v))
|
| 82 |
+
for k, v in sorted(s.labels.items())])
|
| 83 |
+
else:
|
| 84 |
+
labelstr = ''
|
| 85 |
+
output.append(f'{prefixstr}{_sanitize(s.name)}{labelstr} {float(s.value)} {now}\n')
|
| 86 |
+
|
| 87 |
+
conn = socket.create_connection(self._address, self._timeout)
|
| 88 |
+
conn.sendall(''.join(output).encode('ascii'))
|
| 89 |
+
conn.close()
|
| 90 |
+
|
| 91 |
+
def start(self, interval: float = 60.0, prefix: str = '') -> None:
|
| 92 |
+
t = _RegularPush(self, interval, prefix)
|
| 93 |
+
t.daemon = True
|
| 94 |
+
t.start()
|
.venv/lib/python3.11/site-packages/prometheus_client/twisted/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ._exposition import MetricsResource
|
| 2 |
+
|
| 3 |
+
__all__ = ['MetricsResource']
|
.venv/lib/python3.11/site-packages/prometheus_client/twisted/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (289 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/prometheus_client/twisted/__pycache__/_exposition.cpython-311.pyc
ADDED
|
Binary file (745 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/prometheus_client/twisted/_exposition.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from twisted.internet import reactor
|
| 2 |
+
from twisted.web.wsgi import WSGIResource
|
| 3 |
+
|
| 4 |
+
from .. import exposition, REGISTRY
|
| 5 |
+
|
| 6 |
+
MetricsResource = lambda registry=REGISTRY: WSGIResource(
|
| 7 |
+
reactor, reactor.getThreadPool(), exposition.make_wsgi_app(registry)
|
| 8 |
+
)
|
.venv/lib/python3.11/site-packages/referencing/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Cross-specification, implementation-agnostic JSON referencing.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from referencing._core import Anchor, Registry, Resource, Specification
|
| 6 |
+
|
| 7 |
+
__all__ = ["Anchor", "Registry", "Resource", "Specification"]
|
.venv/lib/python3.11/site-packages/referencing/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (444 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/__pycache__/_attrs.cpython-311.pyc
ADDED
|
Binary file (1.69 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/__pycache__/_core.cpython-311.pyc
ADDED
|
Binary file (33.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/__pycache__/exceptions.cpython-311.pyc
ADDED
|
Binary file (7.76 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/__pycache__/jsonschema.cpython-311.pyc
ADDED
|
Binary file (19.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/__pycache__/retrieval.cpython-311.pyc
ADDED
|
Binary file (3.73 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/__pycache__/typing.cpython-311.pyc
ADDED
|
Binary file (2.62 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/_attrs.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import NoReturn, TypeVar
|
| 4 |
+
|
| 5 |
+
from attrs import define as _define, frozen as _frozen
|
| 6 |
+
|
| 7 |
+
_T = TypeVar("_T")
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def define(cls: type[_T]) -> type[_T]: # pragma: no cover
|
| 11 |
+
cls.__init_subclass__ = _do_not_subclass
|
| 12 |
+
return _define(cls)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def frozen(cls: type[_T]) -> type[_T]:
|
| 16 |
+
cls.__init_subclass__ = _do_not_subclass
|
| 17 |
+
return _frozen(cls)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class UnsupportedSubclassing(Exception):
|
| 21 |
+
def __str__(self):
|
| 22 |
+
return (
|
| 23 |
+
"Subclassing is not part of referencing's public API. "
|
| 24 |
+
"If no other suitable API exists for what you're trying to do, "
|
| 25 |
+
"feel free to file an issue asking for one."
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@staticmethod
|
| 30 |
+
def _do_not_subclass() -> NoReturn: # pragma: no cover
|
| 31 |
+
raise UnsupportedSubclassing()
|
.venv/lib/python3.11/site-packages/referencing/_attrs.pyi
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Callable, TypeVar, Union
|
| 2 |
+
|
| 3 |
+
from attr import attrib, field
|
| 4 |
+
|
| 5 |
+
class UnsupportedSubclassing(Exception): ...
|
| 6 |
+
|
| 7 |
+
_T = TypeVar("_T")
|
| 8 |
+
|
| 9 |
+
def __dataclass_transform__(
|
| 10 |
+
*,
|
| 11 |
+
frozen_default: bool = False,
|
| 12 |
+
field_descriptors: tuple[Union[type, Callable[..., Any]], ...] = ...,
|
| 13 |
+
) -> Callable[[_T], _T]: ...
|
| 14 |
+
@__dataclass_transform__(field_descriptors=(attrib, field))
|
| 15 |
+
def define(cls: type[_T]) -> type[_T]: ...
|
| 16 |
+
@__dataclass_transform__(
|
| 17 |
+
frozen_default=True,
|
| 18 |
+
field_descriptors=(attrib, field),
|
| 19 |
+
)
|
| 20 |
+
def frozen(cls: type[_T]) -> type[_T]: ...
|
.venv/lib/python3.11/site-packages/referencing/_core.py
ADDED
|
@@ -0,0 +1,739 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from collections.abc import Iterable, Iterator, Sequence
|
| 4 |
+
from enum import Enum
|
| 5 |
+
from typing import Any, Callable, ClassVar, Generic, Protocol
|
| 6 |
+
from urllib.parse import unquote, urldefrag, urljoin
|
| 7 |
+
|
| 8 |
+
from attrs import evolve, field
|
| 9 |
+
from rpds import HashTrieMap, HashTrieSet, List
|
| 10 |
+
|
| 11 |
+
try:
|
| 12 |
+
from typing_extensions import TypeVar
|
| 13 |
+
except ImportError: # pragma: no cover
|
| 14 |
+
from typing import TypeVar
|
| 15 |
+
|
| 16 |
+
from referencing import exceptions
|
| 17 |
+
from referencing._attrs import frozen
|
| 18 |
+
from referencing.typing import URI, Anchor as AnchorType, D, Mapping, Retrieve
|
| 19 |
+
|
| 20 |
+
EMPTY_UNCRAWLED: HashTrieSet[URI] = HashTrieSet()
|
| 21 |
+
EMPTY_PREVIOUS_RESOLVERS: List[URI] = List()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class _Unset(Enum):
|
| 25 |
+
"""
|
| 26 |
+
What sillyness...
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
SENTINEL = 1
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
_UNSET = _Unset.SENTINEL
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class _MaybeInSubresource(Protocol[D]):
|
| 36 |
+
def __call__(
|
| 37 |
+
self,
|
| 38 |
+
segments: Sequence[int | str],
|
| 39 |
+
resolver: Resolver[D],
|
| 40 |
+
subresource: Resource[D],
|
| 41 |
+
) -> Resolver[D]: ...
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _detect_or_error(contents: D) -> Specification[D]:
|
| 45 |
+
if not isinstance(contents, Mapping):
|
| 46 |
+
raise exceptions.CannotDetermineSpecification(contents)
|
| 47 |
+
|
| 48 |
+
jsonschema_dialect_id = contents.get("$schema") # type: ignore[reportUnknownMemberType]
|
| 49 |
+
if not isinstance(jsonschema_dialect_id, str):
|
| 50 |
+
raise exceptions.CannotDetermineSpecification(contents)
|
| 51 |
+
|
| 52 |
+
from referencing.jsonschema import specification_with
|
| 53 |
+
|
| 54 |
+
return specification_with(jsonschema_dialect_id)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _detect_or_default(
|
| 58 |
+
default: Specification[D],
|
| 59 |
+
) -> Callable[[D], Specification[D]]:
|
| 60 |
+
def _detect(contents: D) -> Specification[D]:
|
| 61 |
+
if not isinstance(contents, Mapping):
|
| 62 |
+
return default
|
| 63 |
+
|
| 64 |
+
jsonschema_dialect_id = contents.get("$schema") # type: ignore[reportUnknownMemberType]
|
| 65 |
+
if jsonschema_dialect_id is None:
|
| 66 |
+
return default
|
| 67 |
+
|
| 68 |
+
from referencing.jsonschema import specification_with
|
| 69 |
+
|
| 70 |
+
return specification_with(
|
| 71 |
+
jsonschema_dialect_id, # type: ignore[reportUnknownArgumentType]
|
| 72 |
+
default=default,
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
return _detect
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class _SpecificationDetector:
|
| 79 |
+
def __get__(
|
| 80 |
+
self,
|
| 81 |
+
instance: Specification[D] | None,
|
| 82 |
+
cls: type[Specification[D]],
|
| 83 |
+
) -> Callable[[D], Specification[D]]:
|
| 84 |
+
if instance is None:
|
| 85 |
+
return _detect_or_error
|
| 86 |
+
else:
|
| 87 |
+
return _detect_or_default(instance)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@frozen
|
| 91 |
+
class Specification(Generic[D]):
|
| 92 |
+
"""
|
| 93 |
+
A specification which defines referencing behavior.
|
| 94 |
+
|
| 95 |
+
The various methods of a `Specification` allow for varying referencing
|
| 96 |
+
behavior across JSON Schema specification versions, etc.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
#: A short human-readable name for the specification, used for debugging.
|
| 100 |
+
name: str
|
| 101 |
+
|
| 102 |
+
#: Find the ID of a given document.
|
| 103 |
+
id_of: Callable[[D], URI | None]
|
| 104 |
+
|
| 105 |
+
#: Retrieve the subresources of the given document (without traversing into
|
| 106 |
+
#: the subresources themselves).
|
| 107 |
+
subresources_of: Callable[[D], Iterable[D]]
|
| 108 |
+
|
| 109 |
+
#: While resolving a JSON pointer, conditionally enter a subresource
|
| 110 |
+
#: (if e.g. we have just entered a keyword whose value is a subresource)
|
| 111 |
+
maybe_in_subresource: _MaybeInSubresource[D]
|
| 112 |
+
|
| 113 |
+
#: Retrieve the anchors contained in the given document.
|
| 114 |
+
_anchors_in: Callable[
|
| 115 |
+
[Specification[D], D],
|
| 116 |
+
Iterable[AnchorType[D]],
|
| 117 |
+
] = field(alias="anchors_in")
|
| 118 |
+
|
| 119 |
+
#: An opaque specification where resources have no subresources
|
| 120 |
+
#: nor internal identifiers.
|
| 121 |
+
OPAQUE: ClassVar[Specification[Any]]
|
| 122 |
+
|
| 123 |
+
#: Attempt to discern which specification applies to the given contents.
|
| 124 |
+
#:
|
| 125 |
+
#: May be called either as an instance method or as a class method, with
|
| 126 |
+
#: slightly different behavior in the following case:
|
| 127 |
+
#:
|
| 128 |
+
#: Recall that not all contents contains enough internal information about
|
| 129 |
+
#: which specification it is written for -- the JSON Schema ``{}``,
|
| 130 |
+
#: for instance, is valid under many different dialects and may be
|
| 131 |
+
#: interpreted as any one of them.
|
| 132 |
+
#:
|
| 133 |
+
#: When this method is used as an instance method (i.e. called on a
|
| 134 |
+
#: specific specification), that specification is used as the default
|
| 135 |
+
#: if the given contents are unidentifiable.
|
| 136 |
+
#:
|
| 137 |
+
#: On the other hand when called as a class method, an error is raised.
|
| 138 |
+
#:
|
| 139 |
+
#: To reiterate, ``DRAFT202012.detect({})`` will return ``DRAFT202012``
|
| 140 |
+
#: whereas the class method ``Specification.detect({})`` will raise an
|
| 141 |
+
#: error.
|
| 142 |
+
#:
|
| 143 |
+
#: (Note that of course ``DRAFT202012.detect(...)`` may return some other
|
| 144 |
+
#: specification when given a schema which *does* identify as being for
|
| 145 |
+
#: another version).
|
| 146 |
+
#:
|
| 147 |
+
#: Raises:
|
| 148 |
+
#:
|
| 149 |
+
#: `CannotDetermineSpecification`
|
| 150 |
+
#:
|
| 151 |
+
#: if the given contents don't have any discernible
|
| 152 |
+
#: information which could be used to guess which
|
| 153 |
+
#: specification they identify as
|
| 154 |
+
detect = _SpecificationDetector()
|
| 155 |
+
|
| 156 |
+
def __repr__(self) -> str:
|
| 157 |
+
return f"<Specification name={self.name!r}>"
|
| 158 |
+
|
| 159 |
+
def anchors_in(self, contents: D):
|
| 160 |
+
"""
|
| 161 |
+
Retrieve the anchors contained in the given document.
|
| 162 |
+
"""
|
| 163 |
+
return self._anchors_in(self, contents)
|
| 164 |
+
|
| 165 |
+
def create_resource(self, contents: D) -> Resource[D]:
|
| 166 |
+
"""
|
| 167 |
+
Create a resource which is interpreted using this specification.
|
| 168 |
+
"""
|
| 169 |
+
return Resource(contents=contents, specification=self)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
Specification.OPAQUE = Specification(
|
| 173 |
+
name="opaque",
|
| 174 |
+
id_of=lambda contents: None,
|
| 175 |
+
subresources_of=lambda contents: [],
|
| 176 |
+
anchors_in=lambda specification, contents: [],
|
| 177 |
+
maybe_in_subresource=lambda segments, resolver, subresource: resolver,
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
@frozen
|
| 182 |
+
class Resource(Generic[D]):
|
| 183 |
+
r"""
|
| 184 |
+
A document (deserialized JSON) with a concrete interpretation under a spec.
|
| 185 |
+
|
| 186 |
+
In other words, a Python object, along with an instance of `Specification`
|
| 187 |
+
which describes how the document interacts with referencing -- both
|
| 188 |
+
internally (how it refers to other `Resource`\ s) and externally (how it
|
| 189 |
+
should be identified such that it is referenceable by other documents).
|
| 190 |
+
"""
|
| 191 |
+
|
| 192 |
+
contents: D
|
| 193 |
+
_specification: Specification[D] = field(alias="specification")
|
| 194 |
+
|
| 195 |
+
@classmethod
|
| 196 |
+
def from_contents(
|
| 197 |
+
cls,
|
| 198 |
+
contents: D,
|
| 199 |
+
default_specification: (
|
| 200 |
+
type[Specification[D]] | Specification[D]
|
| 201 |
+
) = Specification,
|
| 202 |
+
) -> Resource[D]:
|
| 203 |
+
"""
|
| 204 |
+
Create a resource guessing which specification applies to the contents.
|
| 205 |
+
|
| 206 |
+
Raises:
|
| 207 |
+
|
| 208 |
+
`CannotDetermineSpecification`
|
| 209 |
+
|
| 210 |
+
if the given contents don't have any discernible
|
| 211 |
+
information which could be used to guess which
|
| 212 |
+
specification they identify as
|
| 213 |
+
|
| 214 |
+
"""
|
| 215 |
+
specification = default_specification.detect(contents)
|
| 216 |
+
return specification.create_resource(contents=contents)
|
| 217 |
+
|
| 218 |
+
@classmethod
|
| 219 |
+
def opaque(cls, contents: D) -> Resource[D]:
|
| 220 |
+
"""
|
| 221 |
+
Create an opaque `Resource` -- i.e. one with opaque specification.
|
| 222 |
+
|
| 223 |
+
See `Specification.OPAQUE` for details.
|
| 224 |
+
"""
|
| 225 |
+
return Specification.OPAQUE.create_resource(contents=contents)
|
| 226 |
+
|
| 227 |
+
def id(self) -> URI | None:
|
| 228 |
+
"""
|
| 229 |
+
Retrieve this resource's (specification-specific) identifier.
|
| 230 |
+
"""
|
| 231 |
+
id = self._specification.id_of(self.contents)
|
| 232 |
+
if id is None:
|
| 233 |
+
return
|
| 234 |
+
return id.rstrip("#")
|
| 235 |
+
|
| 236 |
+
def subresources(self) -> Iterable[Resource[D]]:
|
| 237 |
+
"""
|
| 238 |
+
Retrieve this resource's subresources.
|
| 239 |
+
"""
|
| 240 |
+
return (
|
| 241 |
+
Resource.from_contents(
|
| 242 |
+
each,
|
| 243 |
+
default_specification=self._specification,
|
| 244 |
+
)
|
| 245 |
+
for each in self._specification.subresources_of(self.contents)
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
def anchors(self) -> Iterable[AnchorType[D]]:
|
| 249 |
+
"""
|
| 250 |
+
Retrieve this resource's (specification-specific) identifier.
|
| 251 |
+
"""
|
| 252 |
+
return self._specification.anchors_in(self.contents)
|
| 253 |
+
|
| 254 |
+
def pointer(self, pointer: str, resolver: Resolver[D]) -> Resolved[D]:
|
| 255 |
+
"""
|
| 256 |
+
Resolve the given JSON pointer.
|
| 257 |
+
|
| 258 |
+
Raises:
|
| 259 |
+
|
| 260 |
+
`exceptions.PointerToNowhere`
|
| 261 |
+
|
| 262 |
+
if the pointer points to a location not present in the document
|
| 263 |
+
|
| 264 |
+
"""
|
| 265 |
+
if not pointer:
|
| 266 |
+
return Resolved(contents=self.contents, resolver=resolver)
|
| 267 |
+
|
| 268 |
+
contents = self.contents
|
| 269 |
+
segments: list[int | str] = []
|
| 270 |
+
for segment in unquote(pointer[1:]).split("/"):
|
| 271 |
+
if isinstance(contents, Sequence):
|
| 272 |
+
segment = int(segment)
|
| 273 |
+
else:
|
| 274 |
+
segment = segment.replace("~1", "/").replace("~0", "~")
|
| 275 |
+
try:
|
| 276 |
+
contents = contents[segment] # type: ignore[reportUnknownArgumentType]
|
| 277 |
+
except LookupError as lookup_error:
|
| 278 |
+
error = exceptions.PointerToNowhere(ref=pointer, resource=self)
|
| 279 |
+
raise error from lookup_error
|
| 280 |
+
|
| 281 |
+
segments.append(segment)
|
| 282 |
+
last = resolver
|
| 283 |
+
resolver = self._specification.maybe_in_subresource(
|
| 284 |
+
segments=segments,
|
| 285 |
+
resolver=resolver,
|
| 286 |
+
subresource=self._specification.create_resource(contents),
|
| 287 |
+
)
|
| 288 |
+
if resolver is not last:
|
| 289 |
+
segments = []
|
| 290 |
+
return Resolved(contents=contents, resolver=resolver) # type: ignore[reportUnknownArgumentType]
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def _fail_to_retrieve(uri: URI):
|
| 294 |
+
raise exceptions.NoSuchResource(ref=uri)
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
@frozen
|
| 298 |
+
class Registry(Mapping[URI, Resource[D]]):
|
| 299 |
+
r"""
|
| 300 |
+
A registry of `Resource`\ s, each identified by their canonical URIs.
|
| 301 |
+
|
| 302 |
+
Registries store a collection of in-memory resources, and optionally
|
| 303 |
+
enable additional resources which may be stored elsewhere (e.g. in a
|
| 304 |
+
database, a separate set of files, over the network, etc.).
|
| 305 |
+
|
| 306 |
+
They also lazily walk their known resources, looking for subresources
|
| 307 |
+
within them. In other words, subresources contained within any added
|
| 308 |
+
resources will be retrievable via their own IDs (though this discovery of
|
| 309 |
+
subresources will be delayed until necessary).
|
| 310 |
+
|
| 311 |
+
Registries are immutable, and their methods return new instances of the
|
| 312 |
+
registry with the additional resources added to them.
|
| 313 |
+
|
| 314 |
+
The ``retrieve`` argument can be used to configure retrieval of resources
|
| 315 |
+
dynamically, either over the network, from a database, or the like.
|
| 316 |
+
Pass it a callable which will be called if any URI not present in the
|
| 317 |
+
registry is accessed. It must either return a `Resource` or else raise a
|
| 318 |
+
`NoSuchResource` exception indicating that the resource does not exist
|
| 319 |
+
even according to the retrieval logic.
|
| 320 |
+
"""
|
| 321 |
+
|
| 322 |
+
_resources: HashTrieMap[URI, Resource[D]] = field(
|
| 323 |
+
default=HashTrieMap(),
|
| 324 |
+
converter=HashTrieMap.convert, # type: ignore[reportGeneralTypeIssues]
|
| 325 |
+
alias="resources",
|
| 326 |
+
)
|
| 327 |
+
_anchors: HashTrieMap[tuple[URI, str], AnchorType[D]] = HashTrieMap()
|
| 328 |
+
_uncrawled: HashTrieSet[URI] = EMPTY_UNCRAWLED
|
| 329 |
+
_retrieve: Retrieve[D] = field(default=_fail_to_retrieve, alias="retrieve")
|
| 330 |
+
|
| 331 |
+
def __getitem__(self, uri: URI) -> Resource[D]:
|
| 332 |
+
"""
|
| 333 |
+
Return the (already crawled) `Resource` identified by the given URI.
|
| 334 |
+
"""
|
| 335 |
+
try:
|
| 336 |
+
return self._resources[uri.rstrip("#")]
|
| 337 |
+
except KeyError:
|
| 338 |
+
raise exceptions.NoSuchResource(ref=uri) from None
|
| 339 |
+
|
| 340 |
+
def __iter__(self) -> Iterator[URI]:
|
| 341 |
+
"""
|
| 342 |
+
Iterate over all crawled URIs in the registry.
|
| 343 |
+
"""
|
| 344 |
+
return iter(self._resources)
|
| 345 |
+
|
| 346 |
+
def __len__(self) -> int:
|
| 347 |
+
"""
|
| 348 |
+
Count the total number of fully crawled resources in this registry.
|
| 349 |
+
"""
|
| 350 |
+
return len(self._resources)
|
| 351 |
+
|
| 352 |
+
def __rmatmul__(
|
| 353 |
+
self,
|
| 354 |
+
new: Resource[D] | Iterable[Resource[D]],
|
| 355 |
+
) -> Registry[D]:
|
| 356 |
+
"""
|
| 357 |
+
Create a new registry with resource(s) added using their internal IDs.
|
| 358 |
+
|
| 359 |
+
Resources must have a internal IDs (e.g. the :kw:`$id` keyword in
|
| 360 |
+
modern JSON Schema versions), otherwise an error will be raised.
|
| 361 |
+
|
| 362 |
+
Both a single resource as well as an iterable of resources works, i.e.:
|
| 363 |
+
|
| 364 |
+
* ``resource @ registry`` or
|
| 365 |
+
|
| 366 |
+
* ``[iterable, of, multiple, resources] @ registry``
|
| 367 |
+
|
| 368 |
+
which -- again, assuming the resources have internal IDs -- is
|
| 369 |
+
equivalent to calling `Registry.with_resources` as such:
|
| 370 |
+
|
| 371 |
+
.. code:: python
|
| 372 |
+
|
| 373 |
+
registry.with_resources(
|
| 374 |
+
(resource.id(), resource) for resource in new_resources
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
Raises:
|
| 378 |
+
|
| 379 |
+
`NoInternalID`
|
| 380 |
+
|
| 381 |
+
if the resource(s) in fact do not have IDs
|
| 382 |
+
|
| 383 |
+
"""
|
| 384 |
+
if isinstance(new, Resource):
|
| 385 |
+
new = (new,)
|
| 386 |
+
|
| 387 |
+
resources = self._resources
|
| 388 |
+
uncrawled = self._uncrawled
|
| 389 |
+
for resource in new:
|
| 390 |
+
id = resource.id()
|
| 391 |
+
if id is None:
|
| 392 |
+
raise exceptions.NoInternalID(resource=resource)
|
| 393 |
+
uncrawled = uncrawled.insert(id)
|
| 394 |
+
resources = resources.insert(id, resource)
|
| 395 |
+
return evolve(self, resources=resources, uncrawled=uncrawled)
|
| 396 |
+
|
| 397 |
+
def __repr__(self) -> str:
|
| 398 |
+
size = len(self)
|
| 399 |
+
pluralized = "resource" if size == 1 else "resources"
|
| 400 |
+
if self._uncrawled:
|
| 401 |
+
uncrawled = len(self._uncrawled)
|
| 402 |
+
if uncrawled == size:
|
| 403 |
+
summary = f"uncrawled {pluralized}"
|
| 404 |
+
else:
|
| 405 |
+
summary = f"{pluralized}, {uncrawled} uncrawled"
|
| 406 |
+
else:
|
| 407 |
+
summary = f"{pluralized}"
|
| 408 |
+
return f"<Registry ({size} {summary})>"
|
| 409 |
+
|
| 410 |
+
def get_or_retrieve(self, uri: URI) -> Retrieved[D, Resource[D]]:
|
| 411 |
+
"""
|
| 412 |
+
Get a resource from the registry, crawling or retrieving if necessary.
|
| 413 |
+
|
| 414 |
+
May involve crawling to find the given URI if it is not already known,
|
| 415 |
+
so the returned object is a `Retrieved` object which contains both the
|
| 416 |
+
resource value as well as the registry which ultimately contained it.
|
| 417 |
+
"""
|
| 418 |
+
resource = self._resources.get(uri)
|
| 419 |
+
if resource is not None:
|
| 420 |
+
return Retrieved(registry=self, value=resource)
|
| 421 |
+
|
| 422 |
+
registry = self.crawl()
|
| 423 |
+
resource = registry._resources.get(uri)
|
| 424 |
+
if resource is not None:
|
| 425 |
+
return Retrieved(registry=registry, value=resource)
|
| 426 |
+
|
| 427 |
+
try:
|
| 428 |
+
resource = registry._retrieve(uri)
|
| 429 |
+
except (
|
| 430 |
+
exceptions.CannotDetermineSpecification,
|
| 431 |
+
exceptions.NoSuchResource,
|
| 432 |
+
):
|
| 433 |
+
raise
|
| 434 |
+
except Exception as error:
|
| 435 |
+
raise exceptions.Unretrievable(ref=uri) from error
|
| 436 |
+
else:
|
| 437 |
+
registry = registry.with_resource(uri, resource)
|
| 438 |
+
return Retrieved(registry=registry, value=resource)
|
| 439 |
+
|
| 440 |
+
def remove(self, uri: URI):
|
| 441 |
+
"""
|
| 442 |
+
Return a registry with the resource identified by a given URI removed.
|
| 443 |
+
"""
|
| 444 |
+
if uri not in self._resources:
|
| 445 |
+
raise exceptions.NoSuchResource(ref=uri)
|
| 446 |
+
|
| 447 |
+
return evolve(
|
| 448 |
+
self,
|
| 449 |
+
resources=self._resources.remove(uri),
|
| 450 |
+
uncrawled=self._uncrawled.discard(uri),
|
| 451 |
+
anchors=HashTrieMap(
|
| 452 |
+
(k, v) for k, v in self._anchors.items() if k[0] != uri
|
| 453 |
+
),
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
def anchor(self, uri: URI, name: str):
|
| 457 |
+
"""
|
| 458 |
+
Retrieve a given anchor from a resource which must already be crawled.
|
| 459 |
+
"""
|
| 460 |
+
value = self._anchors.get((uri, name))
|
| 461 |
+
if value is not None:
|
| 462 |
+
return Retrieved(value=value, registry=self)
|
| 463 |
+
|
| 464 |
+
registry = self.crawl()
|
| 465 |
+
value = registry._anchors.get((uri, name))
|
| 466 |
+
if value is not None:
|
| 467 |
+
return Retrieved(value=value, registry=registry)
|
| 468 |
+
|
| 469 |
+
resource = self[uri]
|
| 470 |
+
canonical_uri = resource.id()
|
| 471 |
+
if canonical_uri is not None:
|
| 472 |
+
value = registry._anchors.get((canonical_uri, name))
|
| 473 |
+
if value is not None:
|
| 474 |
+
return Retrieved(value=value, registry=registry)
|
| 475 |
+
|
| 476 |
+
if "/" in name:
|
| 477 |
+
raise exceptions.InvalidAnchor(
|
| 478 |
+
ref=uri,
|
| 479 |
+
resource=resource,
|
| 480 |
+
anchor=name,
|
| 481 |
+
)
|
| 482 |
+
raise exceptions.NoSuchAnchor(ref=uri, resource=resource, anchor=name)
|
| 483 |
+
|
| 484 |
+
def contents(self, uri: URI) -> D:
|
| 485 |
+
"""
|
| 486 |
+
Retrieve the (already crawled) contents identified by the given URI.
|
| 487 |
+
"""
|
| 488 |
+
return self[uri].contents
|
| 489 |
+
|
| 490 |
+
def crawl(self) -> Registry[D]:
|
| 491 |
+
"""
|
| 492 |
+
Crawl all added resources, discovering subresources.
|
| 493 |
+
"""
|
| 494 |
+
resources = self._resources
|
| 495 |
+
anchors = self._anchors
|
| 496 |
+
uncrawled = [(uri, resources[uri]) for uri in self._uncrawled]
|
| 497 |
+
while uncrawled:
|
| 498 |
+
uri, resource = uncrawled.pop()
|
| 499 |
+
|
| 500 |
+
id = resource.id()
|
| 501 |
+
if id is not None:
|
| 502 |
+
uri = urljoin(uri, id)
|
| 503 |
+
resources = resources.insert(uri, resource)
|
| 504 |
+
for each in resource.anchors():
|
| 505 |
+
anchors = anchors.insert((uri, each.name), each)
|
| 506 |
+
uncrawled.extend((uri, each) for each in resource.subresources())
|
| 507 |
+
return evolve(
|
| 508 |
+
self,
|
| 509 |
+
resources=resources,
|
| 510 |
+
anchors=anchors,
|
| 511 |
+
uncrawled=EMPTY_UNCRAWLED,
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
def with_resource(self, uri: URI, resource: Resource[D]):
|
| 515 |
+
"""
|
| 516 |
+
Add the given `Resource` to the registry, without crawling it.
|
| 517 |
+
"""
|
| 518 |
+
return self.with_resources([(uri, resource)])
|
| 519 |
+
|
| 520 |
+
def with_resources(
|
| 521 |
+
self,
|
| 522 |
+
pairs: Iterable[tuple[URI, Resource[D]]],
|
| 523 |
+
) -> Registry[D]:
|
| 524 |
+
r"""
|
| 525 |
+
Add the given `Resource`\ s to the registry, without crawling them.
|
| 526 |
+
"""
|
| 527 |
+
resources = self._resources
|
| 528 |
+
uncrawled = self._uncrawled
|
| 529 |
+
for uri, resource in pairs:
|
| 530 |
+
# Empty fragment URIs are equivalent to URIs without the fragment.
|
| 531 |
+
# TODO: Is this true for non JSON Schema resources? Probably not.
|
| 532 |
+
uri = uri.rstrip("#")
|
| 533 |
+
uncrawled = uncrawled.insert(uri)
|
| 534 |
+
resources = resources.insert(uri, resource)
|
| 535 |
+
return evolve(self, resources=resources, uncrawled=uncrawled)
|
| 536 |
+
|
| 537 |
+
def with_contents(
|
| 538 |
+
self,
|
| 539 |
+
pairs: Iterable[tuple[URI, D]],
|
| 540 |
+
**kwargs: Any,
|
| 541 |
+
) -> Registry[D]:
|
| 542 |
+
r"""
|
| 543 |
+
Add the given contents to the registry, autodetecting when necessary.
|
| 544 |
+
"""
|
| 545 |
+
return self.with_resources(
|
| 546 |
+
(uri, Resource.from_contents(each, **kwargs))
|
| 547 |
+
for uri, each in pairs
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
def combine(self, *registries: Registry[D]) -> Registry[D]:
|
| 551 |
+
"""
|
| 552 |
+
Combine together one or more other registries, producing a unified one.
|
| 553 |
+
"""
|
| 554 |
+
if registries == (self,):
|
| 555 |
+
return self
|
| 556 |
+
resources = self._resources
|
| 557 |
+
anchors = self._anchors
|
| 558 |
+
uncrawled = self._uncrawled
|
| 559 |
+
retrieve = self._retrieve
|
| 560 |
+
for registry in registries:
|
| 561 |
+
resources = resources.update(registry._resources)
|
| 562 |
+
anchors = anchors.update(registry._anchors)
|
| 563 |
+
uncrawled = uncrawled.update(registry._uncrawled)
|
| 564 |
+
|
| 565 |
+
if registry._retrieve is not _fail_to_retrieve: # type: ignore[reportUnnecessaryComparison] ???
|
| 566 |
+
if registry._retrieve is not retrieve is not _fail_to_retrieve: # type: ignore[reportUnnecessaryComparison] ???
|
| 567 |
+
raise ValueError( # noqa: TRY003
|
| 568 |
+
"Cannot combine registries with conflicting retrieval "
|
| 569 |
+
"functions.",
|
| 570 |
+
)
|
| 571 |
+
retrieve = registry._retrieve
|
| 572 |
+
return evolve(
|
| 573 |
+
self,
|
| 574 |
+
anchors=anchors,
|
| 575 |
+
resources=resources,
|
| 576 |
+
uncrawled=uncrawled,
|
| 577 |
+
retrieve=retrieve,
|
| 578 |
+
)
|
| 579 |
+
|
| 580 |
+
def resolver(self, base_uri: URI = "") -> Resolver[D]:
|
| 581 |
+
"""
|
| 582 |
+
Return a `Resolver` which resolves references against this registry.
|
| 583 |
+
"""
|
| 584 |
+
return Resolver(base_uri=base_uri, registry=self)
|
| 585 |
+
|
| 586 |
+
def resolver_with_root(self, resource: Resource[D]) -> Resolver[D]:
|
| 587 |
+
"""
|
| 588 |
+
Return a `Resolver` with a specific root resource.
|
| 589 |
+
"""
|
| 590 |
+
uri = resource.id() or ""
|
| 591 |
+
return Resolver(
|
| 592 |
+
base_uri=uri,
|
| 593 |
+
registry=self.with_resource(uri, resource),
|
| 594 |
+
)
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
#: An anchor or resource.
|
| 598 |
+
AnchorOrResource = TypeVar(
|
| 599 |
+
"AnchorOrResource",
|
| 600 |
+
AnchorType[Any],
|
| 601 |
+
Resource[Any],
|
| 602 |
+
default=Resource[Any],
|
| 603 |
+
)
|
| 604 |
+
|
| 605 |
+
|
| 606 |
+
@frozen
|
| 607 |
+
class Retrieved(Generic[D, AnchorOrResource]):
|
| 608 |
+
"""
|
| 609 |
+
A value retrieved from a `Registry`.
|
| 610 |
+
"""
|
| 611 |
+
|
| 612 |
+
value: AnchorOrResource
|
| 613 |
+
registry: Registry[D]
|
| 614 |
+
|
| 615 |
+
|
| 616 |
+
@frozen
|
| 617 |
+
class Resolved(Generic[D]):
|
| 618 |
+
"""
|
| 619 |
+
A reference resolved to its contents by a `Resolver`.
|
| 620 |
+
"""
|
| 621 |
+
|
| 622 |
+
contents: D
|
| 623 |
+
resolver: Resolver[D]
|
| 624 |
+
|
| 625 |
+
|
| 626 |
+
@frozen
|
| 627 |
+
class Resolver(Generic[D]):
|
| 628 |
+
"""
|
| 629 |
+
A reference resolver.
|
| 630 |
+
|
| 631 |
+
Resolvers help resolve references (including relative ones) by
|
| 632 |
+
pairing a fixed base URI with a `Registry`.
|
| 633 |
+
|
| 634 |
+
This object, under normal circumstances, is expected to be used by
|
| 635 |
+
*implementers of libraries* built on top of `referencing` (e.g. JSON Schema
|
| 636 |
+
implementations or other libraries resolving JSON references),
|
| 637 |
+
not directly by end-users populating registries or while writing
|
| 638 |
+
schemas or other resources.
|
| 639 |
+
|
| 640 |
+
References are resolved against the base URI, and the combined URI
|
| 641 |
+
is then looked up within the registry.
|
| 642 |
+
|
| 643 |
+
The process of resolving a reference may itself involve calculating
|
| 644 |
+
a *new* base URI for future reference resolution (e.g. if an
|
| 645 |
+
intermediate resource sets a new base URI), or may involve encountering
|
| 646 |
+
additional subresources and adding them to a new registry.
|
| 647 |
+
"""
|
| 648 |
+
|
| 649 |
+
_base_uri: URI = field(alias="base_uri")
|
| 650 |
+
_registry: Registry[D] = field(alias="registry")
|
| 651 |
+
_previous: List[URI] = field(default=List(), repr=False, alias="previous")
|
| 652 |
+
|
| 653 |
+
def lookup(self, ref: URI) -> Resolved[D]:
|
| 654 |
+
"""
|
| 655 |
+
Resolve the given reference to the resource it points to.
|
| 656 |
+
|
| 657 |
+
Raises:
|
| 658 |
+
|
| 659 |
+
`exceptions.Unresolvable`
|
| 660 |
+
|
| 661 |
+
or a subclass thereof (see below) if the reference isn't
|
| 662 |
+
resolvable
|
| 663 |
+
|
| 664 |
+
`exceptions.NoSuchAnchor`
|
| 665 |
+
|
| 666 |
+
if the reference is to a URI where a resource exists but
|
| 667 |
+
contains a plain name fragment which does not exist within
|
| 668 |
+
the resource
|
| 669 |
+
|
| 670 |
+
`exceptions.PointerToNowhere`
|
| 671 |
+
|
| 672 |
+
if the reference is to a URI where a resource exists but
|
| 673 |
+
contains a JSON pointer to a location within the resource
|
| 674 |
+
that does not exist
|
| 675 |
+
|
| 676 |
+
"""
|
| 677 |
+
if ref.startswith("#"):
|
| 678 |
+
uri, fragment = self._base_uri, ref[1:]
|
| 679 |
+
else:
|
| 680 |
+
uri, fragment = urldefrag(urljoin(self._base_uri, ref))
|
| 681 |
+
try:
|
| 682 |
+
retrieved = self._registry.get_or_retrieve(uri)
|
| 683 |
+
except exceptions.NoSuchResource:
|
| 684 |
+
raise exceptions.Unresolvable(ref=ref) from None
|
| 685 |
+
except exceptions.Unretrievable as error:
|
| 686 |
+
raise exceptions.Unresolvable(ref=ref) from error
|
| 687 |
+
|
| 688 |
+
if fragment.startswith("/"):
|
| 689 |
+
resolver = self._evolve(registry=retrieved.registry, base_uri=uri)
|
| 690 |
+
return retrieved.value.pointer(pointer=fragment, resolver=resolver)
|
| 691 |
+
|
| 692 |
+
if fragment:
|
| 693 |
+
retrieved = retrieved.registry.anchor(uri, fragment)
|
| 694 |
+
resolver = self._evolve(registry=retrieved.registry, base_uri=uri)
|
| 695 |
+
return retrieved.value.resolve(resolver=resolver)
|
| 696 |
+
|
| 697 |
+
resolver = self._evolve(registry=retrieved.registry, base_uri=uri)
|
| 698 |
+
return Resolved(contents=retrieved.value.contents, resolver=resolver)
|
| 699 |
+
|
| 700 |
+
def in_subresource(self, subresource: Resource[D]) -> Resolver[D]:
|
| 701 |
+
"""
|
| 702 |
+
Create a resolver for a subresource (which may have a new base URI).
|
| 703 |
+
"""
|
| 704 |
+
id = subresource.id()
|
| 705 |
+
if id is None:
|
| 706 |
+
return self
|
| 707 |
+
return evolve(self, base_uri=urljoin(self._base_uri, id))
|
| 708 |
+
|
| 709 |
+
def dynamic_scope(self) -> Iterable[tuple[URI, Registry[D]]]:
|
| 710 |
+
"""
|
| 711 |
+
In specs with such a notion, return the URIs in the dynamic scope.
|
| 712 |
+
"""
|
| 713 |
+
for uri in self._previous:
|
| 714 |
+
yield uri, self._registry
|
| 715 |
+
|
| 716 |
+
def _evolve(self, base_uri: URI, **kwargs: Any):
|
| 717 |
+
"""
|
| 718 |
+
Evolve, appending to the dynamic scope.
|
| 719 |
+
"""
|
| 720 |
+
previous = self._previous
|
| 721 |
+
if self._base_uri and (not previous or base_uri != self._base_uri):
|
| 722 |
+
previous = previous.push_front(self._base_uri)
|
| 723 |
+
return evolve(self, base_uri=base_uri, previous=previous, **kwargs)
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
@frozen
|
| 727 |
+
class Anchor(Generic[D]):
|
| 728 |
+
"""
|
| 729 |
+
A simple anchor in a `Resource`.
|
| 730 |
+
"""
|
| 731 |
+
|
| 732 |
+
name: str
|
| 733 |
+
resource: Resource[D]
|
| 734 |
+
|
| 735 |
+
def resolve(self, resolver: Resolver[D]):
|
| 736 |
+
"""
|
| 737 |
+
Return the resource for this anchor.
|
| 738 |
+
"""
|
| 739 |
+
return Resolved(contents=self.resource.contents, resolver=resolver)
|
.venv/lib/python3.11/site-packages/referencing/exceptions.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Errors, oh no!
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from typing import TYPE_CHECKING, Any
|
| 8 |
+
|
| 9 |
+
import attrs
|
| 10 |
+
|
| 11 |
+
from referencing._attrs import frozen
|
| 12 |
+
|
| 13 |
+
if TYPE_CHECKING:
|
| 14 |
+
from referencing import Resource
|
| 15 |
+
from referencing.typing import URI
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@frozen
|
| 19 |
+
class NoSuchResource(KeyError):
|
| 20 |
+
"""
|
| 21 |
+
The given URI is not present in a registry.
|
| 22 |
+
|
| 23 |
+
Unlike most exceptions, this class *is* intended to be publicly
|
| 24 |
+
instantiable and *is* part of the public API of the package.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
ref: URI
|
| 28 |
+
|
| 29 |
+
def __eq__(self, other: object) -> bool:
|
| 30 |
+
if self.__class__ is not other.__class__:
|
| 31 |
+
return NotImplemented
|
| 32 |
+
return attrs.astuple(self) == attrs.astuple(other)
|
| 33 |
+
|
| 34 |
+
def __hash__(self) -> int:
|
| 35 |
+
return hash(attrs.astuple(self))
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@frozen
|
| 39 |
+
class NoInternalID(Exception):
|
| 40 |
+
"""
|
| 41 |
+
A resource has no internal ID, but one is needed.
|
| 42 |
+
|
| 43 |
+
E.g. in modern JSON Schema drafts, this is the :kw:`$id` keyword.
|
| 44 |
+
|
| 45 |
+
One might be needed if a resource was to-be added to a registry but no
|
| 46 |
+
other URI is available, and the resource doesn't declare its canonical URI.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
resource: Resource[Any]
|
| 50 |
+
|
| 51 |
+
def __eq__(self, other: object) -> bool:
|
| 52 |
+
if self.__class__ is not other.__class__:
|
| 53 |
+
return NotImplemented
|
| 54 |
+
return attrs.astuple(self) == attrs.astuple(other)
|
| 55 |
+
|
| 56 |
+
def __hash__(self) -> int:
|
| 57 |
+
return hash(attrs.astuple(self))
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@frozen
|
| 61 |
+
class Unretrievable(KeyError):
|
| 62 |
+
"""
|
| 63 |
+
The given URI is not present in a registry, and retrieving it failed.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
ref: URI
|
| 67 |
+
|
| 68 |
+
def __eq__(self, other: object) -> bool:
|
| 69 |
+
if self.__class__ is not other.__class__:
|
| 70 |
+
return NotImplemented
|
| 71 |
+
return attrs.astuple(self) == attrs.astuple(other)
|
| 72 |
+
|
| 73 |
+
def __hash__(self) -> int:
|
| 74 |
+
return hash(attrs.astuple(self))
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@frozen
|
| 78 |
+
class CannotDetermineSpecification(Exception):
|
| 79 |
+
"""
|
| 80 |
+
Attempting to detect the appropriate `Specification` failed.
|
| 81 |
+
|
| 82 |
+
This happens if no discernible information is found in the contents of the
|
| 83 |
+
new resource which would help identify it.
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
contents: Any
|
| 87 |
+
|
| 88 |
+
def __eq__(self, other: object) -> bool:
|
| 89 |
+
if self.__class__ is not other.__class__:
|
| 90 |
+
return NotImplemented
|
| 91 |
+
return attrs.astuple(self) == attrs.astuple(other)
|
| 92 |
+
|
| 93 |
+
def __hash__(self) -> int:
|
| 94 |
+
return hash(attrs.astuple(self))
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@attrs.frozen # Because here we allow subclassing below.
|
| 98 |
+
class Unresolvable(Exception):
|
| 99 |
+
"""
|
| 100 |
+
A reference was unresolvable.
|
| 101 |
+
"""
|
| 102 |
+
|
| 103 |
+
ref: URI
|
| 104 |
+
|
| 105 |
+
def __eq__(self, other: object) -> bool:
|
| 106 |
+
if self.__class__ is not other.__class__:
|
| 107 |
+
return NotImplemented
|
| 108 |
+
return attrs.astuple(self) == attrs.astuple(other)
|
| 109 |
+
|
| 110 |
+
def __hash__(self) -> int:
|
| 111 |
+
return hash(attrs.astuple(self))
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
@frozen
|
| 115 |
+
class PointerToNowhere(Unresolvable):
|
| 116 |
+
"""
|
| 117 |
+
A JSON Pointer leads to a part of a document that does not exist.
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
resource: Resource[Any]
|
| 121 |
+
|
| 122 |
+
def __str__(self) -> str:
|
| 123 |
+
msg = f"{self.ref!r} does not exist within {self.resource.contents!r}"
|
| 124 |
+
if self.ref == "/":
|
| 125 |
+
msg += (
|
| 126 |
+
". The pointer '/' is a valid JSON Pointer but it points to "
|
| 127 |
+
"an empty string property ''. If you intended to point "
|
| 128 |
+
"to the entire resource, you should use '#'."
|
| 129 |
+
)
|
| 130 |
+
return msg
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
@frozen
|
| 134 |
+
class NoSuchAnchor(Unresolvable):
|
| 135 |
+
"""
|
| 136 |
+
An anchor does not exist within a particular resource.
|
| 137 |
+
"""
|
| 138 |
+
|
| 139 |
+
resource: Resource[Any]
|
| 140 |
+
anchor: str
|
| 141 |
+
|
| 142 |
+
def __str__(self) -> str:
|
| 143 |
+
return (
|
| 144 |
+
f"{self.anchor!r} does not exist within {self.resource.contents!r}"
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
@frozen
|
| 149 |
+
class InvalidAnchor(Unresolvable):
|
| 150 |
+
"""
|
| 151 |
+
An anchor which could never exist in a resource was dereferenced.
|
| 152 |
+
|
| 153 |
+
It is somehow syntactically invalid.
|
| 154 |
+
"""
|
| 155 |
+
|
| 156 |
+
resource: Resource[Any]
|
| 157 |
+
anchor: str
|
| 158 |
+
|
| 159 |
+
def __str__(self) -> str:
|
| 160 |
+
return (
|
| 161 |
+
f"'#{self.anchor}' is not a valid anchor, neither as a "
|
| 162 |
+
"plain name anchor nor as a JSON Pointer. You may have intended "
|
| 163 |
+
f"to use '#/{self.anchor}', as the slash is required *before each "
|
| 164 |
+
"segment* of a JSON pointer."
|
| 165 |
+
)
|
.venv/lib/python3.11/site-packages/referencing/jsonschema.py
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Referencing implementations for JSON Schema specs (historic & current).
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from collections.abc import Iterable, Sequence, Set
|
| 8 |
+
from typing import Any, Union
|
| 9 |
+
|
| 10 |
+
from referencing import Anchor, Registry, Resource, Specification, exceptions
|
| 11 |
+
from referencing._attrs import frozen
|
| 12 |
+
from referencing._core import (
|
| 13 |
+
_UNSET, # type: ignore[reportPrivateUsage]
|
| 14 |
+
Resolved as _Resolved,
|
| 15 |
+
Resolver as _Resolver,
|
| 16 |
+
_Unset, # type: ignore[reportPrivateUsage]
|
| 17 |
+
)
|
| 18 |
+
from referencing.typing import URI, Anchor as AnchorType, Mapping
|
| 19 |
+
|
| 20 |
+
#: A JSON Schema which is a JSON object
|
| 21 |
+
ObjectSchema = Mapping[str, Any]
|
| 22 |
+
|
| 23 |
+
#: A JSON Schema of any kind
|
| 24 |
+
Schema = Union[bool, ObjectSchema]
|
| 25 |
+
|
| 26 |
+
#: A Resource whose contents are JSON Schemas
|
| 27 |
+
SchemaResource = Resource[Schema]
|
| 28 |
+
|
| 29 |
+
#: A JSON Schema Registry
|
| 30 |
+
SchemaRegistry = Registry[Schema]
|
| 31 |
+
|
| 32 |
+
#: The empty JSON Schema Registry
|
| 33 |
+
EMPTY_REGISTRY: SchemaRegistry = Registry()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@frozen
|
| 37 |
+
class UnknownDialect(Exception):
|
| 38 |
+
"""
|
| 39 |
+
A dialect identifier was found for a dialect unknown by this library.
|
| 40 |
+
|
| 41 |
+
If it's a custom ("unofficial") dialect, be sure you've registered it.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
uri: URI
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _dollar_id(contents: Schema) -> URI | None:
|
| 48 |
+
if isinstance(contents, bool):
|
| 49 |
+
return
|
| 50 |
+
return contents.get("$id")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _legacy_dollar_id(contents: Schema) -> URI | None:
|
| 54 |
+
if isinstance(contents, bool) or "$ref" in contents:
|
| 55 |
+
return
|
| 56 |
+
id = contents.get("$id")
|
| 57 |
+
if id is not None and not id.startswith("#"):
|
| 58 |
+
return id
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _legacy_id(contents: ObjectSchema) -> URI | None:
|
| 62 |
+
if "$ref" in contents:
|
| 63 |
+
return
|
| 64 |
+
id = contents.get("id")
|
| 65 |
+
if id is not None and not id.startswith("#"):
|
| 66 |
+
return id
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _anchor(
|
| 70 |
+
specification: Specification[Schema],
|
| 71 |
+
contents: Schema,
|
| 72 |
+
) -> Iterable[AnchorType[Schema]]:
|
| 73 |
+
if isinstance(contents, bool):
|
| 74 |
+
return
|
| 75 |
+
anchor = contents.get("$anchor")
|
| 76 |
+
if anchor is not None:
|
| 77 |
+
yield Anchor(
|
| 78 |
+
name=anchor,
|
| 79 |
+
resource=specification.create_resource(contents),
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
dynamic_anchor = contents.get("$dynamicAnchor")
|
| 83 |
+
if dynamic_anchor is not None:
|
| 84 |
+
yield DynamicAnchor(
|
| 85 |
+
name=dynamic_anchor,
|
| 86 |
+
resource=specification.create_resource(contents),
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _anchor_2019(
|
| 91 |
+
specification: Specification[Schema],
|
| 92 |
+
contents: Schema,
|
| 93 |
+
) -> Iterable[Anchor[Schema]]:
|
| 94 |
+
if isinstance(contents, bool):
|
| 95 |
+
return []
|
| 96 |
+
anchor = contents.get("$anchor")
|
| 97 |
+
if anchor is None:
|
| 98 |
+
return []
|
| 99 |
+
return [
|
| 100 |
+
Anchor(
|
| 101 |
+
name=anchor,
|
| 102 |
+
resource=specification.create_resource(contents),
|
| 103 |
+
),
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _legacy_anchor_in_dollar_id(
|
| 108 |
+
specification: Specification[Schema],
|
| 109 |
+
contents: Schema,
|
| 110 |
+
) -> Iterable[Anchor[Schema]]:
|
| 111 |
+
if isinstance(contents, bool):
|
| 112 |
+
return []
|
| 113 |
+
id = contents.get("$id", "")
|
| 114 |
+
if not id.startswith("#"):
|
| 115 |
+
return []
|
| 116 |
+
return [
|
| 117 |
+
Anchor(
|
| 118 |
+
name=id[1:],
|
| 119 |
+
resource=specification.create_resource(contents),
|
| 120 |
+
),
|
| 121 |
+
]
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _legacy_anchor_in_id(
|
| 125 |
+
specification: Specification[ObjectSchema],
|
| 126 |
+
contents: ObjectSchema,
|
| 127 |
+
) -> Iterable[Anchor[ObjectSchema]]:
|
| 128 |
+
id = contents.get("id", "")
|
| 129 |
+
if not id.startswith("#"):
|
| 130 |
+
return []
|
| 131 |
+
return [
|
| 132 |
+
Anchor(
|
| 133 |
+
name=id[1:],
|
| 134 |
+
resource=specification.create_resource(contents),
|
| 135 |
+
),
|
| 136 |
+
]
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _subresources_of(
|
| 140 |
+
in_value: Set[str] = frozenset(),
|
| 141 |
+
in_subvalues: Set[str] = frozenset(),
|
| 142 |
+
in_subarray: Set[str] = frozenset(),
|
| 143 |
+
):
|
| 144 |
+
"""
|
| 145 |
+
Create a callable returning JSON Schema specification-style subschemas.
|
| 146 |
+
|
| 147 |
+
Relies on specifying the set of keywords containing subschemas in their
|
| 148 |
+
values, in a subobject's values, or in a subarray.
|
| 149 |
+
"""
|
| 150 |
+
|
| 151 |
+
def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
|
| 152 |
+
if isinstance(contents, bool):
|
| 153 |
+
return
|
| 154 |
+
for each in in_value:
|
| 155 |
+
if each in contents:
|
| 156 |
+
yield contents[each]
|
| 157 |
+
for each in in_subarray:
|
| 158 |
+
if each in contents:
|
| 159 |
+
yield from contents[each]
|
| 160 |
+
for each in in_subvalues:
|
| 161 |
+
if each in contents:
|
| 162 |
+
yield from contents[each].values()
|
| 163 |
+
|
| 164 |
+
return subresources_of
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def _subresources_of_with_crazy_items(
|
| 168 |
+
in_value: Set[str] = frozenset(),
|
| 169 |
+
in_subvalues: Set[str] = frozenset(),
|
| 170 |
+
in_subarray: Set[str] = frozenset(),
|
| 171 |
+
):
|
| 172 |
+
"""
|
| 173 |
+
Specifically handle older drafts where there are some funky keywords.
|
| 174 |
+
"""
|
| 175 |
+
|
| 176 |
+
def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
|
| 177 |
+
if isinstance(contents, bool):
|
| 178 |
+
return
|
| 179 |
+
for each in in_value:
|
| 180 |
+
if each in contents:
|
| 181 |
+
yield contents[each]
|
| 182 |
+
for each in in_subarray:
|
| 183 |
+
if each in contents:
|
| 184 |
+
yield from contents[each]
|
| 185 |
+
for each in in_subvalues:
|
| 186 |
+
if each in contents:
|
| 187 |
+
yield from contents[each].values()
|
| 188 |
+
|
| 189 |
+
items = contents.get("items")
|
| 190 |
+
if items is not None:
|
| 191 |
+
if isinstance(items, Sequence):
|
| 192 |
+
yield from items
|
| 193 |
+
else:
|
| 194 |
+
yield items
|
| 195 |
+
|
| 196 |
+
return subresources_of
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def _subresources_of_with_crazy_items_dependencies(
|
| 200 |
+
in_value: Set[str] = frozenset(),
|
| 201 |
+
in_subvalues: Set[str] = frozenset(),
|
| 202 |
+
in_subarray: Set[str] = frozenset(),
|
| 203 |
+
):
|
| 204 |
+
"""
|
| 205 |
+
Specifically handle older drafts where there are some funky keywords.
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
|
| 209 |
+
if isinstance(contents, bool):
|
| 210 |
+
return
|
| 211 |
+
for each in in_value:
|
| 212 |
+
if each in contents:
|
| 213 |
+
yield contents[each]
|
| 214 |
+
for each in in_subarray:
|
| 215 |
+
if each in contents:
|
| 216 |
+
yield from contents[each]
|
| 217 |
+
for each in in_subvalues:
|
| 218 |
+
if each in contents:
|
| 219 |
+
yield from contents[each].values()
|
| 220 |
+
|
| 221 |
+
items = contents.get("items")
|
| 222 |
+
if items is not None:
|
| 223 |
+
if isinstance(items, Sequence):
|
| 224 |
+
yield from items
|
| 225 |
+
else:
|
| 226 |
+
yield items
|
| 227 |
+
dependencies = contents.get("dependencies")
|
| 228 |
+
if dependencies is not None:
|
| 229 |
+
values = iter(dependencies.values())
|
| 230 |
+
value = next(values, None)
|
| 231 |
+
if isinstance(value, Mapping):
|
| 232 |
+
yield value
|
| 233 |
+
yield from values
|
| 234 |
+
|
| 235 |
+
return subresources_of
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def _subresources_of_with_crazy_aP_items_dependencies(
|
| 239 |
+
in_value: Set[str] = frozenset(),
|
| 240 |
+
in_subvalues: Set[str] = frozenset(),
|
| 241 |
+
in_subarray: Set[str] = frozenset(),
|
| 242 |
+
):
|
| 243 |
+
"""
|
| 244 |
+
Specifically handle even older drafts where there are some funky keywords.
|
| 245 |
+
"""
|
| 246 |
+
|
| 247 |
+
def subresources_of(contents: ObjectSchema) -> Iterable[ObjectSchema]:
|
| 248 |
+
for each in in_value:
|
| 249 |
+
if each in contents:
|
| 250 |
+
yield contents[each]
|
| 251 |
+
for each in in_subarray:
|
| 252 |
+
if each in contents:
|
| 253 |
+
yield from contents[each]
|
| 254 |
+
for each in in_subvalues:
|
| 255 |
+
if each in contents:
|
| 256 |
+
yield from contents[each].values()
|
| 257 |
+
|
| 258 |
+
items = contents.get("items")
|
| 259 |
+
if items is not None:
|
| 260 |
+
if isinstance(items, Sequence):
|
| 261 |
+
yield from items
|
| 262 |
+
else:
|
| 263 |
+
yield items
|
| 264 |
+
dependencies = contents.get("dependencies")
|
| 265 |
+
if dependencies is not None:
|
| 266 |
+
values = iter(dependencies.values())
|
| 267 |
+
value = next(values, None)
|
| 268 |
+
if isinstance(value, Mapping):
|
| 269 |
+
yield value
|
| 270 |
+
yield from values
|
| 271 |
+
|
| 272 |
+
for each in "additionalItems", "additionalProperties":
|
| 273 |
+
value = contents.get(each)
|
| 274 |
+
if isinstance(value, Mapping):
|
| 275 |
+
yield value
|
| 276 |
+
|
| 277 |
+
return subresources_of
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def _maybe_in_subresource(
|
| 281 |
+
in_value: Set[str] = frozenset(),
|
| 282 |
+
in_subvalues: Set[str] = frozenset(),
|
| 283 |
+
in_subarray: Set[str] = frozenset(),
|
| 284 |
+
):
|
| 285 |
+
in_child = in_subvalues | in_subarray
|
| 286 |
+
|
| 287 |
+
def maybe_in_subresource(
|
| 288 |
+
segments: Sequence[int | str],
|
| 289 |
+
resolver: _Resolver[Any],
|
| 290 |
+
subresource: Resource[Any],
|
| 291 |
+
) -> _Resolver[Any]:
|
| 292 |
+
_segments = iter(segments)
|
| 293 |
+
for segment in _segments:
|
| 294 |
+
if segment not in in_value and (
|
| 295 |
+
segment not in in_child or next(_segments, None) is None
|
| 296 |
+
):
|
| 297 |
+
return resolver
|
| 298 |
+
return resolver.in_subresource(subresource)
|
| 299 |
+
|
| 300 |
+
return maybe_in_subresource
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def _maybe_in_subresource_crazy_items(
|
| 304 |
+
in_value: Set[str] = frozenset(),
|
| 305 |
+
in_subvalues: Set[str] = frozenset(),
|
| 306 |
+
in_subarray: Set[str] = frozenset(),
|
| 307 |
+
):
|
| 308 |
+
in_child = in_subvalues | in_subarray
|
| 309 |
+
|
| 310 |
+
def maybe_in_subresource(
|
| 311 |
+
segments: Sequence[int | str],
|
| 312 |
+
resolver: _Resolver[Any],
|
| 313 |
+
subresource: Resource[Any],
|
| 314 |
+
) -> _Resolver[Any]:
|
| 315 |
+
_segments = iter(segments)
|
| 316 |
+
for segment in _segments:
|
| 317 |
+
if segment == "items" and isinstance(
|
| 318 |
+
subresource.contents,
|
| 319 |
+
Mapping,
|
| 320 |
+
):
|
| 321 |
+
return resolver.in_subresource(subresource)
|
| 322 |
+
if segment not in in_value and (
|
| 323 |
+
segment not in in_child or next(_segments, None) is None
|
| 324 |
+
):
|
| 325 |
+
return resolver
|
| 326 |
+
return resolver.in_subresource(subresource)
|
| 327 |
+
|
| 328 |
+
return maybe_in_subresource
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
def _maybe_in_subresource_crazy_items_dependencies(
|
| 332 |
+
in_value: Set[str] = frozenset(),
|
| 333 |
+
in_subvalues: Set[str] = frozenset(),
|
| 334 |
+
in_subarray: Set[str] = frozenset(),
|
| 335 |
+
):
|
| 336 |
+
in_child = in_subvalues | in_subarray
|
| 337 |
+
|
| 338 |
+
def maybe_in_subresource(
|
| 339 |
+
segments: Sequence[int | str],
|
| 340 |
+
resolver: _Resolver[Any],
|
| 341 |
+
subresource: Resource[Any],
|
| 342 |
+
) -> _Resolver[Any]:
|
| 343 |
+
_segments = iter(segments)
|
| 344 |
+
for segment in _segments:
|
| 345 |
+
if segment in {"items", "dependencies"} and isinstance(
|
| 346 |
+
subresource.contents,
|
| 347 |
+
Mapping,
|
| 348 |
+
):
|
| 349 |
+
return resolver.in_subresource(subresource)
|
| 350 |
+
if segment not in in_value and (
|
| 351 |
+
segment not in in_child or next(_segments, None) is None
|
| 352 |
+
):
|
| 353 |
+
return resolver
|
| 354 |
+
return resolver.in_subresource(subresource)
|
| 355 |
+
|
| 356 |
+
return maybe_in_subresource
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
#: JSON Schema draft 2020-12
|
| 360 |
+
DRAFT202012 = Specification(
|
| 361 |
+
name="draft2020-12",
|
| 362 |
+
id_of=_dollar_id,
|
| 363 |
+
subresources_of=_subresources_of(
|
| 364 |
+
in_value={
|
| 365 |
+
"additionalProperties",
|
| 366 |
+
"contains",
|
| 367 |
+
"contentSchema",
|
| 368 |
+
"else",
|
| 369 |
+
"if",
|
| 370 |
+
"items",
|
| 371 |
+
"not",
|
| 372 |
+
"propertyNames",
|
| 373 |
+
"then",
|
| 374 |
+
"unevaluatedItems",
|
| 375 |
+
"unevaluatedProperties",
|
| 376 |
+
},
|
| 377 |
+
in_subarray={"allOf", "anyOf", "oneOf", "prefixItems"},
|
| 378 |
+
in_subvalues={
|
| 379 |
+
"$defs",
|
| 380 |
+
"definitions",
|
| 381 |
+
"dependentSchemas",
|
| 382 |
+
"patternProperties",
|
| 383 |
+
"properties",
|
| 384 |
+
},
|
| 385 |
+
),
|
| 386 |
+
anchors_in=_anchor,
|
| 387 |
+
maybe_in_subresource=_maybe_in_subresource(
|
| 388 |
+
in_value={
|
| 389 |
+
"additionalProperties",
|
| 390 |
+
"contains",
|
| 391 |
+
"contentSchema",
|
| 392 |
+
"else",
|
| 393 |
+
"if",
|
| 394 |
+
"items",
|
| 395 |
+
"not",
|
| 396 |
+
"propertyNames",
|
| 397 |
+
"then",
|
| 398 |
+
"unevaluatedItems",
|
| 399 |
+
"unevaluatedProperties",
|
| 400 |
+
},
|
| 401 |
+
in_subarray={"allOf", "anyOf", "oneOf", "prefixItems"},
|
| 402 |
+
in_subvalues={
|
| 403 |
+
"$defs",
|
| 404 |
+
"definitions",
|
| 405 |
+
"dependentSchemas",
|
| 406 |
+
"patternProperties",
|
| 407 |
+
"properties",
|
| 408 |
+
},
|
| 409 |
+
),
|
| 410 |
+
)
|
| 411 |
+
#: JSON Schema draft 2019-09
|
| 412 |
+
DRAFT201909 = Specification(
|
| 413 |
+
name="draft2019-09",
|
| 414 |
+
id_of=_dollar_id,
|
| 415 |
+
subresources_of=_subresources_of_with_crazy_items(
|
| 416 |
+
in_value={
|
| 417 |
+
"additionalItems",
|
| 418 |
+
"additionalProperties",
|
| 419 |
+
"contains",
|
| 420 |
+
"contentSchema",
|
| 421 |
+
"else",
|
| 422 |
+
"if",
|
| 423 |
+
"not",
|
| 424 |
+
"propertyNames",
|
| 425 |
+
"then",
|
| 426 |
+
"unevaluatedItems",
|
| 427 |
+
"unevaluatedProperties",
|
| 428 |
+
},
|
| 429 |
+
in_subarray={"allOf", "anyOf", "oneOf"},
|
| 430 |
+
in_subvalues={
|
| 431 |
+
"$defs",
|
| 432 |
+
"definitions",
|
| 433 |
+
"dependentSchemas",
|
| 434 |
+
"patternProperties",
|
| 435 |
+
"properties",
|
| 436 |
+
},
|
| 437 |
+
),
|
| 438 |
+
anchors_in=_anchor_2019,
|
| 439 |
+
maybe_in_subresource=_maybe_in_subresource_crazy_items(
|
| 440 |
+
in_value={
|
| 441 |
+
"additionalItems",
|
| 442 |
+
"additionalProperties",
|
| 443 |
+
"contains",
|
| 444 |
+
"contentSchema",
|
| 445 |
+
"else",
|
| 446 |
+
"if",
|
| 447 |
+
"not",
|
| 448 |
+
"propertyNames",
|
| 449 |
+
"then",
|
| 450 |
+
"unevaluatedItems",
|
| 451 |
+
"unevaluatedProperties",
|
| 452 |
+
},
|
| 453 |
+
in_subarray={"allOf", "anyOf", "oneOf"},
|
| 454 |
+
in_subvalues={
|
| 455 |
+
"$defs",
|
| 456 |
+
"definitions",
|
| 457 |
+
"dependentSchemas",
|
| 458 |
+
"patternProperties",
|
| 459 |
+
"properties",
|
| 460 |
+
},
|
| 461 |
+
),
|
| 462 |
+
)
|
| 463 |
+
#: JSON Schema draft 7
|
| 464 |
+
DRAFT7 = Specification(
|
| 465 |
+
name="draft-07",
|
| 466 |
+
id_of=_legacy_dollar_id,
|
| 467 |
+
subresources_of=_subresources_of_with_crazy_items_dependencies(
|
| 468 |
+
in_value={
|
| 469 |
+
"additionalItems",
|
| 470 |
+
"additionalProperties",
|
| 471 |
+
"contains",
|
| 472 |
+
"else",
|
| 473 |
+
"if",
|
| 474 |
+
"not",
|
| 475 |
+
"propertyNames",
|
| 476 |
+
"then",
|
| 477 |
+
},
|
| 478 |
+
in_subarray={"allOf", "anyOf", "oneOf"},
|
| 479 |
+
in_subvalues={"definitions", "patternProperties", "properties"},
|
| 480 |
+
),
|
| 481 |
+
anchors_in=_legacy_anchor_in_dollar_id,
|
| 482 |
+
maybe_in_subresource=_maybe_in_subresource_crazy_items_dependencies(
|
| 483 |
+
in_value={
|
| 484 |
+
"additionalItems",
|
| 485 |
+
"additionalProperties",
|
| 486 |
+
"contains",
|
| 487 |
+
"else",
|
| 488 |
+
"if",
|
| 489 |
+
"not",
|
| 490 |
+
"propertyNames",
|
| 491 |
+
"then",
|
| 492 |
+
},
|
| 493 |
+
in_subarray={"allOf", "anyOf", "oneOf"},
|
| 494 |
+
in_subvalues={"definitions", "patternProperties", "properties"},
|
| 495 |
+
),
|
| 496 |
+
)
|
| 497 |
+
#: JSON Schema draft 6
|
| 498 |
+
DRAFT6 = Specification(
|
| 499 |
+
name="draft-06",
|
| 500 |
+
id_of=_legacy_dollar_id,
|
| 501 |
+
subresources_of=_subresources_of_with_crazy_items_dependencies(
|
| 502 |
+
in_value={
|
| 503 |
+
"additionalItems",
|
| 504 |
+
"additionalProperties",
|
| 505 |
+
"contains",
|
| 506 |
+
"not",
|
| 507 |
+
"propertyNames",
|
| 508 |
+
},
|
| 509 |
+
in_subarray={"allOf", "anyOf", "oneOf"},
|
| 510 |
+
in_subvalues={"definitions", "patternProperties", "properties"},
|
| 511 |
+
),
|
| 512 |
+
anchors_in=_legacy_anchor_in_dollar_id,
|
| 513 |
+
maybe_in_subresource=_maybe_in_subresource_crazy_items_dependencies(
|
| 514 |
+
in_value={
|
| 515 |
+
"additionalItems",
|
| 516 |
+
"additionalProperties",
|
| 517 |
+
"contains",
|
| 518 |
+
"not",
|
| 519 |
+
"propertyNames",
|
| 520 |
+
},
|
| 521 |
+
in_subarray={"allOf", "anyOf", "oneOf"},
|
| 522 |
+
in_subvalues={"definitions", "patternProperties", "properties"},
|
| 523 |
+
),
|
| 524 |
+
)
|
| 525 |
+
#: JSON Schema draft 4
|
| 526 |
+
DRAFT4 = Specification(
|
| 527 |
+
name="draft-04",
|
| 528 |
+
id_of=_legacy_id,
|
| 529 |
+
subresources_of=_subresources_of_with_crazy_aP_items_dependencies(
|
| 530 |
+
in_value={"not"},
|
| 531 |
+
in_subarray={"allOf", "anyOf", "oneOf"},
|
| 532 |
+
in_subvalues={"definitions", "patternProperties", "properties"},
|
| 533 |
+
),
|
| 534 |
+
anchors_in=_legacy_anchor_in_id,
|
| 535 |
+
maybe_in_subresource=_maybe_in_subresource_crazy_items_dependencies(
|
| 536 |
+
in_value={"additionalItems", "additionalProperties", "not"},
|
| 537 |
+
in_subarray={"allOf", "anyOf", "oneOf"},
|
| 538 |
+
in_subvalues={"definitions", "patternProperties", "properties"},
|
| 539 |
+
),
|
| 540 |
+
)
|
| 541 |
+
#: JSON Schema draft 3
|
| 542 |
+
DRAFT3 = Specification(
|
| 543 |
+
name="draft-03",
|
| 544 |
+
id_of=_legacy_id,
|
| 545 |
+
subresources_of=_subresources_of_with_crazy_aP_items_dependencies(
|
| 546 |
+
in_subarray={"extends"},
|
| 547 |
+
in_subvalues={"definitions", "patternProperties", "properties"},
|
| 548 |
+
),
|
| 549 |
+
anchors_in=_legacy_anchor_in_id,
|
| 550 |
+
maybe_in_subresource=_maybe_in_subresource_crazy_items_dependencies(
|
| 551 |
+
in_value={"additionalItems", "additionalProperties"},
|
| 552 |
+
in_subarray={"extends"},
|
| 553 |
+
in_subvalues={"definitions", "patternProperties", "properties"},
|
| 554 |
+
),
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
|
| 558 |
+
_SPECIFICATIONS: Registry[Specification[Schema]] = Registry(
|
| 559 |
+
{
|
| 560 |
+
dialect_id: Resource.opaque(specification)
|
| 561 |
+
for dialect_id, specification in [
|
| 562 |
+
("https://json-schema.org/draft/2020-12/schema", DRAFT202012),
|
| 563 |
+
("https://json-schema.org/draft/2019-09/schema", DRAFT201909),
|
| 564 |
+
("http://json-schema.org/draft-07/schema", DRAFT7),
|
| 565 |
+
("http://json-schema.org/draft-06/schema", DRAFT6),
|
| 566 |
+
("http://json-schema.org/draft-04/schema", DRAFT4),
|
| 567 |
+
("http://json-schema.org/draft-03/schema", DRAFT3),
|
| 568 |
+
]
|
| 569 |
+
},
|
| 570 |
+
)
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
def specification_with(
|
| 574 |
+
dialect_id: URI,
|
| 575 |
+
default: Specification[Any] | _Unset = _UNSET,
|
| 576 |
+
) -> Specification[Any]:
|
| 577 |
+
"""
|
| 578 |
+
Retrieve the `Specification` with the given dialect identifier.
|
| 579 |
+
|
| 580 |
+
Raises:
|
| 581 |
+
|
| 582 |
+
`UnknownDialect`
|
| 583 |
+
|
| 584 |
+
if the given ``dialect_id`` isn't known
|
| 585 |
+
|
| 586 |
+
"""
|
| 587 |
+
resource = _SPECIFICATIONS.get(dialect_id.rstrip("#"))
|
| 588 |
+
if resource is not None:
|
| 589 |
+
return resource.contents
|
| 590 |
+
if default is _UNSET:
|
| 591 |
+
raise UnknownDialect(dialect_id)
|
| 592 |
+
return default
|
| 593 |
+
|
| 594 |
+
|
| 595 |
+
@frozen
|
| 596 |
+
class DynamicAnchor:
|
| 597 |
+
"""
|
| 598 |
+
Dynamic anchors, introduced in draft 2020.
|
| 599 |
+
"""
|
| 600 |
+
|
| 601 |
+
name: str
|
| 602 |
+
resource: SchemaResource
|
| 603 |
+
|
| 604 |
+
def resolve(self, resolver: _Resolver[Schema]) -> _Resolved[Schema]:
|
| 605 |
+
"""
|
| 606 |
+
Resolve this anchor dynamically.
|
| 607 |
+
"""
|
| 608 |
+
last = self.resource
|
| 609 |
+
for uri, registry in resolver.dynamic_scope():
|
| 610 |
+
try:
|
| 611 |
+
anchor = registry.anchor(uri, self.name).value
|
| 612 |
+
except exceptions.NoSuchAnchor:
|
| 613 |
+
continue
|
| 614 |
+
if isinstance(anchor, DynamicAnchor):
|
| 615 |
+
last = anchor.resource
|
| 616 |
+
return _Resolved(
|
| 617 |
+
contents=last.contents,
|
| 618 |
+
resolver=resolver.in_subresource(last),
|
| 619 |
+
)
|
| 620 |
+
|
| 621 |
+
|
| 622 |
+
def lookup_recursive_ref(resolver: _Resolver[Schema]) -> _Resolved[Schema]:
|
| 623 |
+
"""
|
| 624 |
+
Recursive references (via recursive anchors), present only in draft 2019.
|
| 625 |
+
|
| 626 |
+
As per the 2019 specification (§ 8.2.4.2.1), only the ``#`` recursive
|
| 627 |
+
reference is supported (and is therefore assumed to be the relevant
|
| 628 |
+
reference).
|
| 629 |
+
"""
|
| 630 |
+
resolved = resolver.lookup("#")
|
| 631 |
+
if isinstance(resolved.contents, Mapping) and resolved.contents.get(
|
| 632 |
+
"$recursiveAnchor",
|
| 633 |
+
):
|
| 634 |
+
for uri, _ in resolver.dynamic_scope():
|
| 635 |
+
next_resolved = resolver.lookup(uri)
|
| 636 |
+
if not isinstance(
|
| 637 |
+
next_resolved.contents,
|
| 638 |
+
Mapping,
|
| 639 |
+
) or not next_resolved.contents.get("$recursiveAnchor"):
|
| 640 |
+
break
|
| 641 |
+
resolved = next_resolved
|
| 642 |
+
return resolved
|
.venv/lib/python3.11/site-packages/referencing/py.typed
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/referencing/retrieval.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Helpers related to (dynamic) resource retrieval.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from functools import lru_cache
|
| 8 |
+
from typing import TYPE_CHECKING, Callable
|
| 9 |
+
import json
|
| 10 |
+
|
| 11 |
+
try:
|
| 12 |
+
from typing_extensions import TypeVar
|
| 13 |
+
except ImportError: # pragma: no cover
|
| 14 |
+
from typing import TypeVar
|
| 15 |
+
|
| 16 |
+
from referencing import Resource
|
| 17 |
+
|
| 18 |
+
if TYPE_CHECKING:
|
| 19 |
+
from referencing.typing import URI, D, Retrieve
|
| 20 |
+
|
| 21 |
+
#: A serialized document (e.g. a JSON string)
|
| 22 |
+
_T = TypeVar("_T", default=str)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def to_cached_resource(
|
| 26 |
+
cache: Callable[[Retrieve[D]], Retrieve[D]] | None = None,
|
| 27 |
+
loads: Callable[[_T], D] = json.loads,
|
| 28 |
+
from_contents: Callable[[D], Resource[D]] = Resource.from_contents,
|
| 29 |
+
) -> Callable[[Callable[[URI], _T]], Retrieve[D]]:
|
| 30 |
+
"""
|
| 31 |
+
Create a retriever which caches its return values from a simpler callable.
|
| 32 |
+
|
| 33 |
+
Takes a function which returns things like serialized JSON (strings) and
|
| 34 |
+
returns something suitable for passing to `Registry` as a retrieve
|
| 35 |
+
function.
|
| 36 |
+
|
| 37 |
+
This decorator both reduces a small bit of boilerplate for a common case
|
| 38 |
+
(deserializing JSON from strings and creating `Resource` objects from the
|
| 39 |
+
result) as well as makes the probable need for caching a bit easier.
|
| 40 |
+
Retrievers which otherwise do expensive operations (like hitting the
|
| 41 |
+
network) might otherwise be called repeatedly.
|
| 42 |
+
|
| 43 |
+
Examples
|
| 44 |
+
--------
|
| 45 |
+
|
| 46 |
+
.. testcode::
|
| 47 |
+
|
| 48 |
+
from referencing import Registry
|
| 49 |
+
from referencing.typing import URI
|
| 50 |
+
import referencing.retrieval
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@referencing.retrieval.to_cached_resource()
|
| 54 |
+
def retrieve(uri: URI):
|
| 55 |
+
print(f"Retrieved {uri}")
|
| 56 |
+
|
| 57 |
+
# Normally, go get some expensive JSON from the network, a file ...
|
| 58 |
+
return '''
|
| 59 |
+
{
|
| 60 |
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
| 61 |
+
"foo": "bar"
|
| 62 |
+
}
|
| 63 |
+
'''
|
| 64 |
+
|
| 65 |
+
one = Registry(retrieve=retrieve).get_or_retrieve("urn:example:foo")
|
| 66 |
+
print(one.value.contents["foo"])
|
| 67 |
+
|
| 68 |
+
# Retrieving the same URI again reuses the same value (and thus doesn't
|
| 69 |
+
# print another retrieval message here)
|
| 70 |
+
two = Registry(retrieve=retrieve).get_or_retrieve("urn:example:foo")
|
| 71 |
+
print(two.value.contents["foo"])
|
| 72 |
+
|
| 73 |
+
.. testoutput::
|
| 74 |
+
|
| 75 |
+
Retrieved urn:example:foo
|
| 76 |
+
bar
|
| 77 |
+
bar
|
| 78 |
+
|
| 79 |
+
"""
|
| 80 |
+
if cache is None:
|
| 81 |
+
cache = lru_cache(maxsize=None)
|
| 82 |
+
|
| 83 |
+
def decorator(retrieve: Callable[[URI], _T]):
|
| 84 |
+
@cache
|
| 85 |
+
def cached_retrieve(uri: URI):
|
| 86 |
+
response = retrieve(uri)
|
| 87 |
+
contents = loads(response)
|
| 88 |
+
return from_contents(contents)
|
| 89 |
+
|
| 90 |
+
return cached_retrieve
|
| 91 |
+
|
| 92 |
+
return decorator
|
.venv/lib/python3.11/site-packages/referencing/tests/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/referencing/tests/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (190 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/tests/__pycache__/test_core.cpython-311.pyc
ADDED
|
Binary file (69.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/tests/__pycache__/test_exceptions.cpython-311.pyc
ADDED
|
Binary file (2.95 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/tests/__pycache__/test_jsonschema.cpython-311.pyc
ADDED
|
Binary file (13.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/tests/__pycache__/test_referencing_suite.cpython-311.pyc
ADDED
|
Binary file (5.17 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/tests/__pycache__/test_retrieval.cpython-311.pyc
ADDED
|
Binary file (6.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/referencing/tests/test_core.py
ADDED
|
@@ -0,0 +1,1057 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rpds import HashTrieMap
|
| 2 |
+
import pytest
|
| 3 |
+
|
| 4 |
+
from referencing import Anchor, Registry, Resource, Specification, exceptions
|
| 5 |
+
from referencing.jsonschema import DRAFT202012
|
| 6 |
+
|
| 7 |
+
ID_AND_CHILDREN = Specification(
|
| 8 |
+
name="id-and-children",
|
| 9 |
+
id_of=lambda contents: contents.get("ID"),
|
| 10 |
+
subresources_of=lambda contents: contents.get("children", []),
|
| 11 |
+
anchors_in=lambda specification, contents: [
|
| 12 |
+
Anchor(
|
| 13 |
+
name=name,
|
| 14 |
+
resource=specification.create_resource(contents=each),
|
| 15 |
+
)
|
| 16 |
+
for name, each in contents.get("anchors", {}).items()
|
| 17 |
+
],
|
| 18 |
+
maybe_in_subresource=lambda segments, resolver, subresource: (
|
| 19 |
+
resolver.in_subresource(subresource)
|
| 20 |
+
if not len(segments) % 2
|
| 21 |
+
and all(each == "children" for each in segments[::2])
|
| 22 |
+
else resolver
|
| 23 |
+
),
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def blow_up(uri): # pragma: no cover
|
| 28 |
+
"""
|
| 29 |
+
A retriever suitable for use in tests which expect it never to be used.
|
| 30 |
+
"""
|
| 31 |
+
raise RuntimeError("This retrieve function expects to never be called!")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class TestRegistry:
|
| 35 |
+
def test_with_resource(self):
|
| 36 |
+
"""
|
| 37 |
+
Adding a resource to the registry then allows re-retrieving it.
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
resource = Resource.opaque(contents={"foo": "bar"})
|
| 41 |
+
uri = "urn:example"
|
| 42 |
+
registry = Registry().with_resource(uri=uri, resource=resource)
|
| 43 |
+
assert registry[uri] is resource
|
| 44 |
+
|
| 45 |
+
def test_with_resources(self):
|
| 46 |
+
"""
|
| 47 |
+
Adding multiple resources to the registry is like adding each one.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
one = Resource.opaque(contents={})
|
| 51 |
+
two = Resource(contents={"foo": "bar"}, specification=ID_AND_CHILDREN)
|
| 52 |
+
registry = Registry().with_resources(
|
| 53 |
+
[
|
| 54 |
+
("http://example.com/1", one),
|
| 55 |
+
("http://example.com/foo/bar", two),
|
| 56 |
+
],
|
| 57 |
+
)
|
| 58 |
+
assert registry == Registry().with_resource(
|
| 59 |
+
uri="http://example.com/1",
|
| 60 |
+
resource=one,
|
| 61 |
+
).with_resource(
|
| 62 |
+
uri="http://example.com/foo/bar",
|
| 63 |
+
resource=two,
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
def test_matmul_resource(self):
|
| 67 |
+
uri = "urn:example:resource"
|
| 68 |
+
resource = ID_AND_CHILDREN.create_resource({"ID": uri, "foo": 12})
|
| 69 |
+
registry = resource @ Registry()
|
| 70 |
+
assert registry == Registry().with_resource(uri, resource)
|
| 71 |
+
|
| 72 |
+
def test_matmul_many_resources(self):
|
| 73 |
+
one_uri = "urn:example:one"
|
| 74 |
+
one = ID_AND_CHILDREN.create_resource({"ID": one_uri, "foo": 12})
|
| 75 |
+
|
| 76 |
+
two_uri = "urn:example:two"
|
| 77 |
+
two = ID_AND_CHILDREN.create_resource({"ID": two_uri, "foo": 12})
|
| 78 |
+
|
| 79 |
+
registry = [one, two] @ Registry()
|
| 80 |
+
assert registry == Registry().with_resources(
|
| 81 |
+
[(one_uri, one), (two_uri, two)],
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
def test_matmul_resource_without_id(self):
|
| 85 |
+
resource = Resource.opaque(contents={"foo": "bar"})
|
| 86 |
+
with pytest.raises(exceptions.NoInternalID) as e:
|
| 87 |
+
resource @ Registry()
|
| 88 |
+
assert e.value == exceptions.NoInternalID(resource=resource)
|
| 89 |
+
|
| 90 |
+
def test_with_contents_from_json_schema(self):
|
| 91 |
+
uri = "urn:example"
|
| 92 |
+
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
| 93 |
+
registry = Registry().with_contents([(uri, schema)])
|
| 94 |
+
|
| 95 |
+
expected = Resource(contents=schema, specification=DRAFT202012)
|
| 96 |
+
assert registry[uri] == expected
|
| 97 |
+
|
| 98 |
+
def test_with_contents_and_default_specification(self):
|
| 99 |
+
uri = "urn:example"
|
| 100 |
+
registry = Registry().with_contents(
|
| 101 |
+
[(uri, {"foo": "bar"})],
|
| 102 |
+
default_specification=Specification.OPAQUE,
|
| 103 |
+
)
|
| 104 |
+
assert registry[uri] == Resource.opaque({"foo": "bar"})
|
| 105 |
+
|
| 106 |
+
def test_len(self):
|
| 107 |
+
total = 5
|
| 108 |
+
registry = Registry().with_contents(
|
| 109 |
+
[(str(i), {"foo": "bar"}) for i in range(total)],
|
| 110 |
+
default_specification=Specification.OPAQUE,
|
| 111 |
+
)
|
| 112 |
+
assert len(registry) == total
|
| 113 |
+
|
| 114 |
+
def test_bool_empty(self):
|
| 115 |
+
assert not Registry()
|
| 116 |
+
|
| 117 |
+
def test_bool_not_empty(self):
|
| 118 |
+
registry = Registry().with_contents(
|
| 119 |
+
[(str(i), {"foo": "bar"}) for i in range(3)],
|
| 120 |
+
default_specification=Specification.OPAQUE,
|
| 121 |
+
)
|
| 122 |
+
assert registry
|
| 123 |
+
|
| 124 |
+
def test_iter(self):
|
| 125 |
+
registry = Registry().with_contents(
|
| 126 |
+
[(str(i), {"foo": "bar"}) for i in range(8)],
|
| 127 |
+
default_specification=Specification.OPAQUE,
|
| 128 |
+
)
|
| 129 |
+
assert set(registry) == {str(i) for i in range(8)}
|
| 130 |
+
|
| 131 |
+
def test_crawl_still_has_top_level_resource(self):
|
| 132 |
+
resource = Resource.opaque({"foo": "bar"})
|
| 133 |
+
uri = "urn:example"
|
| 134 |
+
registry = Registry({uri: resource}).crawl()
|
| 135 |
+
assert registry[uri] is resource
|
| 136 |
+
|
| 137 |
+
def test_crawl_finds_a_subresource(self):
|
| 138 |
+
child_id = "urn:child"
|
| 139 |
+
root = ID_AND_CHILDREN.create_resource(
|
| 140 |
+
{"ID": "urn:root", "children": [{"ID": child_id, "foo": 12}]},
|
| 141 |
+
)
|
| 142 |
+
registry = root @ Registry()
|
| 143 |
+
with pytest.raises(LookupError):
|
| 144 |
+
registry[child_id]
|
| 145 |
+
|
| 146 |
+
expected = ID_AND_CHILDREN.create_resource({"ID": child_id, "foo": 12})
|
| 147 |
+
assert registry.crawl()[child_id] == expected
|
| 148 |
+
|
| 149 |
+
def test_crawl_finds_anchors_with_id(self):
|
| 150 |
+
resource = ID_AND_CHILDREN.create_resource(
|
| 151 |
+
{"ID": "urn:bar", "anchors": {"foo": 12}},
|
| 152 |
+
)
|
| 153 |
+
registry = resource @ Registry()
|
| 154 |
+
|
| 155 |
+
assert registry.crawl().anchor(resource.id(), "foo").value == Anchor(
|
| 156 |
+
name="foo",
|
| 157 |
+
resource=ID_AND_CHILDREN.create_resource(12),
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
def test_crawl_finds_anchors_no_id(self):
|
| 161 |
+
resource = ID_AND_CHILDREN.create_resource({"anchors": {"foo": 12}})
|
| 162 |
+
registry = Registry().with_resource("urn:root", resource)
|
| 163 |
+
|
| 164 |
+
assert registry.crawl().anchor("urn:root", "foo").value == Anchor(
|
| 165 |
+
name="foo",
|
| 166 |
+
resource=ID_AND_CHILDREN.create_resource(12),
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
def test_contents(self):
|
| 170 |
+
resource = Resource.opaque({"foo": "bar"})
|
| 171 |
+
uri = "urn:example"
|
| 172 |
+
registry = Registry().with_resource(uri, resource)
|
| 173 |
+
assert registry.contents(uri) == {"foo": "bar"}
|
| 174 |
+
|
| 175 |
+
def test_getitem_strips_empty_fragments(self):
|
| 176 |
+
uri = "http://example.com/"
|
| 177 |
+
resource = ID_AND_CHILDREN.create_resource({"ID": uri + "#"})
|
| 178 |
+
registry = resource @ Registry()
|
| 179 |
+
assert registry[uri] == registry[uri + "#"] == resource
|
| 180 |
+
|
| 181 |
+
def test_contents_strips_empty_fragments(self):
|
| 182 |
+
uri = "http://example.com/"
|
| 183 |
+
resource = ID_AND_CHILDREN.create_resource({"ID": uri + "#"})
|
| 184 |
+
registry = resource @ Registry()
|
| 185 |
+
assert (
|
| 186 |
+
registry.contents(uri)
|
| 187 |
+
== registry.contents(uri + "#")
|
| 188 |
+
== {"ID": uri + "#"}
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
def test_contents_nonexistent_resource(self):
|
| 192 |
+
registry = Registry()
|
| 193 |
+
with pytest.raises(exceptions.NoSuchResource) as e:
|
| 194 |
+
registry.contents("urn:example")
|
| 195 |
+
assert e.value == exceptions.NoSuchResource(ref="urn:example")
|
| 196 |
+
|
| 197 |
+
def test_crawled_anchor(self):
|
| 198 |
+
resource = ID_AND_CHILDREN.create_resource({"anchors": {"foo": "bar"}})
|
| 199 |
+
registry = Registry().with_resource("urn:example", resource)
|
| 200 |
+
retrieved = registry.anchor("urn:example", "foo")
|
| 201 |
+
assert retrieved.value == Anchor(
|
| 202 |
+
name="foo",
|
| 203 |
+
resource=ID_AND_CHILDREN.create_resource("bar"),
|
| 204 |
+
)
|
| 205 |
+
assert retrieved.registry == registry.crawl()
|
| 206 |
+
|
| 207 |
+
def test_anchor_in_nonexistent_resource(self):
|
| 208 |
+
registry = Registry()
|
| 209 |
+
with pytest.raises(exceptions.NoSuchResource) as e:
|
| 210 |
+
registry.anchor("urn:example", "foo")
|
| 211 |
+
assert e.value == exceptions.NoSuchResource(ref="urn:example")
|
| 212 |
+
|
| 213 |
+
def test_init(self):
|
| 214 |
+
one = Resource.opaque(contents={})
|
| 215 |
+
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
|
| 216 |
+
registry = Registry(
|
| 217 |
+
{
|
| 218 |
+
"http://example.com/1": one,
|
| 219 |
+
"http://example.com/foo/bar": two,
|
| 220 |
+
},
|
| 221 |
+
)
|
| 222 |
+
assert (
|
| 223 |
+
registry
|
| 224 |
+
== Registry()
|
| 225 |
+
.with_resources(
|
| 226 |
+
[
|
| 227 |
+
("http://example.com/1", one),
|
| 228 |
+
("http://example.com/foo/bar", two),
|
| 229 |
+
],
|
| 230 |
+
)
|
| 231 |
+
.crawl()
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
def test_dict_conversion(self):
|
| 235 |
+
"""
|
| 236 |
+
Passing a `dict` to `Registry` gets converted to a `HashTrieMap`.
|
| 237 |
+
|
| 238 |
+
So continuing to use the registry works.
|
| 239 |
+
"""
|
| 240 |
+
|
| 241 |
+
one = Resource.opaque(contents={})
|
| 242 |
+
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
|
| 243 |
+
registry = Registry(
|
| 244 |
+
{"http://example.com/1": one},
|
| 245 |
+
).with_resource("http://example.com/foo/bar", two)
|
| 246 |
+
assert (
|
| 247 |
+
registry.crawl()
|
| 248 |
+
== Registry()
|
| 249 |
+
.with_resources(
|
| 250 |
+
[
|
| 251 |
+
("http://example.com/1", one),
|
| 252 |
+
("http://example.com/foo/bar", two),
|
| 253 |
+
],
|
| 254 |
+
)
|
| 255 |
+
.crawl()
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
def test_no_such_resource(self):
|
| 259 |
+
registry = Registry()
|
| 260 |
+
with pytest.raises(exceptions.NoSuchResource) as e:
|
| 261 |
+
registry["urn:bigboom"]
|
| 262 |
+
assert e.value == exceptions.NoSuchResource(ref="urn:bigboom")
|
| 263 |
+
|
| 264 |
+
def test_combine(self):
|
| 265 |
+
one = Resource.opaque(contents={})
|
| 266 |
+
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
|
| 267 |
+
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
|
| 268 |
+
four = ID_AND_CHILDREN.create_resource({"anchors": {"foo": 12}})
|
| 269 |
+
|
| 270 |
+
first = Registry({"http://example.com/1": one})
|
| 271 |
+
second = Registry().with_resource("http://example.com/foo/bar", two)
|
| 272 |
+
third = Registry(
|
| 273 |
+
{
|
| 274 |
+
"http://example.com/1": one,
|
| 275 |
+
"http://example.com/baz": three,
|
| 276 |
+
},
|
| 277 |
+
)
|
| 278 |
+
fourth = (
|
| 279 |
+
Registry()
|
| 280 |
+
.with_resource(
|
| 281 |
+
"http://example.com/foo/quux",
|
| 282 |
+
four,
|
| 283 |
+
)
|
| 284 |
+
.crawl()
|
| 285 |
+
)
|
| 286 |
+
assert first.combine(second, third, fourth) == Registry(
|
| 287 |
+
[
|
| 288 |
+
("http://example.com/1", one),
|
| 289 |
+
("http://example.com/baz", three),
|
| 290 |
+
("http://example.com/foo/quux", four),
|
| 291 |
+
],
|
| 292 |
+
anchors=HashTrieMap(
|
| 293 |
+
{
|
| 294 |
+
("http://example.com/foo/quux", "foo"): Anchor(
|
| 295 |
+
name="foo",
|
| 296 |
+
resource=ID_AND_CHILDREN.create_resource(12),
|
| 297 |
+
),
|
| 298 |
+
},
|
| 299 |
+
),
|
| 300 |
+
).with_resource("http://example.com/foo/bar", two)
|
| 301 |
+
|
| 302 |
+
def test_combine_self(self):
|
| 303 |
+
"""
|
| 304 |
+
Combining a registry with itself short-circuits.
|
| 305 |
+
|
| 306 |
+
This is a performance optimization -- otherwise we do lots more work
|
| 307 |
+
(in jsonschema this seems to correspond to making the test suite take
|
| 308 |
+
*3x* longer).
|
| 309 |
+
"""
|
| 310 |
+
|
| 311 |
+
registry = Registry({"urn:foo": "bar"})
|
| 312 |
+
assert registry.combine(registry) is registry
|
| 313 |
+
|
| 314 |
+
def test_combine_with_uncrawled_resources(self):
|
| 315 |
+
one = Resource.opaque(contents={})
|
| 316 |
+
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
|
| 317 |
+
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
|
| 318 |
+
|
| 319 |
+
first = Registry().with_resource("http://example.com/1", one)
|
| 320 |
+
second = Registry().with_resource("http://example.com/foo/bar", two)
|
| 321 |
+
third = Registry(
|
| 322 |
+
{
|
| 323 |
+
"http://example.com/1": one,
|
| 324 |
+
"http://example.com/baz": three,
|
| 325 |
+
},
|
| 326 |
+
)
|
| 327 |
+
expected = Registry(
|
| 328 |
+
[
|
| 329 |
+
("http://example.com/1", one),
|
| 330 |
+
("http://example.com/foo/bar", two),
|
| 331 |
+
("http://example.com/baz", three),
|
| 332 |
+
],
|
| 333 |
+
)
|
| 334 |
+
combined = first.combine(second, third)
|
| 335 |
+
assert combined != expected
|
| 336 |
+
assert combined.crawl() == expected
|
| 337 |
+
|
| 338 |
+
def test_combine_with_single_retrieve(self):
|
| 339 |
+
one = Resource.opaque(contents={})
|
| 340 |
+
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
|
| 341 |
+
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
|
| 342 |
+
|
| 343 |
+
def retrieve(uri): # pragma: no cover
|
| 344 |
+
pass
|
| 345 |
+
|
| 346 |
+
first = Registry().with_resource("http://example.com/1", one)
|
| 347 |
+
second = Registry(
|
| 348 |
+
retrieve=retrieve,
|
| 349 |
+
).with_resource("http://example.com/2", two)
|
| 350 |
+
third = Registry().with_resource("http://example.com/3", three)
|
| 351 |
+
|
| 352 |
+
assert first.combine(second, third) == Registry(
|
| 353 |
+
retrieve=retrieve,
|
| 354 |
+
).with_resources(
|
| 355 |
+
[
|
| 356 |
+
("http://example.com/1", one),
|
| 357 |
+
("http://example.com/2", two),
|
| 358 |
+
("http://example.com/3", three),
|
| 359 |
+
],
|
| 360 |
+
)
|
| 361 |
+
assert second.combine(first, third) == Registry(
|
| 362 |
+
retrieve=retrieve,
|
| 363 |
+
).with_resources(
|
| 364 |
+
[
|
| 365 |
+
("http://example.com/1", one),
|
| 366 |
+
("http://example.com/2", two),
|
| 367 |
+
("http://example.com/3", three),
|
| 368 |
+
],
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
def test_combine_with_common_retrieve(self):
|
| 372 |
+
one = Resource.opaque(contents={})
|
| 373 |
+
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
|
| 374 |
+
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
|
| 375 |
+
|
| 376 |
+
def retrieve(uri): # pragma: no cover
|
| 377 |
+
pass
|
| 378 |
+
|
| 379 |
+
first = Registry(retrieve=retrieve).with_resource(
|
| 380 |
+
"http://example.com/1",
|
| 381 |
+
one,
|
| 382 |
+
)
|
| 383 |
+
second = Registry(
|
| 384 |
+
retrieve=retrieve,
|
| 385 |
+
).with_resource("http://example.com/2", two)
|
| 386 |
+
third = Registry(retrieve=retrieve).with_resource(
|
| 387 |
+
"http://example.com/3",
|
| 388 |
+
three,
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
assert first.combine(second, third) == Registry(
|
| 392 |
+
retrieve=retrieve,
|
| 393 |
+
).with_resources(
|
| 394 |
+
[
|
| 395 |
+
("http://example.com/1", one),
|
| 396 |
+
("http://example.com/2", two),
|
| 397 |
+
("http://example.com/3", three),
|
| 398 |
+
],
|
| 399 |
+
)
|
| 400 |
+
assert second.combine(first, third) == Registry(
|
| 401 |
+
retrieve=retrieve,
|
| 402 |
+
).with_resources(
|
| 403 |
+
[
|
| 404 |
+
("http://example.com/1", one),
|
| 405 |
+
("http://example.com/2", two),
|
| 406 |
+
("http://example.com/3", three),
|
| 407 |
+
],
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
def test_combine_conflicting_retrieve(self):
|
| 411 |
+
one = Resource.opaque(contents={})
|
| 412 |
+
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
|
| 413 |
+
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
|
| 414 |
+
|
| 415 |
+
def foo_retrieve(uri): # pragma: no cover
|
| 416 |
+
pass
|
| 417 |
+
|
| 418 |
+
def bar_retrieve(uri): # pragma: no cover
|
| 419 |
+
pass
|
| 420 |
+
|
| 421 |
+
first = Registry(retrieve=foo_retrieve).with_resource(
|
| 422 |
+
"http://example.com/1",
|
| 423 |
+
one,
|
| 424 |
+
)
|
| 425 |
+
second = Registry().with_resource("http://example.com/2", two)
|
| 426 |
+
third = Registry(retrieve=bar_retrieve).with_resource(
|
| 427 |
+
"http://example.com/3",
|
| 428 |
+
three,
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
with pytest.raises(Exception, match="conflict.*retriev"):
|
| 432 |
+
first.combine(second, third)
|
| 433 |
+
|
| 434 |
+
def test_remove(self):
|
| 435 |
+
one = Resource.opaque(contents={})
|
| 436 |
+
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
|
| 437 |
+
registry = Registry({"urn:foo": one, "urn:bar": two})
|
| 438 |
+
assert registry.remove("urn:foo") == Registry({"urn:bar": two})
|
| 439 |
+
|
| 440 |
+
def test_remove_uncrawled(self):
|
| 441 |
+
one = Resource.opaque(contents={})
|
| 442 |
+
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
|
| 443 |
+
registry = Registry().with_resources(
|
| 444 |
+
[("urn:foo", one), ("urn:bar", two)],
|
| 445 |
+
)
|
| 446 |
+
assert registry.remove("urn:foo") == Registry().with_resource(
|
| 447 |
+
"urn:bar",
|
| 448 |
+
two,
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
def test_remove_with_anchors(self):
|
| 452 |
+
one = Resource.opaque(contents={})
|
| 453 |
+
two = ID_AND_CHILDREN.create_resource({"anchors": {"foo": "bar"}})
|
| 454 |
+
registry = (
|
| 455 |
+
Registry()
|
| 456 |
+
.with_resources(
|
| 457 |
+
[("urn:foo", one), ("urn:bar", two)],
|
| 458 |
+
)
|
| 459 |
+
.crawl()
|
| 460 |
+
)
|
| 461 |
+
assert (
|
| 462 |
+
registry.remove("urn:bar")
|
| 463 |
+
== Registry()
|
| 464 |
+
.with_resource(
|
| 465 |
+
"urn:foo",
|
| 466 |
+
one,
|
| 467 |
+
)
|
| 468 |
+
.crawl()
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
def test_remove_nonexistent_uri(self):
|
| 472 |
+
with pytest.raises(exceptions.NoSuchResource) as e:
|
| 473 |
+
Registry().remove("urn:doesNotExist")
|
| 474 |
+
assert e.value == exceptions.NoSuchResource(ref="urn:doesNotExist")
|
| 475 |
+
|
| 476 |
+
def test_retrieve(self):
|
| 477 |
+
foo = Resource.opaque({"foo": "bar"})
|
| 478 |
+
registry = Registry(retrieve=lambda uri: foo)
|
| 479 |
+
assert registry.get_or_retrieve("urn:example").value == foo
|
| 480 |
+
|
| 481 |
+
def test_retrieve_arbitrary_exception(self):
|
| 482 |
+
foo = Resource.opaque({"foo": "bar"})
|
| 483 |
+
|
| 484 |
+
def retrieve(uri):
|
| 485 |
+
if uri == "urn:succeed":
|
| 486 |
+
return foo
|
| 487 |
+
raise Exception("Oh no!")
|
| 488 |
+
|
| 489 |
+
registry = Registry(retrieve=retrieve)
|
| 490 |
+
assert registry.get_or_retrieve("urn:succeed").value == foo
|
| 491 |
+
with pytest.raises(exceptions.Unretrievable):
|
| 492 |
+
registry.get_or_retrieve("urn:uhoh")
|
| 493 |
+
|
| 494 |
+
def test_retrieve_no_such_resource(self):
|
| 495 |
+
foo = Resource.opaque({"foo": "bar"})
|
| 496 |
+
|
| 497 |
+
def retrieve(uri):
|
| 498 |
+
if uri == "urn:succeed":
|
| 499 |
+
return foo
|
| 500 |
+
raise exceptions.NoSuchResource(ref=uri)
|
| 501 |
+
|
| 502 |
+
registry = Registry(retrieve=retrieve)
|
| 503 |
+
assert registry.get_or_retrieve("urn:succeed").value == foo
|
| 504 |
+
with pytest.raises(exceptions.NoSuchResource):
|
| 505 |
+
registry.get_or_retrieve("urn:uhoh")
|
| 506 |
+
|
| 507 |
+
def test_retrieve_cannot_determine_specification(self):
|
| 508 |
+
def retrieve(uri):
|
| 509 |
+
return Resource.from_contents({})
|
| 510 |
+
|
| 511 |
+
registry = Registry(retrieve=retrieve)
|
| 512 |
+
with pytest.raises(exceptions.CannotDetermineSpecification):
|
| 513 |
+
registry.get_or_retrieve("urn:uhoh")
|
| 514 |
+
|
| 515 |
+
def test_retrieve_already_available_resource(self):
|
| 516 |
+
foo = Resource.opaque({"foo": "bar"})
|
| 517 |
+
registry = Registry({"urn:example": foo}, retrieve=blow_up)
|
| 518 |
+
assert registry["urn:example"] == foo
|
| 519 |
+
assert registry.get_or_retrieve("urn:example").value == foo
|
| 520 |
+
|
| 521 |
+
def test_retrieve_first_checks_crawlable_resource(self):
|
| 522 |
+
child = ID_AND_CHILDREN.create_resource({"ID": "urn:child", "foo": 12})
|
| 523 |
+
root = ID_AND_CHILDREN.create_resource({"children": [child.contents]})
|
| 524 |
+
registry = Registry(retrieve=blow_up).with_resource("urn:root", root)
|
| 525 |
+
assert registry.crawl()["urn:child"] == child
|
| 526 |
+
|
| 527 |
+
def test_resolver(self):
|
| 528 |
+
one = Resource.opaque(contents={})
|
| 529 |
+
registry = Registry({"http://example.com": one})
|
| 530 |
+
resolver = registry.resolver(base_uri="http://example.com")
|
| 531 |
+
assert resolver.lookup("#").contents == {}
|
| 532 |
+
|
| 533 |
+
def test_resolver_with_root_identified(self):
|
| 534 |
+
root = ID_AND_CHILDREN.create_resource({"ID": "http://example.com"})
|
| 535 |
+
resolver = Registry().resolver_with_root(root)
|
| 536 |
+
assert resolver.lookup("http://example.com").contents == root.contents
|
| 537 |
+
assert resolver.lookup("#").contents == root.contents
|
| 538 |
+
|
| 539 |
+
def test_resolver_with_root_unidentified(self):
|
| 540 |
+
root = Resource.opaque(contents={})
|
| 541 |
+
resolver = Registry().resolver_with_root(root)
|
| 542 |
+
assert resolver.lookup("#").contents == root.contents
|
| 543 |
+
|
| 544 |
+
def test_repr(self):
|
| 545 |
+
one = Resource.opaque(contents={})
|
| 546 |
+
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
|
| 547 |
+
registry = Registry().with_resources(
|
| 548 |
+
[
|
| 549 |
+
("http://example.com/1", one),
|
| 550 |
+
("http://example.com/foo/bar", two),
|
| 551 |
+
],
|
| 552 |
+
)
|
| 553 |
+
assert repr(registry) == "<Registry (2 uncrawled resources)>"
|
| 554 |
+
assert repr(registry.crawl()) == "<Registry (2 resources)>"
|
| 555 |
+
|
| 556 |
+
def test_repr_mixed_crawled(self):
|
| 557 |
+
one = Resource.opaque(contents={})
|
| 558 |
+
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
|
| 559 |
+
registry = (
|
| 560 |
+
Registry(
|
| 561 |
+
{"http://example.com/1": one},
|
| 562 |
+
)
|
| 563 |
+
.crawl()
|
| 564 |
+
.with_resource(uri="http://example.com/foo/bar", resource=two)
|
| 565 |
+
)
|
| 566 |
+
assert repr(registry) == "<Registry (2 resources, 1 uncrawled)>"
|
| 567 |
+
|
| 568 |
+
def test_repr_one_resource(self):
|
| 569 |
+
registry = Registry().with_resource(
|
| 570 |
+
uri="http://example.com/1",
|
| 571 |
+
resource=Resource.opaque(contents={}),
|
| 572 |
+
)
|
| 573 |
+
assert repr(registry) == "<Registry (1 uncrawled resource)>"
|
| 574 |
+
|
| 575 |
+
def test_repr_empty(self):
|
| 576 |
+
assert repr(Registry()) == "<Registry (0 resources)>"
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
class TestResource:
|
| 580 |
+
def test_from_contents_from_json_schema(self):
|
| 581 |
+
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
| 582 |
+
resource = Resource.from_contents(schema)
|
| 583 |
+
assert resource == Resource(contents=schema, specification=DRAFT202012)
|
| 584 |
+
|
| 585 |
+
def test_from_contents_with_no_discernible_information(self):
|
| 586 |
+
"""
|
| 587 |
+
Creating a resource with no discernible way to see what
|
| 588 |
+
specification it belongs to (e.g. no ``$schema`` keyword for JSON
|
| 589 |
+
Schema) raises an error.
|
| 590 |
+
"""
|
| 591 |
+
|
| 592 |
+
with pytest.raises(exceptions.CannotDetermineSpecification):
|
| 593 |
+
Resource.from_contents({"foo": "bar"})
|
| 594 |
+
|
| 595 |
+
def test_from_contents_with_no_discernible_information_and_default(self):
|
| 596 |
+
resource = Resource.from_contents(
|
| 597 |
+
{"foo": "bar"},
|
| 598 |
+
default_specification=Specification.OPAQUE,
|
| 599 |
+
)
|
| 600 |
+
assert resource == Resource.opaque(contents={"foo": "bar"})
|
| 601 |
+
|
| 602 |
+
def test_from_contents_unneeded_default(self):
|
| 603 |
+
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
| 604 |
+
resource = Resource.from_contents(
|
| 605 |
+
schema,
|
| 606 |
+
default_specification=Specification.OPAQUE,
|
| 607 |
+
)
|
| 608 |
+
assert resource == Resource(
|
| 609 |
+
contents=schema,
|
| 610 |
+
specification=DRAFT202012,
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
def test_non_mapping_from_contents(self):
|
| 614 |
+
resource = Resource.from_contents(
|
| 615 |
+
True,
|
| 616 |
+
default_specification=ID_AND_CHILDREN,
|
| 617 |
+
)
|
| 618 |
+
assert resource == Resource(
|
| 619 |
+
contents=True,
|
| 620 |
+
specification=ID_AND_CHILDREN,
|
| 621 |
+
)
|
| 622 |
+
|
| 623 |
+
def test_from_contents_with_fallback(self):
|
| 624 |
+
resource = Resource.from_contents(
|
| 625 |
+
{"foo": "bar"},
|
| 626 |
+
default_specification=Specification.OPAQUE,
|
| 627 |
+
)
|
| 628 |
+
assert resource == Resource.opaque(contents={"foo": "bar"})
|
| 629 |
+
|
| 630 |
+
def test_id_delegates_to_specification(self):
|
| 631 |
+
specification = Specification(
|
| 632 |
+
name="",
|
| 633 |
+
id_of=lambda contents: "urn:fixedID",
|
| 634 |
+
subresources_of=lambda contents: [],
|
| 635 |
+
anchors_in=lambda specification, contents: [],
|
| 636 |
+
maybe_in_subresource=(
|
| 637 |
+
lambda segments, resolver, subresource: resolver
|
| 638 |
+
),
|
| 639 |
+
)
|
| 640 |
+
resource = Resource(
|
| 641 |
+
contents={"foo": "baz"},
|
| 642 |
+
specification=specification,
|
| 643 |
+
)
|
| 644 |
+
assert resource.id() == "urn:fixedID"
|
| 645 |
+
|
| 646 |
+
def test_id_strips_empty_fragment(self):
|
| 647 |
+
uri = "http://example.com/"
|
| 648 |
+
root = ID_AND_CHILDREN.create_resource({"ID": uri + "#"})
|
| 649 |
+
assert root.id() == uri
|
| 650 |
+
|
| 651 |
+
def test_subresources_delegates_to_specification(self):
|
| 652 |
+
resource = ID_AND_CHILDREN.create_resource({"children": [{}, 12]})
|
| 653 |
+
assert list(resource.subresources()) == [
|
| 654 |
+
ID_AND_CHILDREN.create_resource(each) for each in [{}, 12]
|
| 655 |
+
]
|
| 656 |
+
|
| 657 |
+
def test_subresource_with_different_specification(self):
|
| 658 |
+
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
| 659 |
+
resource = ID_AND_CHILDREN.create_resource({"children": [schema]})
|
| 660 |
+
assert list(resource.subresources()) == [
|
| 661 |
+
DRAFT202012.create_resource(schema),
|
| 662 |
+
]
|
| 663 |
+
|
| 664 |
+
def test_anchors_delegates_to_specification(self):
|
| 665 |
+
resource = ID_AND_CHILDREN.create_resource(
|
| 666 |
+
{"anchors": {"foo": {}, "bar": 1, "baz": ""}},
|
| 667 |
+
)
|
| 668 |
+
assert list(resource.anchors()) == [
|
| 669 |
+
Anchor(name="foo", resource=ID_AND_CHILDREN.create_resource({})),
|
| 670 |
+
Anchor(name="bar", resource=ID_AND_CHILDREN.create_resource(1)),
|
| 671 |
+
Anchor(name="baz", resource=ID_AND_CHILDREN.create_resource("")),
|
| 672 |
+
]
|
| 673 |
+
|
| 674 |
+
def test_pointer_to_mapping(self):
|
| 675 |
+
resource = Resource.opaque(contents={"foo": "baz"})
|
| 676 |
+
resolver = Registry().resolver()
|
| 677 |
+
assert resource.pointer("/foo", resolver=resolver).contents == "baz"
|
| 678 |
+
|
| 679 |
+
def test_pointer_to_array(self):
|
| 680 |
+
resource = Resource.opaque(contents={"foo": {"bar": [3]}})
|
| 681 |
+
resolver = Registry().resolver()
|
| 682 |
+
assert resource.pointer("/foo/bar/0", resolver=resolver).contents == 3
|
| 683 |
+
|
| 684 |
+
def test_root_pointer(self):
|
| 685 |
+
contents = {"foo": "baz"}
|
| 686 |
+
resource = Resource.opaque(contents=contents)
|
| 687 |
+
resolver = Registry().resolver()
|
| 688 |
+
assert resource.pointer("", resolver=resolver).contents == contents
|
| 689 |
+
|
| 690 |
+
def test_opaque(self):
|
| 691 |
+
contents = {"foo": "bar"}
|
| 692 |
+
assert Resource.opaque(contents) == Resource(
|
| 693 |
+
contents=contents,
|
| 694 |
+
specification=Specification.OPAQUE,
|
| 695 |
+
)
|
| 696 |
+
|
| 697 |
+
|
| 698 |
+
class TestResolver:
|
| 699 |
+
def test_lookup_exact_uri(self):
|
| 700 |
+
resource = Resource.opaque(contents={"foo": "baz"})
|
| 701 |
+
resolver = Registry({"http://example.com/1": resource}).resolver()
|
| 702 |
+
resolved = resolver.lookup("http://example.com/1")
|
| 703 |
+
assert resolved.contents == resource.contents
|
| 704 |
+
|
| 705 |
+
def test_lookup_subresource(self):
|
| 706 |
+
root = ID_AND_CHILDREN.create_resource(
|
| 707 |
+
{
|
| 708 |
+
"ID": "http://example.com/",
|
| 709 |
+
"children": [
|
| 710 |
+
{"ID": "http://example.com/a", "foo": 12},
|
| 711 |
+
],
|
| 712 |
+
},
|
| 713 |
+
)
|
| 714 |
+
registry = root @ Registry()
|
| 715 |
+
resolved = registry.resolver().lookup("http://example.com/a")
|
| 716 |
+
assert resolved.contents == {"ID": "http://example.com/a", "foo": 12}
|
| 717 |
+
|
| 718 |
+
def test_lookup_anchor_with_id(self):
|
| 719 |
+
root = ID_AND_CHILDREN.create_resource(
|
| 720 |
+
{
|
| 721 |
+
"ID": "http://example.com/",
|
| 722 |
+
"anchors": {"foo": 12},
|
| 723 |
+
},
|
| 724 |
+
)
|
| 725 |
+
registry = root @ Registry()
|
| 726 |
+
resolved = registry.resolver().lookup("http://example.com/#foo")
|
| 727 |
+
assert resolved.contents == 12
|
| 728 |
+
|
| 729 |
+
def test_lookup_anchor_without_id(self):
|
| 730 |
+
root = ID_AND_CHILDREN.create_resource({"anchors": {"foo": 12}})
|
| 731 |
+
resolver = Registry().with_resource("urn:example", root).resolver()
|
| 732 |
+
resolved = resolver.lookup("urn:example#foo")
|
| 733 |
+
assert resolved.contents == 12
|
| 734 |
+
|
| 735 |
+
def test_lookup_unknown_reference(self):
|
| 736 |
+
resolver = Registry().resolver()
|
| 737 |
+
ref = "http://example.com/does/not/exist"
|
| 738 |
+
with pytest.raises(exceptions.Unresolvable) as e:
|
| 739 |
+
resolver.lookup(ref)
|
| 740 |
+
assert e.value == exceptions.Unresolvable(ref=ref)
|
| 741 |
+
|
| 742 |
+
def test_lookup_non_existent_pointer(self):
|
| 743 |
+
resource = Resource.opaque({"foo": {}})
|
| 744 |
+
resolver = Registry({"http://example.com/1": resource}).resolver()
|
| 745 |
+
ref = "http://example.com/1#/foo/bar"
|
| 746 |
+
with pytest.raises(exceptions.Unresolvable) as e:
|
| 747 |
+
resolver.lookup(ref)
|
| 748 |
+
assert e.value == exceptions.PointerToNowhere(
|
| 749 |
+
ref="/foo/bar",
|
| 750 |
+
resource=resource,
|
| 751 |
+
)
|
| 752 |
+
assert str(e.value) == "'/foo/bar' does not exist within {'foo': {}}"
|
| 753 |
+
|
| 754 |
+
def test_lookup_non_existent_pointer_to_array_index(self):
|
| 755 |
+
resource = Resource.opaque([1, 2, 4, 8])
|
| 756 |
+
resolver = Registry({"http://example.com/1": resource}).resolver()
|
| 757 |
+
ref = "http://example.com/1#/10"
|
| 758 |
+
with pytest.raises(exceptions.Unresolvable) as e:
|
| 759 |
+
resolver.lookup(ref)
|
| 760 |
+
assert e.value == exceptions.PointerToNowhere(
|
| 761 |
+
ref="/10",
|
| 762 |
+
resource=resource,
|
| 763 |
+
)
|
| 764 |
+
|
| 765 |
+
def test_lookup_pointer_to_empty_string(self):
|
| 766 |
+
resolver = Registry().resolver_with_root(Resource.opaque({"": {}}))
|
| 767 |
+
assert resolver.lookup("#/").contents == {}
|
| 768 |
+
|
| 769 |
+
def test_lookup_non_existent_pointer_to_empty_string(self):
|
| 770 |
+
resource = Resource.opaque({"foo": {}})
|
| 771 |
+
resolver = Registry().resolver_with_root(resource)
|
| 772 |
+
with pytest.raises(
|
| 773 |
+
exceptions.Unresolvable,
|
| 774 |
+
match="^'/' does not exist within {'foo': {}}.*'#'",
|
| 775 |
+
) as e:
|
| 776 |
+
resolver.lookup("#/")
|
| 777 |
+
assert e.value == exceptions.PointerToNowhere(
|
| 778 |
+
ref="/",
|
| 779 |
+
resource=resource,
|
| 780 |
+
)
|
| 781 |
+
|
| 782 |
+
def test_lookup_non_existent_anchor(self):
|
| 783 |
+
root = ID_AND_CHILDREN.create_resource({"anchors": {}})
|
| 784 |
+
resolver = Registry().with_resource("urn:example", root).resolver()
|
| 785 |
+
resolved = resolver.lookup("urn:example")
|
| 786 |
+
assert resolved.contents == root.contents
|
| 787 |
+
|
| 788 |
+
ref = "urn:example#noSuchAnchor"
|
| 789 |
+
with pytest.raises(exceptions.Unresolvable) as e:
|
| 790 |
+
resolver.lookup(ref)
|
| 791 |
+
assert "'noSuchAnchor' does not exist" in str(e.value)
|
| 792 |
+
assert e.value == exceptions.NoSuchAnchor(
|
| 793 |
+
ref="urn:example",
|
| 794 |
+
resource=root,
|
| 795 |
+
anchor="noSuchAnchor",
|
| 796 |
+
)
|
| 797 |
+
|
| 798 |
+
def test_lookup_invalid_JSON_pointerish_anchor(self):
|
| 799 |
+
resolver = Registry().resolver_with_root(
|
| 800 |
+
ID_AND_CHILDREN.create_resource(
|
| 801 |
+
{
|
| 802 |
+
"ID": "http://example.com/",
|
| 803 |
+
"foo": {"bar": 12},
|
| 804 |
+
},
|
| 805 |
+
),
|
| 806 |
+
)
|
| 807 |
+
|
| 808 |
+
valid = resolver.lookup("#/foo/bar")
|
| 809 |
+
assert valid.contents == 12
|
| 810 |
+
|
| 811 |
+
with pytest.raises(exceptions.InvalidAnchor) as e:
|
| 812 |
+
resolver.lookup("#foo/bar")
|
| 813 |
+
assert " '#/foo/bar'" in str(e.value)
|
| 814 |
+
|
| 815 |
+
def test_lookup_retrieved_resource(self):
|
| 816 |
+
resource = Resource.opaque(contents={"foo": "baz"})
|
| 817 |
+
resolver = Registry(retrieve=lambda uri: resource).resolver()
|
| 818 |
+
resolved = resolver.lookup("http://example.com/")
|
| 819 |
+
assert resolved.contents == resource.contents
|
| 820 |
+
|
| 821 |
+
def test_lookup_failed_retrieved_resource(self):
|
| 822 |
+
"""
|
| 823 |
+
Unretrievable exceptions are also wrapped in Unresolvable.
|
| 824 |
+
"""
|
| 825 |
+
|
| 826 |
+
uri = "http://example.com/"
|
| 827 |
+
|
| 828 |
+
registry = Registry(retrieve=blow_up)
|
| 829 |
+
with pytest.raises(exceptions.Unretrievable):
|
| 830 |
+
registry.get_or_retrieve(uri)
|
| 831 |
+
|
| 832 |
+
resolver = registry.resolver()
|
| 833 |
+
with pytest.raises(exceptions.Unresolvable):
|
| 834 |
+
resolver.lookup(uri)
|
| 835 |
+
|
| 836 |
+
def test_repeated_lookup_from_retrieved_resource(self):
|
| 837 |
+
"""
|
| 838 |
+
A (custom-)retrieved resource is added to the registry returned by
|
| 839 |
+
looking it up.
|
| 840 |
+
"""
|
| 841 |
+
resource = Resource.opaque(contents={"foo": "baz"})
|
| 842 |
+
once = [resource]
|
| 843 |
+
|
| 844 |
+
def retrieve(uri):
|
| 845 |
+
return once.pop()
|
| 846 |
+
|
| 847 |
+
resolver = Registry(retrieve=retrieve).resolver()
|
| 848 |
+
resolved = resolver.lookup("http://example.com/")
|
| 849 |
+
assert resolved.contents == resource.contents
|
| 850 |
+
|
| 851 |
+
resolved = resolved.resolver.lookup("http://example.com/")
|
| 852 |
+
assert resolved.contents == resource.contents
|
| 853 |
+
|
| 854 |
+
def test_repeated_anchor_lookup_from_retrieved_resource(self):
|
| 855 |
+
resource = Resource.opaque(contents={"foo": "baz"})
|
| 856 |
+
once = [resource]
|
| 857 |
+
|
| 858 |
+
def retrieve(uri):
|
| 859 |
+
return once.pop()
|
| 860 |
+
|
| 861 |
+
resolver = Registry(retrieve=retrieve).resolver()
|
| 862 |
+
resolved = resolver.lookup("http://example.com/")
|
| 863 |
+
assert resolved.contents == resource.contents
|
| 864 |
+
|
| 865 |
+
resolved = resolved.resolver.lookup("#")
|
| 866 |
+
assert resolved.contents == resource.contents
|
| 867 |
+
|
| 868 |
+
# FIXME: The tests below aren't really representable in the current
|
| 869 |
+
# suite, though we should probably think of ways to do so.
|
| 870 |
+
|
| 871 |
+
def test_in_subresource(self):
|
| 872 |
+
root = ID_AND_CHILDREN.create_resource(
|
| 873 |
+
{
|
| 874 |
+
"ID": "http://example.com/",
|
| 875 |
+
"children": [
|
| 876 |
+
{
|
| 877 |
+
"ID": "child/",
|
| 878 |
+
"children": [{"ID": "grandchild"}],
|
| 879 |
+
},
|
| 880 |
+
],
|
| 881 |
+
},
|
| 882 |
+
)
|
| 883 |
+
registry = root @ Registry()
|
| 884 |
+
|
| 885 |
+
resolver = registry.resolver()
|
| 886 |
+
first = resolver.lookup("http://example.com/")
|
| 887 |
+
assert first.contents == root.contents
|
| 888 |
+
|
| 889 |
+
with pytest.raises(exceptions.Unresolvable):
|
| 890 |
+
first.resolver.lookup("grandchild")
|
| 891 |
+
|
| 892 |
+
sub = first.resolver.in_subresource(
|
| 893 |
+
ID_AND_CHILDREN.create_resource(first.contents["children"][0]),
|
| 894 |
+
)
|
| 895 |
+
second = sub.lookup("grandchild")
|
| 896 |
+
assert second.contents == {"ID": "grandchild"}
|
| 897 |
+
|
| 898 |
+
def test_in_pointer_subresource(self):
|
| 899 |
+
root = ID_AND_CHILDREN.create_resource(
|
| 900 |
+
{
|
| 901 |
+
"ID": "http://example.com/",
|
| 902 |
+
"children": [
|
| 903 |
+
{
|
| 904 |
+
"ID": "child/",
|
| 905 |
+
"children": [{"ID": "grandchild"}],
|
| 906 |
+
},
|
| 907 |
+
],
|
| 908 |
+
},
|
| 909 |
+
)
|
| 910 |
+
registry = root @ Registry()
|
| 911 |
+
|
| 912 |
+
resolver = registry.resolver()
|
| 913 |
+
first = resolver.lookup("http://example.com/")
|
| 914 |
+
assert first.contents == root.contents
|
| 915 |
+
|
| 916 |
+
with pytest.raises(exceptions.Unresolvable):
|
| 917 |
+
first.resolver.lookup("grandchild")
|
| 918 |
+
|
| 919 |
+
second = first.resolver.lookup("#/children/0")
|
| 920 |
+
third = second.resolver.lookup("grandchild")
|
| 921 |
+
assert third.contents == {"ID": "grandchild"}
|
| 922 |
+
|
| 923 |
+
def test_dynamic_scope(self):
|
| 924 |
+
one = ID_AND_CHILDREN.create_resource(
|
| 925 |
+
{
|
| 926 |
+
"ID": "http://example.com/",
|
| 927 |
+
"children": [
|
| 928 |
+
{
|
| 929 |
+
"ID": "child/",
|
| 930 |
+
"children": [{"ID": "grandchild"}],
|
| 931 |
+
},
|
| 932 |
+
],
|
| 933 |
+
},
|
| 934 |
+
)
|
| 935 |
+
two = ID_AND_CHILDREN.create_resource(
|
| 936 |
+
{
|
| 937 |
+
"ID": "http://example.com/two",
|
| 938 |
+
"children": [{"ID": "two-child/"}],
|
| 939 |
+
},
|
| 940 |
+
)
|
| 941 |
+
registry = [one, two] @ Registry()
|
| 942 |
+
|
| 943 |
+
resolver = registry.resolver()
|
| 944 |
+
first = resolver.lookup("http://example.com/")
|
| 945 |
+
second = first.resolver.lookup("#/children/0")
|
| 946 |
+
third = second.resolver.lookup("grandchild")
|
| 947 |
+
fourth = third.resolver.lookup("http://example.com/two")
|
| 948 |
+
assert list(fourth.resolver.dynamic_scope()) == [
|
| 949 |
+
("http://example.com/child/grandchild", fourth.resolver._registry),
|
| 950 |
+
("http://example.com/child/", fourth.resolver._registry),
|
| 951 |
+
("http://example.com/", fourth.resolver._registry),
|
| 952 |
+
]
|
| 953 |
+
assert list(third.resolver.dynamic_scope()) == [
|
| 954 |
+
("http://example.com/child/", third.resolver._registry),
|
| 955 |
+
("http://example.com/", third.resolver._registry),
|
| 956 |
+
]
|
| 957 |
+
assert list(second.resolver.dynamic_scope()) == [
|
| 958 |
+
("http://example.com/", second.resolver._registry),
|
| 959 |
+
]
|
| 960 |
+
assert list(first.resolver.dynamic_scope()) == []
|
| 961 |
+
|
| 962 |
+
|
| 963 |
+
class TestSpecification:
|
| 964 |
+
def test_create_resource(self):
|
| 965 |
+
specification = Specification(
|
| 966 |
+
name="",
|
| 967 |
+
id_of=lambda contents: "urn:fixedID",
|
| 968 |
+
subresources_of=lambda contents: [],
|
| 969 |
+
anchors_in=lambda specification, contents: [],
|
| 970 |
+
maybe_in_subresource=(
|
| 971 |
+
lambda segments, resolver, subresource: resolver
|
| 972 |
+
),
|
| 973 |
+
)
|
| 974 |
+
resource = specification.create_resource(contents={"foo": "baz"})
|
| 975 |
+
assert resource == Resource(
|
| 976 |
+
contents={"foo": "baz"},
|
| 977 |
+
specification=specification,
|
| 978 |
+
)
|
| 979 |
+
assert resource.id() == "urn:fixedID"
|
| 980 |
+
|
| 981 |
+
def test_detect_from_json_schema(self):
|
| 982 |
+
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
| 983 |
+
specification = Specification.detect(schema)
|
| 984 |
+
assert specification == DRAFT202012
|
| 985 |
+
|
| 986 |
+
def test_detect_with_no_discernible_information(self):
|
| 987 |
+
with pytest.raises(exceptions.CannotDetermineSpecification):
|
| 988 |
+
Specification.detect({"foo": "bar"})
|
| 989 |
+
|
| 990 |
+
def test_detect_with_non_URI_schema(self):
|
| 991 |
+
with pytest.raises(exceptions.CannotDetermineSpecification):
|
| 992 |
+
Specification.detect({"$schema": 37})
|
| 993 |
+
|
| 994 |
+
def test_detect_with_no_discernible_information_and_default(self):
|
| 995 |
+
specification = Specification.OPAQUE.detect({"foo": "bar"})
|
| 996 |
+
assert specification is Specification.OPAQUE
|
| 997 |
+
|
| 998 |
+
def test_detect_unneeded_default(self):
|
| 999 |
+
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
| 1000 |
+
specification = Specification.OPAQUE.detect(schema)
|
| 1001 |
+
assert specification == DRAFT202012
|
| 1002 |
+
|
| 1003 |
+
def test_non_mapping_detect(self):
|
| 1004 |
+
with pytest.raises(exceptions.CannotDetermineSpecification):
|
| 1005 |
+
Specification.detect(True)
|
| 1006 |
+
|
| 1007 |
+
def test_non_mapping_detect_with_default(self):
|
| 1008 |
+
specification = ID_AND_CHILDREN.detect(True)
|
| 1009 |
+
assert specification is ID_AND_CHILDREN
|
| 1010 |
+
|
| 1011 |
+
def test_detect_with_fallback(self):
|
| 1012 |
+
specification = Specification.OPAQUE.detect({"foo": "bar"})
|
| 1013 |
+
assert specification is Specification.OPAQUE
|
| 1014 |
+
|
| 1015 |
+
def test_repr(self):
|
| 1016 |
+
assert (
|
| 1017 |
+
repr(ID_AND_CHILDREN) == "<Specification name='id-and-children'>"
|
| 1018 |
+
)
|
| 1019 |
+
|
| 1020 |
+
|
| 1021 |
+
class TestOpaqueSpecification:
|
| 1022 |
+
THINGS = [{"foo": "bar"}, True, 37, "foo", object()]
|
| 1023 |
+
|
| 1024 |
+
@pytest.mark.parametrize("thing", THINGS)
|
| 1025 |
+
def test_no_id(self, thing):
|
| 1026 |
+
"""
|
| 1027 |
+
An arbitrary thing has no ID.
|
| 1028 |
+
"""
|
| 1029 |
+
|
| 1030 |
+
assert Specification.OPAQUE.id_of(thing) is None
|
| 1031 |
+
|
| 1032 |
+
@pytest.mark.parametrize("thing", THINGS)
|
| 1033 |
+
def test_no_subresources(self, thing):
|
| 1034 |
+
"""
|
| 1035 |
+
An arbitrary thing has no subresources.
|
| 1036 |
+
"""
|
| 1037 |
+
|
| 1038 |
+
assert list(Specification.OPAQUE.subresources_of(thing)) == []
|
| 1039 |
+
|
| 1040 |
+
@pytest.mark.parametrize("thing", THINGS)
|
| 1041 |
+
def test_no_anchors(self, thing):
|
| 1042 |
+
"""
|
| 1043 |
+
An arbitrary thing has no anchors.
|
| 1044 |
+
"""
|
| 1045 |
+
|
| 1046 |
+
assert list(Specification.OPAQUE.anchors_in(thing)) == []
|
| 1047 |
+
|
| 1048 |
+
|
| 1049 |
+
@pytest.mark.parametrize(
|
| 1050 |
+
"cls",
|
| 1051 |
+
[Anchor, Registry, Resource, Specification, exceptions.PointerToNowhere],
|
| 1052 |
+
)
|
| 1053 |
+
def test_nonsubclassable(cls):
|
| 1054 |
+
with pytest.raises(Exception, match="(?i)subclassing"):
|
| 1055 |
+
|
| 1056 |
+
class Boom(cls): # pragma: no cover
|
| 1057 |
+
pass
|
.venv/lib/python3.11/site-packages/referencing/tests/test_exceptions.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import itertools
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from referencing import Resource, exceptions
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def pairs(choices):
|
| 9 |
+
return itertools.combinations(choices, 2)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
TRUE = Resource.opaque(True)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
thunks = (
|
| 16 |
+
lambda: exceptions.CannotDetermineSpecification(TRUE),
|
| 17 |
+
lambda: exceptions.NoSuchResource("urn:example:foo"),
|
| 18 |
+
lambda: exceptions.NoInternalID(TRUE),
|
| 19 |
+
lambda: exceptions.InvalidAnchor(resource=TRUE, anchor="foo", ref="a#b"),
|
| 20 |
+
lambda: exceptions.NoSuchAnchor(resource=TRUE, anchor="foo", ref="a#b"),
|
| 21 |
+
lambda: exceptions.PointerToNowhere(resource=TRUE, ref="urn:example:foo"),
|
| 22 |
+
lambda: exceptions.Unresolvable("urn:example:foo"),
|
| 23 |
+
lambda: exceptions.Unretrievable("urn:example:foo"),
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@pytest.mark.parametrize("one, two", pairs(each() for each in thunks))
|
| 28 |
+
def test_eq_incompatible_types(one, two):
|
| 29 |
+
assert one != two
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@pytest.mark.parametrize("thunk", thunks)
|
| 33 |
+
def test_hash(thunk):
|
| 34 |
+
assert thunk() in {thunk()}
|
.venv/lib/python3.11/site-packages/referencing/tests/test_jsonschema.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
from referencing import Registry, Resource, Specification
|
| 4 |
+
import referencing.jsonschema
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@pytest.mark.parametrize(
|
| 8 |
+
"uri, expected",
|
| 9 |
+
[
|
| 10 |
+
(
|
| 11 |
+
"https://json-schema.org/draft/2020-12/schema",
|
| 12 |
+
referencing.jsonschema.DRAFT202012,
|
| 13 |
+
),
|
| 14 |
+
(
|
| 15 |
+
"https://json-schema.org/draft/2019-09/schema",
|
| 16 |
+
referencing.jsonschema.DRAFT201909,
|
| 17 |
+
),
|
| 18 |
+
(
|
| 19 |
+
"http://json-schema.org/draft-07/schema#",
|
| 20 |
+
referencing.jsonschema.DRAFT7,
|
| 21 |
+
),
|
| 22 |
+
(
|
| 23 |
+
"http://json-schema.org/draft-06/schema#",
|
| 24 |
+
referencing.jsonschema.DRAFT6,
|
| 25 |
+
),
|
| 26 |
+
(
|
| 27 |
+
"http://json-schema.org/draft-04/schema#",
|
| 28 |
+
referencing.jsonschema.DRAFT4,
|
| 29 |
+
),
|
| 30 |
+
(
|
| 31 |
+
"http://json-schema.org/draft-03/schema#",
|
| 32 |
+
referencing.jsonschema.DRAFT3,
|
| 33 |
+
),
|
| 34 |
+
],
|
| 35 |
+
)
|
| 36 |
+
def test_schemas_with_explicit_schema_keywords_are_detected(uri, expected):
|
| 37 |
+
"""
|
| 38 |
+
The $schema keyword in JSON Schema is a dialect identifier.
|
| 39 |
+
"""
|
| 40 |
+
contents = {"$schema": uri}
|
| 41 |
+
resource = Resource.from_contents(contents)
|
| 42 |
+
assert resource == Resource(contents=contents, specification=expected)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_unknown_dialect():
|
| 46 |
+
dialect_id = "http://example.com/unknown-json-schema-dialect-id"
|
| 47 |
+
with pytest.raises(referencing.jsonschema.UnknownDialect) as excinfo:
|
| 48 |
+
Resource.from_contents({"$schema": dialect_id})
|
| 49 |
+
assert excinfo.value.uri == dialect_id
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@pytest.mark.parametrize(
|
| 53 |
+
"id, specification",
|
| 54 |
+
[
|
| 55 |
+
("$id", referencing.jsonschema.DRAFT202012),
|
| 56 |
+
("$id", referencing.jsonschema.DRAFT201909),
|
| 57 |
+
("$id", referencing.jsonschema.DRAFT7),
|
| 58 |
+
("$id", referencing.jsonschema.DRAFT6),
|
| 59 |
+
("id", referencing.jsonschema.DRAFT4),
|
| 60 |
+
("id", referencing.jsonschema.DRAFT3),
|
| 61 |
+
],
|
| 62 |
+
)
|
| 63 |
+
def test_id_of_mapping(id, specification):
|
| 64 |
+
uri = "http://example.com/some-schema"
|
| 65 |
+
assert specification.id_of({id: uri}) == uri
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@pytest.mark.parametrize(
|
| 69 |
+
"specification",
|
| 70 |
+
[
|
| 71 |
+
referencing.jsonschema.DRAFT202012,
|
| 72 |
+
referencing.jsonschema.DRAFT201909,
|
| 73 |
+
referencing.jsonschema.DRAFT7,
|
| 74 |
+
referencing.jsonschema.DRAFT6,
|
| 75 |
+
],
|
| 76 |
+
)
|
| 77 |
+
@pytest.mark.parametrize("value", [True, False])
|
| 78 |
+
def test_id_of_bool(specification, value):
|
| 79 |
+
assert specification.id_of(value) is None
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
@pytest.mark.parametrize(
|
| 83 |
+
"specification",
|
| 84 |
+
[
|
| 85 |
+
referencing.jsonschema.DRAFT202012,
|
| 86 |
+
referencing.jsonschema.DRAFT201909,
|
| 87 |
+
referencing.jsonschema.DRAFT7,
|
| 88 |
+
referencing.jsonschema.DRAFT6,
|
| 89 |
+
],
|
| 90 |
+
)
|
| 91 |
+
@pytest.mark.parametrize("value", [True, False])
|
| 92 |
+
def test_anchors_in_bool(specification, value):
|
| 93 |
+
assert list(specification.anchors_in(value)) == []
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@pytest.mark.parametrize(
|
| 97 |
+
"specification",
|
| 98 |
+
[
|
| 99 |
+
referencing.jsonschema.DRAFT202012,
|
| 100 |
+
referencing.jsonschema.DRAFT201909,
|
| 101 |
+
referencing.jsonschema.DRAFT7,
|
| 102 |
+
referencing.jsonschema.DRAFT6,
|
| 103 |
+
],
|
| 104 |
+
)
|
| 105 |
+
@pytest.mark.parametrize("value", [True, False])
|
| 106 |
+
def test_subresources_of_bool(specification, value):
|
| 107 |
+
assert list(specification.subresources_of(value)) == []
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
@pytest.mark.parametrize(
|
| 111 |
+
"uri, expected",
|
| 112 |
+
[
|
| 113 |
+
(
|
| 114 |
+
"https://json-schema.org/draft/2020-12/schema",
|
| 115 |
+
referencing.jsonschema.DRAFT202012,
|
| 116 |
+
),
|
| 117 |
+
(
|
| 118 |
+
"https://json-schema.org/draft/2019-09/schema",
|
| 119 |
+
referencing.jsonschema.DRAFT201909,
|
| 120 |
+
),
|
| 121 |
+
(
|
| 122 |
+
"http://json-schema.org/draft-07/schema#",
|
| 123 |
+
referencing.jsonschema.DRAFT7,
|
| 124 |
+
),
|
| 125 |
+
(
|
| 126 |
+
"http://json-schema.org/draft-06/schema#",
|
| 127 |
+
referencing.jsonschema.DRAFT6,
|
| 128 |
+
),
|
| 129 |
+
(
|
| 130 |
+
"http://json-schema.org/draft-04/schema#",
|
| 131 |
+
referencing.jsonschema.DRAFT4,
|
| 132 |
+
),
|
| 133 |
+
(
|
| 134 |
+
"http://json-schema.org/draft-03/schema#",
|
| 135 |
+
referencing.jsonschema.DRAFT3,
|
| 136 |
+
),
|
| 137 |
+
],
|
| 138 |
+
)
|
| 139 |
+
def test_specification_with(uri, expected):
|
| 140 |
+
assert referencing.jsonschema.specification_with(uri) == expected
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@pytest.mark.parametrize(
|
| 144 |
+
"uri, expected",
|
| 145 |
+
[
|
| 146 |
+
(
|
| 147 |
+
"http://json-schema.org/draft-07/schema",
|
| 148 |
+
referencing.jsonschema.DRAFT7,
|
| 149 |
+
),
|
| 150 |
+
(
|
| 151 |
+
"http://json-schema.org/draft-06/schema",
|
| 152 |
+
referencing.jsonschema.DRAFT6,
|
| 153 |
+
),
|
| 154 |
+
(
|
| 155 |
+
"http://json-schema.org/draft-04/schema",
|
| 156 |
+
referencing.jsonschema.DRAFT4,
|
| 157 |
+
),
|
| 158 |
+
(
|
| 159 |
+
"http://json-schema.org/draft-03/schema",
|
| 160 |
+
referencing.jsonschema.DRAFT3,
|
| 161 |
+
),
|
| 162 |
+
],
|
| 163 |
+
)
|
| 164 |
+
def test_specification_with_no_empty_fragment(uri, expected):
|
| 165 |
+
assert referencing.jsonschema.specification_with(uri) == expected
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def test_specification_with_unknown_dialect():
|
| 169 |
+
dialect_id = "http://example.com/unknown-json-schema-dialect-id"
|
| 170 |
+
with pytest.raises(referencing.jsonschema.UnknownDialect) as excinfo:
|
| 171 |
+
referencing.jsonschema.specification_with(dialect_id)
|
| 172 |
+
assert excinfo.value.uri == dialect_id
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def test_specification_with_default():
|
| 176 |
+
dialect_id = "http://example.com/unknown-json-schema-dialect-id"
|
| 177 |
+
specification = referencing.jsonschema.specification_with(
|
| 178 |
+
dialect_id,
|
| 179 |
+
default=Specification.OPAQUE,
|
| 180 |
+
)
|
| 181 |
+
assert specification is Specification.OPAQUE
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
# FIXME: The tests below should move to the referencing suite but I haven't yet
|
| 185 |
+
# figured out how to represent dynamic (& recursive) ref lookups in it.
|
| 186 |
+
def test_lookup_trivial_dynamic_ref():
|
| 187 |
+
one = referencing.jsonschema.DRAFT202012.create_resource(
|
| 188 |
+
{"$dynamicAnchor": "foo"},
|
| 189 |
+
)
|
| 190 |
+
resolver = Registry().with_resource("http://example.com", one).resolver()
|
| 191 |
+
resolved = resolver.lookup("http://example.com#foo")
|
| 192 |
+
assert resolved.contents == one.contents
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def test_multiple_lookup_trivial_dynamic_ref():
|
| 196 |
+
TRUE = referencing.jsonschema.DRAFT202012.create_resource(True)
|
| 197 |
+
root = referencing.jsonschema.DRAFT202012.create_resource(
|
| 198 |
+
{
|
| 199 |
+
"$id": "http://example.com",
|
| 200 |
+
"$dynamicAnchor": "fooAnchor",
|
| 201 |
+
"$defs": {
|
| 202 |
+
"foo": {
|
| 203 |
+
"$id": "foo",
|
| 204 |
+
"$dynamicAnchor": "fooAnchor",
|
| 205 |
+
"$defs": {
|
| 206 |
+
"bar": True,
|
| 207 |
+
"baz": {
|
| 208 |
+
"$dynamicAnchor": "fooAnchor",
|
| 209 |
+
},
|
| 210 |
+
},
|
| 211 |
+
},
|
| 212 |
+
},
|
| 213 |
+
},
|
| 214 |
+
)
|
| 215 |
+
resolver = (
|
| 216 |
+
Registry()
|
| 217 |
+
.with_resources(
|
| 218 |
+
[
|
| 219 |
+
("http://example.com", root),
|
| 220 |
+
("http://example.com/foo/", TRUE),
|
| 221 |
+
("http://example.com/foo/bar", root),
|
| 222 |
+
],
|
| 223 |
+
)
|
| 224 |
+
.resolver()
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
first = resolver.lookup("http://example.com")
|
| 228 |
+
second = first.resolver.lookup("foo/")
|
| 229 |
+
resolver = second.resolver.lookup("bar").resolver
|
| 230 |
+
fourth = resolver.lookup("#fooAnchor")
|
| 231 |
+
assert fourth.contents == root.contents
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def test_multiple_lookup_dynamic_ref_to_nondynamic_ref():
|
| 235 |
+
one = referencing.jsonschema.DRAFT202012.create_resource(
|
| 236 |
+
{"$anchor": "fooAnchor"},
|
| 237 |
+
)
|
| 238 |
+
two = referencing.jsonschema.DRAFT202012.create_resource(
|
| 239 |
+
{
|
| 240 |
+
"$id": "http://example.com",
|
| 241 |
+
"$dynamicAnchor": "fooAnchor",
|
| 242 |
+
"$defs": {
|
| 243 |
+
"foo": {
|
| 244 |
+
"$id": "foo",
|
| 245 |
+
"$dynamicAnchor": "fooAnchor",
|
| 246 |
+
"$defs": {
|
| 247 |
+
"bar": True,
|
| 248 |
+
"baz": {
|
| 249 |
+
"$dynamicAnchor": "fooAnchor",
|
| 250 |
+
},
|
| 251 |
+
},
|
| 252 |
+
},
|
| 253 |
+
},
|
| 254 |
+
},
|
| 255 |
+
)
|
| 256 |
+
resolver = (
|
| 257 |
+
Registry()
|
| 258 |
+
.with_resources(
|
| 259 |
+
[
|
| 260 |
+
("http://example.com", two),
|
| 261 |
+
("http://example.com/foo/", one),
|
| 262 |
+
("http://example.com/foo/bar", two),
|
| 263 |
+
],
|
| 264 |
+
)
|
| 265 |
+
.resolver()
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
first = resolver.lookup("http://example.com")
|
| 269 |
+
second = first.resolver.lookup("foo/")
|
| 270 |
+
resolver = second.resolver.lookup("bar").resolver
|
| 271 |
+
fourth = resolver.lookup("#fooAnchor")
|
| 272 |
+
assert fourth.contents == two.contents
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def test_lookup_trivial_recursive_ref():
|
| 276 |
+
one = referencing.jsonschema.DRAFT201909.create_resource(
|
| 277 |
+
{"$recursiveAnchor": True},
|
| 278 |
+
)
|
| 279 |
+
resolver = Registry().with_resource("http://example.com", one).resolver()
|
| 280 |
+
first = resolver.lookup("http://example.com")
|
| 281 |
+
resolved = referencing.jsonschema.lookup_recursive_ref(
|
| 282 |
+
resolver=first.resolver,
|
| 283 |
+
)
|
| 284 |
+
assert resolved.contents == one.contents
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def test_lookup_recursive_ref_to_bool():
|
| 288 |
+
TRUE = referencing.jsonschema.DRAFT201909.create_resource(True)
|
| 289 |
+
registry = Registry({"http://example.com": TRUE})
|
| 290 |
+
resolved = referencing.jsonschema.lookup_recursive_ref(
|
| 291 |
+
resolver=registry.resolver(base_uri="http://example.com"),
|
| 292 |
+
)
|
| 293 |
+
assert resolved.contents == TRUE.contents
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def test_multiple_lookup_recursive_ref_to_bool():
|
| 297 |
+
TRUE = referencing.jsonschema.DRAFT201909.create_resource(True)
|
| 298 |
+
root = referencing.jsonschema.DRAFT201909.create_resource(
|
| 299 |
+
{
|
| 300 |
+
"$id": "http://example.com",
|
| 301 |
+
"$recursiveAnchor": True,
|
| 302 |
+
"$defs": {
|
| 303 |
+
"foo": {
|
| 304 |
+
"$id": "foo",
|
| 305 |
+
"$recursiveAnchor": True,
|
| 306 |
+
"$defs": {
|
| 307 |
+
"bar": True,
|
| 308 |
+
"baz": {
|
| 309 |
+
"$recursiveAnchor": True,
|
| 310 |
+
"$anchor": "fooAnchor",
|
| 311 |
+
},
|
| 312 |
+
},
|
| 313 |
+
},
|
| 314 |
+
},
|
| 315 |
+
},
|
| 316 |
+
)
|
| 317 |
+
resolver = (
|
| 318 |
+
Registry()
|
| 319 |
+
.with_resources(
|
| 320 |
+
[
|
| 321 |
+
("http://example.com", root),
|
| 322 |
+
("http://example.com/foo/", TRUE),
|
| 323 |
+
("http://example.com/foo/bar", root),
|
| 324 |
+
],
|
| 325 |
+
)
|
| 326 |
+
.resolver()
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
first = resolver.lookup("http://example.com")
|
| 330 |
+
second = first.resolver.lookup("foo/")
|
| 331 |
+
resolver = second.resolver.lookup("bar").resolver
|
| 332 |
+
fourth = referencing.jsonschema.lookup_recursive_ref(resolver=resolver)
|
| 333 |
+
assert fourth.contents == root.contents
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def test_multiple_lookup_recursive_ref_with_nonrecursive_ref():
|
| 337 |
+
one = referencing.jsonschema.DRAFT201909.create_resource(
|
| 338 |
+
{"$recursiveAnchor": True},
|
| 339 |
+
)
|
| 340 |
+
two = referencing.jsonschema.DRAFT201909.create_resource(
|
| 341 |
+
{
|
| 342 |
+
"$id": "http://example.com",
|
| 343 |
+
"$recursiveAnchor": True,
|
| 344 |
+
"$defs": {
|
| 345 |
+
"foo": {
|
| 346 |
+
"$id": "foo",
|
| 347 |
+
"$recursiveAnchor": True,
|
| 348 |
+
"$defs": {
|
| 349 |
+
"bar": True,
|
| 350 |
+
"baz": {
|
| 351 |
+
"$recursiveAnchor": True,
|
| 352 |
+
"$anchor": "fooAnchor",
|
| 353 |
+
},
|
| 354 |
+
},
|
| 355 |
+
},
|
| 356 |
+
},
|
| 357 |
+
},
|
| 358 |
+
)
|
| 359 |
+
three = referencing.jsonschema.DRAFT201909.create_resource(
|
| 360 |
+
{"$recursiveAnchor": False},
|
| 361 |
+
)
|
| 362 |
+
resolver = (
|
| 363 |
+
Registry()
|
| 364 |
+
.with_resources(
|
| 365 |
+
[
|
| 366 |
+
("http://example.com", three),
|
| 367 |
+
("http://example.com/foo/", two),
|
| 368 |
+
("http://example.com/foo/bar", one),
|
| 369 |
+
],
|
| 370 |
+
)
|
| 371 |
+
.resolver()
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
first = resolver.lookup("http://example.com")
|
| 375 |
+
second = first.resolver.lookup("foo/")
|
| 376 |
+
resolver = second.resolver.lookup("bar").resolver
|
| 377 |
+
fourth = referencing.jsonschema.lookup_recursive_ref(resolver=resolver)
|
| 378 |
+
assert fourth.contents == two.contents
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def test_empty_registry():
|
| 382 |
+
assert referencing.jsonschema.EMPTY_REGISTRY == Registry()
|
.venv/lib/python3.11/site-packages/referencing/tests/test_referencing_suite.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
from referencing import Registry
|
| 8 |
+
from referencing.exceptions import Unresolvable
|
| 9 |
+
import referencing.jsonschema
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class SuiteNotFound(Exception):
|
| 13 |
+
def __str__(self): # pragma: no cover
|
| 14 |
+
return (
|
| 15 |
+
"Cannot find the referencing suite. "
|
| 16 |
+
"Set the REFERENCING_SUITE environment variable to the path to "
|
| 17 |
+
"the suite, or run the test suite from alongside a full checkout "
|
| 18 |
+
"of the git repository."
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
if "REFERENCING_SUITE" in os.environ: # pragma: no cover
|
| 23 |
+
SUITE = Path(os.environ["REFERENCING_SUITE"]) / "tests"
|
| 24 |
+
else:
|
| 25 |
+
SUITE = Path(__file__).parent.parent.parent / "suite/tests"
|
| 26 |
+
if not SUITE.is_dir(): # pragma: no cover
|
| 27 |
+
raise SuiteNotFound()
|
| 28 |
+
DIALECT_IDS = json.loads(SUITE.joinpath("specifications.json").read_text())
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@pytest.mark.parametrize(
|
| 32 |
+
"test_path",
|
| 33 |
+
[
|
| 34 |
+
pytest.param(each, id=f"{each.parent.name}-{each.stem}")
|
| 35 |
+
for each in SUITE.glob("*/**/*.json")
|
| 36 |
+
],
|
| 37 |
+
)
|
| 38 |
+
def test_referencing_suite(test_path, subtests):
|
| 39 |
+
dialect_id = DIALECT_IDS[test_path.relative_to(SUITE).parts[0]]
|
| 40 |
+
specification = referencing.jsonschema.specification_with(dialect_id)
|
| 41 |
+
loaded = json.loads(test_path.read_text())
|
| 42 |
+
registry = loaded["registry"]
|
| 43 |
+
registry = Registry().with_resources(
|
| 44 |
+
(uri, specification.create_resource(contents))
|
| 45 |
+
for uri, contents in loaded["registry"].items()
|
| 46 |
+
)
|
| 47 |
+
for test in loaded["tests"]:
|
| 48 |
+
with subtests.test(test=test):
|
| 49 |
+
if "normalization" in test_path.stem:
|
| 50 |
+
pytest.xfail("APIs need to change for proper URL support.")
|
| 51 |
+
|
| 52 |
+
resolver = registry.resolver(base_uri=test.get("base_uri", ""))
|
| 53 |
+
|
| 54 |
+
if test.get("error"):
|
| 55 |
+
with pytest.raises(Unresolvable):
|
| 56 |
+
resolver.lookup(test["ref"])
|
| 57 |
+
else:
|
| 58 |
+
resolved = resolver.lookup(test["ref"])
|
| 59 |
+
assert resolved.contents == test["target"]
|
| 60 |
+
|
| 61 |
+
then = test.get("then")
|
| 62 |
+
while then: # pragma: no cover
|
| 63 |
+
with subtests.test(test=test, then=then):
|
| 64 |
+
resolved = resolved.resolver.lookup(then["ref"])
|
| 65 |
+
assert resolved.contents == then["target"]
|
| 66 |
+
then = then.get("then")
|
.venv/lib/python3.11/site-packages/referencing/tests/test_retrieval.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from functools import lru_cache
|
| 2 |
+
import json
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
from referencing import Registry, Resource, exceptions
|
| 7 |
+
from referencing.jsonschema import DRAFT202012
|
| 8 |
+
from referencing.retrieval import to_cached_resource
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TestToCachedResource:
|
| 12 |
+
def test_it_caches_retrieved_resources(self):
|
| 13 |
+
contents = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
| 14 |
+
stack = [json.dumps(contents)]
|
| 15 |
+
|
| 16 |
+
@to_cached_resource()
|
| 17 |
+
def retrieve(uri):
|
| 18 |
+
return stack.pop()
|
| 19 |
+
|
| 20 |
+
registry = Registry(retrieve=retrieve)
|
| 21 |
+
|
| 22 |
+
expected = Resource.from_contents(contents)
|
| 23 |
+
|
| 24 |
+
got = registry.get_or_retrieve("urn:example:schema")
|
| 25 |
+
assert got.value == expected
|
| 26 |
+
|
| 27 |
+
# And a second time we get the same value.
|
| 28 |
+
again = registry.get_or_retrieve("urn:example:schema")
|
| 29 |
+
assert again.value is got.value
|
| 30 |
+
|
| 31 |
+
def test_custom_loader(self):
|
| 32 |
+
contents = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
| 33 |
+
stack = [json.dumps(contents)[::-1]]
|
| 34 |
+
|
| 35 |
+
@to_cached_resource(loads=lambda s: json.loads(s[::-1]))
|
| 36 |
+
def retrieve(uri):
|
| 37 |
+
return stack.pop()
|
| 38 |
+
|
| 39 |
+
registry = Registry(retrieve=retrieve)
|
| 40 |
+
|
| 41 |
+
expected = Resource.from_contents(contents)
|
| 42 |
+
|
| 43 |
+
got = registry.get_or_retrieve("urn:example:schema")
|
| 44 |
+
assert got.value == expected
|
| 45 |
+
|
| 46 |
+
# And a second time we get the same value.
|
| 47 |
+
again = registry.get_or_retrieve("urn:example:schema")
|
| 48 |
+
assert again.value is got.value
|
| 49 |
+
|
| 50 |
+
def test_custom_from_contents(self):
|
| 51 |
+
contents = {}
|
| 52 |
+
stack = [json.dumps(contents)]
|
| 53 |
+
|
| 54 |
+
@to_cached_resource(from_contents=DRAFT202012.create_resource)
|
| 55 |
+
def retrieve(uri):
|
| 56 |
+
return stack.pop()
|
| 57 |
+
|
| 58 |
+
registry = Registry(retrieve=retrieve)
|
| 59 |
+
|
| 60 |
+
expected = DRAFT202012.create_resource(contents)
|
| 61 |
+
|
| 62 |
+
got = registry.get_or_retrieve("urn:example:schema")
|
| 63 |
+
assert got.value == expected
|
| 64 |
+
|
| 65 |
+
# And a second time we get the same value.
|
| 66 |
+
again = registry.get_or_retrieve("urn:example:schema")
|
| 67 |
+
assert again.value is got.value
|
| 68 |
+
|
| 69 |
+
def test_custom_cache(self):
|
| 70 |
+
schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
|
| 71 |
+
mapping = {
|
| 72 |
+
"urn:example:1": dict(schema, foo=1),
|
| 73 |
+
"urn:example:2": dict(schema, foo=2),
|
| 74 |
+
"urn:example:3": dict(schema, foo=3),
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
resources = {
|
| 78 |
+
uri: Resource.from_contents(contents)
|
| 79 |
+
for uri, contents in mapping.items()
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
@to_cached_resource(cache=lru_cache(maxsize=2))
|
| 83 |
+
def retrieve(uri):
|
| 84 |
+
return json.dumps(mapping.pop(uri))
|
| 85 |
+
|
| 86 |
+
registry = Registry(retrieve=retrieve)
|
| 87 |
+
|
| 88 |
+
got = registry.get_or_retrieve("urn:example:1")
|
| 89 |
+
assert got.value == resources["urn:example:1"]
|
| 90 |
+
assert registry.get_or_retrieve("urn:example:1").value is got.value
|
| 91 |
+
assert registry.get_or_retrieve("urn:example:1").value is got.value
|
| 92 |
+
|
| 93 |
+
got = registry.get_or_retrieve("urn:example:2")
|
| 94 |
+
assert got.value == resources["urn:example:2"]
|
| 95 |
+
assert registry.get_or_retrieve("urn:example:2").value is got.value
|
| 96 |
+
assert registry.get_or_retrieve("urn:example:2").value is got.value
|
| 97 |
+
|
| 98 |
+
# This still succeeds, but evicts the first URI
|
| 99 |
+
got = registry.get_or_retrieve("urn:example:3")
|
| 100 |
+
assert got.value == resources["urn:example:3"]
|
| 101 |
+
assert registry.get_or_retrieve("urn:example:3").value is got.value
|
| 102 |
+
assert registry.get_or_retrieve("urn:example:3").value is got.value
|
| 103 |
+
|
| 104 |
+
# And now this fails (as we popped the value out of `mapping`)
|
| 105 |
+
with pytest.raises(exceptions.Unretrievable):
|
| 106 |
+
registry.get_or_retrieve("urn:example:1")
|
.venv/lib/python3.11/site-packages/referencing/typing.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Type-annotation related support for the referencing library.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from collections.abc import Mapping as Mapping
|
| 8 |
+
from typing import TYPE_CHECKING, Any, Protocol
|
| 9 |
+
|
| 10 |
+
try:
|
| 11 |
+
from typing_extensions import TypeVar
|
| 12 |
+
except ImportError: # pragma: no cover
|
| 13 |
+
from typing import TypeVar
|
| 14 |
+
|
| 15 |
+
if TYPE_CHECKING:
|
| 16 |
+
from referencing._core import Resolved, Resolver, Resource
|
| 17 |
+
|
| 18 |
+
#: A URI which identifies a `Resource`.
|
| 19 |
+
URI = str
|
| 20 |
+
|
| 21 |
+
#: The type of documents within a registry.
|
| 22 |
+
D = TypeVar("D", default=Any)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class Retrieve(Protocol[D]):
|
| 26 |
+
"""
|
| 27 |
+
A retrieval callable, usable within a `Registry` for resource retrieval.
|
| 28 |
+
|
| 29 |
+
Does not make assumptions about where the resource might be coming from.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __call__(self, uri: URI) -> Resource[D]:
|
| 33 |
+
"""
|
| 34 |
+
Retrieve the resource with the given URI.
|
| 35 |
+
|
| 36 |
+
Raise `referencing.exceptions.NoSuchResource` if you wish to indicate
|
| 37 |
+
the retriever cannot lookup the given URI.
|
| 38 |
+
"""
|
| 39 |
+
...
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class Anchor(Protocol[D]):
|
| 43 |
+
"""
|
| 44 |
+
An anchor within a `Resource`.
|
| 45 |
+
|
| 46 |
+
Beyond "simple" anchors, some specifications like JSON Schema's 2020
|
| 47 |
+
version have dynamic anchors.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
@property
|
| 51 |
+
def name(self) -> str:
|
| 52 |
+
"""
|
| 53 |
+
Return the name of this anchor.
|
| 54 |
+
"""
|
| 55 |
+
...
|
| 56 |
+
|
| 57 |
+
def resolve(self, resolver: Resolver[D]) -> Resolved[D]:
|
| 58 |
+
"""
|
| 59 |
+
Return the resource for this anchor.
|
| 60 |
+
"""
|
| 61 |
+
...
|
.venv/lib/python3.11/site-packages/starlette/_exception_handler.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing
|
| 4 |
+
|
| 5 |
+
from starlette._utils import is_async_callable
|
| 6 |
+
from starlette.concurrency import run_in_threadpool
|
| 7 |
+
from starlette.exceptions import HTTPException
|
| 8 |
+
from starlette.requests import Request
|
| 9 |
+
from starlette.types import ASGIApp, ExceptionHandler, Message, Receive, Scope, Send
|
| 10 |
+
from starlette.websockets import WebSocket
|
| 11 |
+
|
| 12 |
+
ExceptionHandlers = dict[typing.Any, ExceptionHandler]
|
| 13 |
+
StatusHandlers = dict[int, ExceptionHandler]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _lookup_exception_handler(exc_handlers: ExceptionHandlers, exc: Exception) -> ExceptionHandler | None:
|
| 17 |
+
for cls in type(exc).__mro__:
|
| 18 |
+
if cls in exc_handlers:
|
| 19 |
+
return exc_handlers[cls]
|
| 20 |
+
return None
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def wrap_app_handling_exceptions(app: ASGIApp, conn: Request | WebSocket) -> ASGIApp:
|
| 24 |
+
exception_handlers: ExceptionHandlers
|
| 25 |
+
status_handlers: StatusHandlers
|
| 26 |
+
try:
|
| 27 |
+
exception_handlers, status_handlers = conn.scope["starlette.exception_handlers"]
|
| 28 |
+
except KeyError:
|
| 29 |
+
exception_handlers, status_handlers = {}, {}
|
| 30 |
+
|
| 31 |
+
async def wrapped_app(scope: Scope, receive: Receive, send: Send) -> None:
|
| 32 |
+
response_started = False
|
| 33 |
+
|
| 34 |
+
async def sender(message: Message) -> None:
|
| 35 |
+
nonlocal response_started
|
| 36 |
+
|
| 37 |
+
if message["type"] == "http.response.start":
|
| 38 |
+
response_started = True
|
| 39 |
+
await send(message)
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
await app(scope, receive, sender)
|
| 43 |
+
except Exception as exc:
|
| 44 |
+
handler = None
|
| 45 |
+
|
| 46 |
+
if isinstance(exc, HTTPException):
|
| 47 |
+
handler = status_handlers.get(exc.status_code)
|
| 48 |
+
|
| 49 |
+
if handler is None:
|
| 50 |
+
handler = _lookup_exception_handler(exception_handlers, exc)
|
| 51 |
+
|
| 52 |
+
if handler is None:
|
| 53 |
+
raise exc
|
| 54 |
+
|
| 55 |
+
if response_started:
|
| 56 |
+
raise RuntimeError("Caught handled exception, but response already started.") from exc
|
| 57 |
+
|
| 58 |
+
if is_async_callable(handler):
|
| 59 |
+
response = await handler(conn, exc)
|
| 60 |
+
else:
|
| 61 |
+
response = await run_in_threadpool(handler, conn, exc) # type: ignore
|
| 62 |
+
if response is not None:
|
| 63 |
+
await response(scope, receive, sender)
|
| 64 |
+
|
| 65 |
+
return wrapped_app
|
.venv/lib/python3.11/site-packages/starlette/applications.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import typing
|
| 5 |
+
import warnings
|
| 6 |
+
|
| 7 |
+
if sys.version_info >= (3, 10): # pragma: no cover
|
| 8 |
+
from typing import ParamSpec
|
| 9 |
+
else: # pragma: no cover
|
| 10 |
+
from typing_extensions import ParamSpec
|
| 11 |
+
|
| 12 |
+
from starlette.datastructures import State, URLPath
|
| 13 |
+
from starlette.middleware import Middleware, _MiddlewareFactory
|
| 14 |
+
from starlette.middleware.base import BaseHTTPMiddleware
|
| 15 |
+
from starlette.middleware.errors import ServerErrorMiddleware
|
| 16 |
+
from starlette.middleware.exceptions import ExceptionMiddleware
|
| 17 |
+
from starlette.requests import Request
|
| 18 |
+
from starlette.responses import Response
|
| 19 |
+
from starlette.routing import BaseRoute, Router
|
| 20 |
+
from starlette.types import ASGIApp, ExceptionHandler, Lifespan, Receive, Scope, Send
|
| 21 |
+
from starlette.websockets import WebSocket
|
| 22 |
+
|
| 23 |
+
AppType = typing.TypeVar("AppType", bound="Starlette")
|
| 24 |
+
P = ParamSpec("P")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class Starlette:
|
| 28 |
+
"""Creates an Starlette application."""
|
| 29 |
+
|
| 30 |
+
def __init__(
|
| 31 |
+
self: AppType,
|
| 32 |
+
debug: bool = False,
|
| 33 |
+
routes: typing.Sequence[BaseRoute] | None = None,
|
| 34 |
+
middleware: typing.Sequence[Middleware] | None = None,
|
| 35 |
+
exception_handlers: typing.Mapping[typing.Any, ExceptionHandler] | None = None,
|
| 36 |
+
on_startup: typing.Sequence[typing.Callable[[], typing.Any]] | None = None,
|
| 37 |
+
on_shutdown: typing.Sequence[typing.Callable[[], typing.Any]] | None = None,
|
| 38 |
+
lifespan: Lifespan[AppType] | None = None,
|
| 39 |
+
) -> None:
|
| 40 |
+
"""Initializes the application.
|
| 41 |
+
|
| 42 |
+
Parameters:
|
| 43 |
+
debug: Boolean indicating if debug tracebacks should be returned on errors.
|
| 44 |
+
routes: A list of routes to serve incoming HTTP and WebSocket requests.
|
| 45 |
+
middleware: A list of middleware to run for every request. A starlette
|
| 46 |
+
application will always automatically include two middleware classes.
|
| 47 |
+
`ServerErrorMiddleware` is added as the very outermost middleware, to handle
|
| 48 |
+
any uncaught errors occurring anywhere in the entire stack.
|
| 49 |
+
`ExceptionMiddleware` is added as the very innermost middleware, to deal
|
| 50 |
+
with handled exception cases occurring in the routing or endpoints.
|
| 51 |
+
exception_handlers: A mapping of either integer status codes,
|
| 52 |
+
or exception class types onto callables which handle the exceptions.
|
| 53 |
+
Exception handler callables should be of the form
|
| 54 |
+
`handler(request, exc) -> response` and may be either standard functions, or
|
| 55 |
+
async functions.
|
| 56 |
+
on_startup: A list of callables to run on application startup.
|
| 57 |
+
Startup handler callables do not take any arguments, and may be either
|
| 58 |
+
standard functions, or async functions.
|
| 59 |
+
on_shutdown: A list of callables to run on application shutdown.
|
| 60 |
+
Shutdown handler callables do not take any arguments, and may be either
|
| 61 |
+
standard functions, or async functions.
|
| 62 |
+
lifespan: A lifespan context function, which can be used to perform
|
| 63 |
+
startup and shutdown tasks. This is a newer style that replaces the
|
| 64 |
+
`on_startup` and `on_shutdown` handlers. Use one or the other, not both.
|
| 65 |
+
"""
|
| 66 |
+
# The lifespan context function is a newer style that replaces
|
| 67 |
+
# on_startup / on_shutdown handlers. Use one or the other, not both.
|
| 68 |
+
assert lifespan is None or (
|
| 69 |
+
on_startup is None and on_shutdown is None
|
| 70 |
+
), "Use either 'lifespan' or 'on_startup'/'on_shutdown', not both."
|
| 71 |
+
|
| 72 |
+
self.debug = debug
|
| 73 |
+
self.state = State()
|
| 74 |
+
self.router = Router(routes, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan)
|
| 75 |
+
self.exception_handlers = {} if exception_handlers is None else dict(exception_handlers)
|
| 76 |
+
self.user_middleware = [] if middleware is None else list(middleware)
|
| 77 |
+
self.middleware_stack: ASGIApp | None = None
|
| 78 |
+
|
| 79 |
+
def build_middleware_stack(self) -> ASGIApp:
|
| 80 |
+
debug = self.debug
|
| 81 |
+
error_handler = None
|
| 82 |
+
exception_handlers: dict[typing.Any, typing.Callable[[Request, Exception], Response]] = {}
|
| 83 |
+
|
| 84 |
+
for key, value in self.exception_handlers.items():
|
| 85 |
+
if key in (500, Exception):
|
| 86 |
+
error_handler = value
|
| 87 |
+
else:
|
| 88 |
+
exception_handlers[key] = value
|
| 89 |
+
|
| 90 |
+
middleware = (
|
| 91 |
+
[Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
|
| 92 |
+
+ self.user_middleware
|
| 93 |
+
+ [Middleware(ExceptionMiddleware, handlers=exception_handlers, debug=debug)]
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
app = self.router
|
| 97 |
+
for cls, args, kwargs in reversed(middleware):
|
| 98 |
+
app = cls(app, *args, **kwargs)
|
| 99 |
+
return app
|
| 100 |
+
|
| 101 |
+
@property
|
| 102 |
+
def routes(self) -> list[BaseRoute]:
|
| 103 |
+
return self.router.routes
|
| 104 |
+
|
| 105 |
+
def url_path_for(self, name: str, /, **path_params: typing.Any) -> URLPath:
|
| 106 |
+
return self.router.url_path_for(name, **path_params)
|
| 107 |
+
|
| 108 |
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
| 109 |
+
scope["app"] = self
|
| 110 |
+
if self.middleware_stack is None:
|
| 111 |
+
self.middleware_stack = self.build_middleware_stack()
|
| 112 |
+
await self.middleware_stack(scope, receive, send)
|
| 113 |
+
|
| 114 |
+
def on_event(self, event_type: str) -> typing.Callable: # type: ignore[type-arg]
|
| 115 |
+
return self.router.on_event(event_type) # pragma: no cover
|
| 116 |
+
|
| 117 |
+
def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None:
|
| 118 |
+
self.router.mount(path, app=app, name=name) # pragma: no cover
|
| 119 |
+
|
| 120 |
+
def host(self, host: str, app: ASGIApp, name: str | None = None) -> None:
|
| 121 |
+
self.router.host(host, app=app, name=name) # pragma: no cover
|
| 122 |
+
|
| 123 |
+
def add_middleware(
|
| 124 |
+
self,
|
| 125 |
+
middleware_class: _MiddlewareFactory[P],
|
| 126 |
+
*args: P.args,
|
| 127 |
+
**kwargs: P.kwargs,
|
| 128 |
+
) -> None:
|
| 129 |
+
if self.middleware_stack is not None: # pragma: no cover
|
| 130 |
+
raise RuntimeError("Cannot add middleware after an application has started")
|
| 131 |
+
self.user_middleware.insert(0, Middleware(middleware_class, *args, **kwargs))
|
| 132 |
+
|
| 133 |
+
def add_exception_handler(
|
| 134 |
+
self,
|
| 135 |
+
exc_class_or_status_code: int | type[Exception],
|
| 136 |
+
handler: ExceptionHandler,
|
| 137 |
+
) -> None: # pragma: no cover
|
| 138 |
+
self.exception_handlers[exc_class_or_status_code] = handler
|
| 139 |
+
|
| 140 |
+
def add_event_handler(
|
| 141 |
+
self,
|
| 142 |
+
event_type: str,
|
| 143 |
+
func: typing.Callable, # type: ignore[type-arg]
|
| 144 |
+
) -> None: # pragma: no cover
|
| 145 |
+
self.router.add_event_handler(event_type, func)
|
| 146 |
+
|
| 147 |
+
def add_route(
|
| 148 |
+
self,
|
| 149 |
+
path: str,
|
| 150 |
+
route: typing.Callable[[Request], typing.Awaitable[Response] | Response],
|
| 151 |
+
methods: list[str] | None = None,
|
| 152 |
+
name: str | None = None,
|
| 153 |
+
include_in_schema: bool = True,
|
| 154 |
+
) -> None: # pragma: no cover
|
| 155 |
+
self.router.add_route(path, route, methods=methods, name=name, include_in_schema=include_in_schema)
|
| 156 |
+
|
| 157 |
+
def add_websocket_route(
|
| 158 |
+
self,
|
| 159 |
+
path: str,
|
| 160 |
+
route: typing.Callable[[WebSocket], typing.Awaitable[None]],
|
| 161 |
+
name: str | None = None,
|
| 162 |
+
) -> None: # pragma: no cover
|
| 163 |
+
self.router.add_websocket_route(path, route, name=name)
|
| 164 |
+
|
| 165 |
+
def exception_handler(self, exc_class_or_status_code: int | type[Exception]) -> typing.Callable: # type: ignore[type-arg]
|
| 166 |
+
warnings.warn(
|
| 167 |
+
"The `exception_handler` decorator is deprecated, and will be removed in version 1.0.0. "
|
| 168 |
+
"Refer to https://www.starlette.io/exceptions/ for the recommended approach.",
|
| 169 |
+
DeprecationWarning,
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg]
|
| 173 |
+
self.add_exception_handler(exc_class_or_status_code, func)
|
| 174 |
+
return func
|
| 175 |
+
|
| 176 |
+
return decorator
|
| 177 |
+
|
| 178 |
+
def route(
|
| 179 |
+
self,
|
| 180 |
+
path: str,
|
| 181 |
+
methods: list[str] | None = None,
|
| 182 |
+
name: str | None = None,
|
| 183 |
+
include_in_schema: bool = True,
|
| 184 |
+
) -> typing.Callable: # type: ignore[type-arg]
|
| 185 |
+
"""
|
| 186 |
+
We no longer document this decorator style API, and its usage is discouraged.
|
| 187 |
+
Instead you should use the following approach:
|
| 188 |
+
|
| 189 |
+
>>> routes = [Route(path, endpoint=...), ...]
|
| 190 |
+
>>> app = Starlette(routes=routes)
|
| 191 |
+
"""
|
| 192 |
+
warnings.warn(
|
| 193 |
+
"The `route` decorator is deprecated, and will be removed in version 1.0.0. "
|
| 194 |
+
"Refer to https://www.starlette.io/routing/ for the recommended approach.",
|
| 195 |
+
DeprecationWarning,
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg]
|
| 199 |
+
self.router.add_route(
|
| 200 |
+
path,
|
| 201 |
+
func,
|
| 202 |
+
methods=methods,
|
| 203 |
+
name=name,
|
| 204 |
+
include_in_schema=include_in_schema,
|
| 205 |
+
)
|
| 206 |
+
return func
|
| 207 |
+
|
| 208 |
+
return decorator
|
| 209 |
+
|
| 210 |
+
def websocket_route(self, path: str, name: str | None = None) -> typing.Callable: # type: ignore[type-arg]
|
| 211 |
+
"""
|
| 212 |
+
We no longer document this decorator style API, and its usage is discouraged.
|
| 213 |
+
Instead you should use the following approach:
|
| 214 |
+
|
| 215 |
+
>>> routes = [WebSocketRoute(path, endpoint=...), ...]
|
| 216 |
+
>>> app = Starlette(routes=routes)
|
| 217 |
+
"""
|
| 218 |
+
warnings.warn(
|
| 219 |
+
"The `websocket_route` decorator is deprecated, and will be removed in version 1.0.0. "
|
| 220 |
+
"Refer to https://www.starlette.io/routing/#websocket-routing for the recommended approach.",
|
| 221 |
+
DeprecationWarning,
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg]
|
| 225 |
+
self.router.add_websocket_route(path, func, name=name)
|
| 226 |
+
return func
|
| 227 |
+
|
| 228 |
+
return decorator
|
| 229 |
+
|
| 230 |
+
def middleware(self, middleware_type: str) -> typing.Callable: # type: ignore[type-arg]
|
| 231 |
+
"""
|
| 232 |
+
We no longer document this decorator style API, and its usage is discouraged.
|
| 233 |
+
Instead you should use the following approach:
|
| 234 |
+
|
| 235 |
+
>>> middleware = [Middleware(...), ...]
|
| 236 |
+
>>> app = Starlette(middleware=middleware)
|
| 237 |
+
"""
|
| 238 |
+
warnings.warn(
|
| 239 |
+
"The `middleware` decorator is deprecated, and will be removed in version 1.0.0. "
|
| 240 |
+
"Refer to https://www.starlette.io/middleware/#using-middleware for recommended approach.",
|
| 241 |
+
DeprecationWarning,
|
| 242 |
+
)
|
| 243 |
+
assert middleware_type == "http", 'Currently only middleware("http") is supported.'
|
| 244 |
+
|
| 245 |
+
def decorator(func: typing.Callable) -> typing.Callable: # type: ignore[type-arg]
|
| 246 |
+
self.add_middleware(BaseHTTPMiddleware, dispatch=func)
|
| 247 |
+
return func
|
| 248 |
+
|
| 249 |
+
return decorator
|
.venv/lib/python3.11/site-packages/starlette/concurrency.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import functools
|
| 4 |
+
import sys
|
| 5 |
+
import typing
|
| 6 |
+
import warnings
|
| 7 |
+
|
| 8 |
+
import anyio.to_thread
|
| 9 |
+
|
| 10 |
+
if sys.version_info >= (3, 10): # pragma: no cover
|
| 11 |
+
from typing import ParamSpec
|
| 12 |
+
else: # pragma: no cover
|
| 13 |
+
from typing_extensions import ParamSpec
|
| 14 |
+
|
| 15 |
+
P = ParamSpec("P")
|
| 16 |
+
T = typing.TypeVar("T")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
async def run_until_first_complete(*args: tuple[typing.Callable, dict]) -> None: # type: ignore[type-arg]
|
| 20 |
+
warnings.warn(
|
| 21 |
+
"run_until_first_complete is deprecated and will be removed in a future version.",
|
| 22 |
+
DeprecationWarning,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
async with anyio.create_task_group() as task_group:
|
| 26 |
+
|
| 27 |
+
async def run(func: typing.Callable[[], typing.Coroutine]) -> None: # type: ignore[type-arg]
|
| 28 |
+
await func()
|
| 29 |
+
task_group.cancel_scope.cancel()
|
| 30 |
+
|
| 31 |
+
for func, kwargs in args:
|
| 32 |
+
task_group.start_soon(run, functools.partial(func, **kwargs))
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
async def run_in_threadpool(func: typing.Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
|
| 36 |
+
func = functools.partial(func, *args, **kwargs)
|
| 37 |
+
return await anyio.to_thread.run_sync(func)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class _StopIteration(Exception):
|
| 41 |
+
pass
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _next(iterator: typing.Iterator[T]) -> T:
|
| 45 |
+
# We can't raise `StopIteration` from within the threadpool iterator
|
| 46 |
+
# and catch it outside that context, so we coerce them into a different
|
| 47 |
+
# exception type.
|
| 48 |
+
try:
|
| 49 |
+
return next(iterator)
|
| 50 |
+
except StopIteration:
|
| 51 |
+
raise _StopIteration
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
async def iterate_in_threadpool(
|
| 55 |
+
iterator: typing.Iterable[T],
|
| 56 |
+
) -> typing.AsyncIterator[T]:
|
| 57 |
+
as_iterator = iter(iterator)
|
| 58 |
+
while True:
|
| 59 |
+
try:
|
| 60 |
+
yield await anyio.to_thread.run_sync(_next, as_iterator)
|
| 61 |
+
except _StopIteration:
|
| 62 |
+
break
|