offering-protocol 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- offering_protocol/__init__.py +7 -0
- offering_protocol/agent/__init__.py +66 -0
- offering_protocol/agent/agent.py +135 -0
- offering_protocol/agent/cache.py +51 -0
- offering_protocol/agent/capabilities.py +262 -0
- offering_protocol/agent/client.py +667 -0
- offering_protocol/agent/details.py +227 -0
- offering_protocol/agent/schema.py +129 -0
- offering_protocol/core/__init__.py +64 -0
- offering_protocol/core/models.py +475 -0
- offering_protocol/core/references.py +135 -0
- offering_protocol/core/schemas/action-relation.schema.json +9 -0
- offering_protocol/core/schemas/action-request.schema.json +18 -0
- offering_protocol/core/schemas/action.schema.json +56 -0
- offering_protocol/core/schemas/attribute-schema-reference.schema.json +6 -0
- offering_protocol/core/schemas/authentication-requirement.schema.json +11 -0
- offering_protocol/core/schemas/capability-identifier.schema.json +9 -0
- offering_protocol/core/schemas/capability-link.schema.json +14 -0
- offering_protocol/core/schemas/collection-search-request.schema.json +45 -0
- offering_protocol/core/schemas/collection.schema.json +72 -0
- offering_protocol/core/schemas/detail-fields.schema.json +16 -0
- offering_protocol/core/schemas/enrollment-protocol.schema.json +15 -0
- offering_protocol/core/schemas/filter-capability-source.schema.json +41 -0
- offering_protocol/core/schemas/filter-definition-page.schema.json +25 -0
- offering_protocol/core/schemas/filter-definition.schema.json +63 -0
- offering_protocol/core/schemas/filter-expression.schema.json +45 -0
- offering_protocol/core/schemas/filter-operator.schema.json +14 -0
- offering_protocol/core/schemas/filter-type.schema.json +14 -0
- offering_protocol/core/schemas/filter-unit.schema.json +45 -0
- offering_protocol/core/schemas/http-action-target.schema.json +36 -0
- offering_protocol/core/schemas/invalid-parameter.schema.json +55 -0
- offering_protocol/core/schemas/local-resource-identifier-list.schema.json +10 -0
- offering_protocol/core/schemas/local-resource-identifier.schema.json +10 -0
- offering_protocol/core/schemas/mcp-endpoint.schema.json +29 -0
- offering_protocol/core/schemas/offering-search-request.schema.json +68 -0
- offering_protocol/core/schemas/offering-search-response.schema.json +20 -0
- offering_protocol/core/schemas/offering.schema.json +92 -0
- offering_protocol/core/schemas/openapi-action-target.schema.json +20 -0
- offering_protocol/core/schemas/operation-descriptor.schema.json +27 -0
- offering_protocol/core/schemas/page-envelope.schema.json +25 -0
- offering_protocol/core/schemas/page-limit.schema.json +8 -0
- offering_protocol/core/schemas/payment-option.schema.json +24 -0
- offering_protocol/core/schemas/payment-protocol.schema.json +34 -0
- offering_protocol/core/schemas/price-preview.schema.json +133 -0
- offering_protocol/core/schemas/problem-code.schema.json +9 -0
- offering_protocol/core/schemas/problem-details.schema.json +66 -0
- offering_protocol/core/schemas/protocol-version.schema.json +8 -0
- offering_protocol/core/schemas/refinement-bucket.schema.json +28 -0
- offering_protocol/core/schemas/refinement-group.schema.json +24 -0
- offering_protocol/core/schemas/representation.schema.json +11 -0
- offering_protocol/core/schemas/resource-identity.schema.json +27 -0
- offering_protocol/core/schemas/resource-image.schema.json +40 -0
- offering_protocol/core/schemas/resource-reference.schema.json +19 -0
- offering_protocol/core/schemas/schema-reference.schema.json +15 -0
- offering_protocol/core/schemas/search-capabilities.schema.json +26 -0
- offering_protocol/core/schemas/service-branding-image.schema.json +23 -0
- offering_protocol/core/schemas/service-branding.schema.json +19 -0
- offering_protocol/core/schemas/service-document.schema.json +337 -0
- offering_protocol/core/schemas/service-openapi.schema.json +15 -0
- offering_protocol/core/schemas/service-origin.schema.json +9 -0
- offering_protocol/core/schemas/service-protocols.schema.json +101 -0
- offering_protocol/core/schemas/sort-capability-source.schema.json +41 -0
- offering_protocol/core/schemas/sort-definition-page.schema.json +25 -0
- offering_protocol/core/schemas/sort-definition.schema.json +35 -0
- offering_protocol/core/schemas/sort-key.schema.json +28 -0
- offering_protocol/core/schemas/top-level-document.schema.json +16 -0
- offering_protocol/core/schemas/trust-protocol.schema.json +15 -0
- offering_protocol/core/validation.py +390 -0
- offering_protocol/directory/__init__.py +51 -0
- offering_protocol/directory/client.py +206 -0
- offering_protocol/directory/models.py +103 -0
- offering_protocol/directory/transport.py +145 -0
- offering_protocol/py.typed +1 -0
- offering_protocol/service/__init__.py +32 -0
- offering_protocol/service/service.py +444 -0
- offering_protocol/service/static_catalog.py +258 -0
- offering_protocol-0.1.0.dist-info/METADATA +362 -0
- offering_protocol-0.1.0.dist-info/RECORD +80 -0
- offering_protocol-0.1.0.dist-info/WHEEL +4 -0
- offering_protocol-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Agent-friendly Offering details and Action resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from urllib.parse import urljoin, urlsplit
|
|
8
|
+
|
|
9
|
+
from offering_protocol.agent.client import AgentError, ServiceClient
|
|
10
|
+
from offering_protocol.agent.schema import resolve_schema
|
|
11
|
+
from offering_protocol.core import (
|
|
12
|
+
Action,
|
|
13
|
+
ActionRelation,
|
|
14
|
+
ActionRequest,
|
|
15
|
+
AuthenticationRequirement,
|
|
16
|
+
Offering,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_MAXIMUM_OPENAPI_BYTES = 1_048_576
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OfferingIssueScope(StrEnum):
|
|
23
|
+
ACTION = "action"
|
|
24
|
+
ATTRIBUTE_SCHEMA = "attribute_schema"
|
|
25
|
+
ATTRIBUTES = "attributes"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class OfferingIssue:
|
|
30
|
+
scope: OfferingIssueScope
|
|
31
|
+
message: str
|
|
32
|
+
action_id: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class DiscoveredHttpAction:
|
|
37
|
+
method: str
|
|
38
|
+
request: ActionRequest | None
|
|
39
|
+
response_content_types: tuple[str, ...]
|
|
40
|
+
url: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class DiscoveredOpenApiAction:
|
|
45
|
+
operation_id: str
|
|
46
|
+
url: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class DiscoveredAction:
|
|
51
|
+
authentication: AuthenticationRequirement
|
|
52
|
+
description: str
|
|
53
|
+
http: DiscoveredHttpAction | None
|
|
54
|
+
id: str
|
|
55
|
+
openapi: DiscoveredOpenApiAction | None
|
|
56
|
+
rel: ActionRelation
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class OfferingDetails:
|
|
61
|
+
actions: tuple[DiscoveredAction, ...]
|
|
62
|
+
attribute_schema: dict[str, object] | None
|
|
63
|
+
issues: tuple[OfferingIssue, ...]
|
|
64
|
+
offering: Offering
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class ResolvedAction:
|
|
69
|
+
action: DiscoveredAction
|
|
70
|
+
openapi_document: dict[str, object] | None = None
|
|
71
|
+
operation: dict[str, object] | None = None
|
|
72
|
+
request_schema: dict[str, object] | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def get_offering_details(client: ServiceClient, identifier: str) -> OfferingDetails:
|
|
76
|
+
inspection = await client.inspect()
|
|
77
|
+
offering = await client.get_offering(identifier)
|
|
78
|
+
service_openapi = (
|
|
79
|
+
inspection.document.http.openapi.url if inspection.document.http.openapi is not None else ""
|
|
80
|
+
)
|
|
81
|
+
actions, issues = _normalize_actions(offering.actions, client.service_origin, service_openapi)
|
|
82
|
+
attribute_schema: dict[str, object] | None = None
|
|
83
|
+
if offering.schema_ is not None:
|
|
84
|
+
try:
|
|
85
|
+
target = _resolve_https_reference(offering.schema_.url, client.service_origin)
|
|
86
|
+
resolved_schema = await resolve_schema(client, target)
|
|
87
|
+
attribute_schema = resolved_schema.schema
|
|
88
|
+
if not resolved_schema.validator.is_valid(offering.attributes):
|
|
89
|
+
offering = offering.model_copy(update={"attributes": {}})
|
|
90
|
+
issues.append(
|
|
91
|
+
OfferingIssue(
|
|
92
|
+
OfferingIssueScope.ATTRIBUTES,
|
|
93
|
+
"Offering attributes do not match their Attribute Schema",
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
except (AgentError, ValueError) as error:
|
|
97
|
+
offering = offering.model_copy(update={"attributes": {}})
|
|
98
|
+
issues.append(OfferingIssue(OfferingIssueScope.ATTRIBUTE_SCHEMA, str(error)))
|
|
99
|
+
return OfferingDetails(tuple(actions), attribute_schema, tuple(issues), offering)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def resolve_action(client: ServiceClient, offering_id: str, action_id: str) -> ResolvedAction:
|
|
103
|
+
details = await get_offering_details(client, offering_id)
|
|
104
|
+
action = next((item for item in details.actions if item.id == action_id), None)
|
|
105
|
+
if action is None:
|
|
106
|
+
raise AgentError(f"ODP Offering does not expose usable Action {action_id}")
|
|
107
|
+
if action.http is not None:
|
|
108
|
+
request_schema = None
|
|
109
|
+
if action.http.request is not None and action.http.request.schema_ is not None:
|
|
110
|
+
target = _resolve_https_reference(
|
|
111
|
+
action.http.request.schema_.url, client.service_origin
|
|
112
|
+
)
|
|
113
|
+
request_schema = (await resolve_schema(client, target)).schema
|
|
114
|
+
return ResolvedAction(action=action, request_schema=request_schema)
|
|
115
|
+
if action.openapi is None:
|
|
116
|
+
raise AgentError("ODP Action has no usable target")
|
|
117
|
+
document = await client._supporting_json(
|
|
118
|
+
action.openapi.url,
|
|
119
|
+
"openapi",
|
|
120
|
+
"application/vnd.oai.openapi+json;version=3.1, application/json;q=0.9",
|
|
121
|
+
{"application/vnd.oai.openapi+json", "application/json"},
|
|
122
|
+
_MAXIMUM_OPENAPI_BYTES,
|
|
123
|
+
)
|
|
124
|
+
version = document.get("openapi")
|
|
125
|
+
if not isinstance(version, str) or not version.startswith("3.1."):
|
|
126
|
+
raise AgentError("ODP Action requires an OpenAPI 3.1 document")
|
|
127
|
+
matches = _openapi_operations(document, action.openapi.operation_id)
|
|
128
|
+
if len(matches) != 1:
|
|
129
|
+
raise AgentError(
|
|
130
|
+
f"ODP Action operation_id {action.openapi.operation_id} must resolve exactly once"
|
|
131
|
+
)
|
|
132
|
+
return ResolvedAction(action=action, openapi_document=document, operation=matches[0])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _normalize_actions(
|
|
136
|
+
actions: list[Action], service_origin: str, service_openapi: str
|
|
137
|
+
) -> tuple[list[DiscoveredAction], list[OfferingIssue]]:
|
|
138
|
+
counts: dict[str, int] = {}
|
|
139
|
+
for action in actions:
|
|
140
|
+
counts[action.id] = counts.get(action.id, 0) + 1
|
|
141
|
+
discovered: list[DiscoveredAction] = []
|
|
142
|
+
issues: list[OfferingIssue] = []
|
|
143
|
+
reported: set[str] = set()
|
|
144
|
+
for action in actions:
|
|
145
|
+
if counts[action.id] > 1:
|
|
146
|
+
if action.id not in reported:
|
|
147
|
+
reported.add(action.id)
|
|
148
|
+
issues.append(
|
|
149
|
+
OfferingIssue(
|
|
150
|
+
OfferingIssueScope.ACTION,
|
|
151
|
+
f"Duplicate Action identifier {action.id}",
|
|
152
|
+
action.id,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
continue
|
|
156
|
+
try:
|
|
157
|
+
value = _normalize_action(action, service_origin, service_openapi)
|
|
158
|
+
if value is not None:
|
|
159
|
+
discovered.append(value)
|
|
160
|
+
except AgentError as error:
|
|
161
|
+
issues.append(OfferingIssue(OfferingIssueScope.ACTION, str(error), action.id))
|
|
162
|
+
return discovered, issues
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _normalize_action(
|
|
166
|
+
action: Action, service_origin: str, service_openapi: str
|
|
167
|
+
) -> DiscoveredAction | None:
|
|
168
|
+
if action.http is not None:
|
|
169
|
+
return DiscoveredAction(
|
|
170
|
+
authentication=action.authentication,
|
|
171
|
+
description=action.description,
|
|
172
|
+
http=DiscoveredHttpAction(
|
|
173
|
+
method=action.http.method,
|
|
174
|
+
request=action.http.request,
|
|
175
|
+
response_content_types=tuple(action.http.response_content_types),
|
|
176
|
+
url=_resolve_http_reference(action.http.href, service_origin),
|
|
177
|
+
),
|
|
178
|
+
id=action.id,
|
|
179
|
+
openapi=None,
|
|
180
|
+
rel=action.rel,
|
|
181
|
+
)
|
|
182
|
+
if action.openapi is not None:
|
|
183
|
+
target = action.openapi.url or service_openapi
|
|
184
|
+
if not target:
|
|
185
|
+
raise AgentError("OpenAPI Action has no OpenAPI document URL")
|
|
186
|
+
return DiscoveredAction(
|
|
187
|
+
authentication=action.authentication,
|
|
188
|
+
description=action.description,
|
|
189
|
+
http=None,
|
|
190
|
+
id=action.id,
|
|
191
|
+
openapi=DiscoveredOpenApiAction(
|
|
192
|
+
action.openapi.operation_id,
|
|
193
|
+
_resolve_https_reference(target, service_origin),
|
|
194
|
+
),
|
|
195
|
+
rel=action.rel,
|
|
196
|
+
)
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _resolve_http_reference(reference: str, base: str) -> str:
|
|
201
|
+
target = urljoin(f"{base}/", reference)
|
|
202
|
+
parsed = urlsplit(target)
|
|
203
|
+
if parsed.scheme not in {"http", "https"} or parsed.hostname is None:
|
|
204
|
+
raise AgentError("ODP Action target must use HTTP or HTTPS")
|
|
205
|
+
return target
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _resolve_https_reference(reference: str, base: str) -> str:
|
|
209
|
+
target = _resolve_http_reference(reference, base)
|
|
210
|
+
if urlsplit(target).scheme != "https":
|
|
211
|
+
raise AgentError("ODP supporting document URL must use HTTPS")
|
|
212
|
+
return target
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _openapi_operations(document: dict[str, object], operation_id: str) -> list[dict[str, object]]:
|
|
216
|
+
paths = document.get("paths")
|
|
217
|
+
if not isinstance(paths, dict):
|
|
218
|
+
raise AgentError("ODP OpenAPI document must contain paths")
|
|
219
|
+
matches: list[dict[str, object]] = []
|
|
220
|
+
for path in paths.values():
|
|
221
|
+
if not isinstance(path, dict):
|
|
222
|
+
continue
|
|
223
|
+
for method in ("delete", "get", "head", "options", "patch", "post", "put", "trace"):
|
|
224
|
+
operation = path.get(method)
|
|
225
|
+
if isinstance(operation, dict) and operation.get("operationId") == operation_id:
|
|
226
|
+
matches.append(operation)
|
|
227
|
+
return matches
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Bounded Attribute Schema resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Protocol
|
|
8
|
+
from urllib.parse import urldefrag, urljoin, urlsplit
|
|
9
|
+
|
|
10
|
+
from jsonschema.validators import validator_for
|
|
11
|
+
from referencing import Registry, Resource
|
|
12
|
+
|
|
13
|
+
from offering_protocol.agent.client import AgentError, ServiceClient
|
|
14
|
+
|
|
15
|
+
_DIALECT = "https://json-schema.org/draft/2020-12/schema"
|
|
16
|
+
_MAXIMUM_DOCUMENT_BYTES = 262_144
|
|
17
|
+
_MAXIMUM_DOCUMENTS = 16
|
|
18
|
+
_MAXIMUM_DEPTH = 8
|
|
19
|
+
_MAXIMUM_GRAPH_BYTES = 1_048_576
|
|
20
|
+
_STANDARD_VOCABULARY = "https://json-schema.org/draft/2020-12/vocab/"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SchemaValidator(Protocol):
|
|
24
|
+
def is_valid(self, instance: Any) -> bool: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class ResolvedSchema:
|
|
29
|
+
schema: dict[str, object]
|
|
30
|
+
validator: SchemaValidator
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def resolve_schema(client: ServiceClient, target: str) -> ResolvedSchema:
|
|
34
|
+
root_url = _document_url(target)
|
|
35
|
+
documents: dict[str, dict[str, object]] = {}
|
|
36
|
+
graph_bytes = 0
|
|
37
|
+
|
|
38
|
+
async def load(document_url: str, depth: int) -> None:
|
|
39
|
+
nonlocal graph_bytes
|
|
40
|
+
if document_url in documents:
|
|
41
|
+
return
|
|
42
|
+
if len(documents) >= _MAXIMUM_DOCUMENTS:
|
|
43
|
+
raise AgentError("ODP Attribute Schema graph exceeds 16 documents")
|
|
44
|
+
if depth > _MAXIMUM_DEPTH:
|
|
45
|
+
raise AgentError("ODP Attribute Schema graph exceeds eight reference levels")
|
|
46
|
+
document = await client._supporting_json(
|
|
47
|
+
document_url,
|
|
48
|
+
"attribute-schema",
|
|
49
|
+
"application/schema+json",
|
|
50
|
+
{"application/schema+json"},
|
|
51
|
+
_MAXIMUM_DOCUMENT_BYTES,
|
|
52
|
+
)
|
|
53
|
+
_require_schema(document)
|
|
54
|
+
encoded = json.dumps(document, separators=(",", ":")).encode()
|
|
55
|
+
graph_bytes += len(encoded)
|
|
56
|
+
if graph_bytes > _MAXIMUM_GRAPH_BYTES:
|
|
57
|
+
raise AgentError("ODP Attribute Schema graph exceeds its byte limit")
|
|
58
|
+
documents[document_url] = document
|
|
59
|
+
for reference_url in _schema_references(document, document_url):
|
|
60
|
+
await load(reference_url, depth + 1)
|
|
61
|
+
|
|
62
|
+
await load(root_url, 0)
|
|
63
|
+
root = documents[root_url]
|
|
64
|
+
registry: Registry[Any] = Registry().with_resources(
|
|
65
|
+
(url, Resource.from_contents(document)) for url, document in documents.items()
|
|
66
|
+
)
|
|
67
|
+
validation_root = {"$id": root_url, **root}
|
|
68
|
+
validator_type = validator_for(validation_root)
|
|
69
|
+
validator_type.check_schema(validation_root)
|
|
70
|
+
return ResolvedSchema(root, validator_type(validation_root, registry=registry))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _document_url(value: str) -> str:
|
|
74
|
+
target, _ = urldefrag(value)
|
|
75
|
+
parsed = urlsplit(target)
|
|
76
|
+
if (
|
|
77
|
+
parsed.scheme != "https"
|
|
78
|
+
or parsed.hostname is None
|
|
79
|
+
or parsed.username is not None
|
|
80
|
+
or parsed.password is not None
|
|
81
|
+
):
|
|
82
|
+
raise AgentError("ODP Attribute Schema references must use HTTPS")
|
|
83
|
+
return target
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _require_schema(document: dict[str, object]) -> None:
|
|
87
|
+
if document.get("$schema") != _DIALECT:
|
|
88
|
+
raise AgentError("ODP Attribute Schema must declare JSON Schema Draft 2020-12")
|
|
89
|
+
pending: list[object] = [document]
|
|
90
|
+
while pending:
|
|
91
|
+
value = pending.pop()
|
|
92
|
+
if isinstance(value, list):
|
|
93
|
+
pending.extend(value)
|
|
94
|
+
elif isinstance(value, dict):
|
|
95
|
+
if "$dynamicRef" in value:
|
|
96
|
+
reference = value["$dynamicRef"]
|
|
97
|
+
if not isinstance(reference, str) or not reference.startswith("#"):
|
|
98
|
+
raise AgentError(
|
|
99
|
+
"ODP Attribute Schema $dynamicRef must be a fragment-only reference"
|
|
100
|
+
)
|
|
101
|
+
vocabulary = value.get("$vocabulary")
|
|
102
|
+
if isinstance(vocabulary, dict):
|
|
103
|
+
for uri, required in vocabulary.items():
|
|
104
|
+
if required is True and not str(uri).startswith(_STANDARD_VOCABULARY):
|
|
105
|
+
raise AgentError(
|
|
106
|
+
f"ODP Attribute Schema requires unsupported vocabulary {uri}"
|
|
107
|
+
)
|
|
108
|
+
pending.extend(value.values())
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _schema_references(document: object, retrieval_url: str) -> tuple[str, ...]:
|
|
112
|
+
references: list[str] = []
|
|
113
|
+
local_resources = {_document_url(retrieval_url)}
|
|
114
|
+
pending = [(document, retrieval_url)]
|
|
115
|
+
while pending:
|
|
116
|
+
value, inherited_base = pending.pop()
|
|
117
|
+
if isinstance(value, list):
|
|
118
|
+
pending.extend((child, inherited_base) for child in value)
|
|
119
|
+
elif isinstance(value, dict):
|
|
120
|
+
base = inherited_base
|
|
121
|
+
identifier = value.get("$id")
|
|
122
|
+
if isinstance(identifier, str):
|
|
123
|
+
base = urljoin(inherited_base, identifier)
|
|
124
|
+
local_resources.add(_document_url(base))
|
|
125
|
+
reference = value.get("$ref")
|
|
126
|
+
if isinstance(reference, str):
|
|
127
|
+
references.append(_document_url(urljoin(base, reference)))
|
|
128
|
+
pending.extend((child, base) for keyword, child in value.items() if keyword != "$ref")
|
|
129
|
+
return tuple(reference for reference in references if reference not in local_resources)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Protocol models, validation, identities, references, and pagination."""
|
|
2
|
+
|
|
3
|
+
from offering_protocol.core.models import * # noqa: F403
|
|
4
|
+
from offering_protocol.core.models import __all__ as _models_all
|
|
5
|
+
from offering_protocol.core.references import (
|
|
6
|
+
ReferenceError,
|
|
7
|
+
build_operation_url,
|
|
8
|
+
create_resource_identity,
|
|
9
|
+
derive_service_origin,
|
|
10
|
+
is_local_resource_identifier,
|
|
11
|
+
operation_method,
|
|
12
|
+
operation_path,
|
|
13
|
+
resolve_continuation,
|
|
14
|
+
resolve_resource_reference,
|
|
15
|
+
)
|
|
16
|
+
from offering_protocol.core.validation import (
|
|
17
|
+
OdpValidationError,
|
|
18
|
+
ValidationIssue,
|
|
19
|
+
parse_collection,
|
|
20
|
+
parse_collection_page,
|
|
21
|
+
parse_collection_search_request,
|
|
22
|
+
parse_filter_definition,
|
|
23
|
+
parse_filter_definition_page,
|
|
24
|
+
parse_offering,
|
|
25
|
+
parse_offering_page,
|
|
26
|
+
parse_offering_search_request,
|
|
27
|
+
parse_problem_details,
|
|
28
|
+
parse_problem_response,
|
|
29
|
+
parse_resource_identity,
|
|
30
|
+
parse_service_document,
|
|
31
|
+
parse_sort_definition,
|
|
32
|
+
parse_sort_definition_page,
|
|
33
|
+
validate_value,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
*_models_all,
|
|
38
|
+
"OdpValidationError",
|
|
39
|
+
"ReferenceError",
|
|
40
|
+
"ValidationIssue",
|
|
41
|
+
"build_operation_url",
|
|
42
|
+
"create_resource_identity",
|
|
43
|
+
"derive_service_origin",
|
|
44
|
+
"is_local_resource_identifier",
|
|
45
|
+
"operation_method",
|
|
46
|
+
"operation_path",
|
|
47
|
+
"parse_collection",
|
|
48
|
+
"parse_collection_page",
|
|
49
|
+
"parse_collection_search_request",
|
|
50
|
+
"parse_filter_definition",
|
|
51
|
+
"parse_filter_definition_page",
|
|
52
|
+
"parse_offering",
|
|
53
|
+
"parse_offering_page",
|
|
54
|
+
"parse_offering_search_request",
|
|
55
|
+
"parse_problem_details",
|
|
56
|
+
"parse_problem_response",
|
|
57
|
+
"parse_resource_identity",
|
|
58
|
+
"parse_service_document",
|
|
59
|
+
"parse_sort_definition",
|
|
60
|
+
"parse_sort_definition_page",
|
|
61
|
+
"resolve_continuation",
|
|
62
|
+
"resolve_resource_reference",
|
|
63
|
+
"validate_value",
|
|
64
|
+
]
|