offering-protocol 0.1.0__tar.gz → 0.1.1__tar.gz
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-0.1.0 → offering_protocol-0.1.1}/PKG-INFO +5 -1
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/README.md +4 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/pyproject.toml +1 -1
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/agent/client.py +47 -8
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/agent/details.py +1 -1
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/__init__.py +2 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/models.py +1 -1
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/validation.py +365 -1
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/directory/client.py +38 -3
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/.gitignore +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/LICENSE +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/__init__.py +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/agent/__init__.py +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/agent/agent.py +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/agent/cache.py +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/agent/capabilities.py +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/agent/schema.py +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/references.py +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/action-relation.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/action-request.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/action.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/attribute-schema-reference.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/authentication-requirement.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/capability-identifier.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/capability-link.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/collection-search-request.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/collection.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/detail-fields.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/enrollment-protocol.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/filter-capability-source.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/filter-definition-page.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/filter-definition.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/filter-expression.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/filter-operator.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/filter-type.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/filter-unit.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/http-action-target.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/invalid-parameter.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/local-resource-identifier-list.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/local-resource-identifier.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/mcp-endpoint.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/offering-search-request.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/offering-search-response.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/offering.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/openapi-action-target.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/operation-descriptor.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/page-envelope.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/page-limit.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/payment-option.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/payment-protocol.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/price-preview.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/problem-code.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/problem-details.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/protocol-version.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/refinement-bucket.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/refinement-group.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/representation.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/resource-identity.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/resource-image.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/resource-reference.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/schema-reference.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/search-capabilities.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/service-branding-image.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/service-branding.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/service-document.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/service-openapi.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/service-origin.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/service-protocols.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/sort-capability-source.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/sort-definition-page.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/sort-definition.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/sort-key.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/top-level-document.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/schemas/trust-protocol.schema.json +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/directory/__init__.py +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/directory/models.py +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/directory/transport.py +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/py.typed +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/service/__init__.py +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/service/service.py +0 -0
- {offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/service/static_catalog.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: offering-protocol
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Official Python SDK for the Offering Discovery Protocol
|
|
5
5
|
Project-URL: Documentation, https://www.offeringprotocol.org/
|
|
6
6
|
Project-URL: Issues, https://github.com/offering-protocol/odp-python/issues
|
|
@@ -281,6 +281,10 @@ its Actions can advertise enrollment, payment, and trust protocols, but ODP does
|
|
|
281
281
|
credentials, invoke Actions, submit payments, or implement trust protocols. Applications compose
|
|
282
282
|
the appropriate protocol clients around an Action resolved through ODP.
|
|
283
283
|
|
|
284
|
+
`parse_service_document` is the strict current-version Service parser. Agent inspection and
|
|
285
|
+
Directory results filter unrecognized enrollment, payment, and trust descriptors while retaining
|
|
286
|
+
strict validation for recognized descriptors.
|
|
287
|
+
|
|
284
288
|
## Errors and validation
|
|
285
289
|
|
|
286
290
|
Each role exposes typed errors:
|
|
@@ -252,6 +252,10 @@ its Actions can advertise enrollment, payment, and trust protocols, but ODP does
|
|
|
252
252
|
credentials, invoke Actions, submit payments, or implement trust protocols. Applications compose
|
|
253
253
|
the appropriate protocol clients around an Action resolved through ODP.
|
|
254
254
|
|
|
255
|
+
`parse_service_document` is the strict current-version Service parser. Agent inspection and
|
|
256
|
+
Directory results filter unrecognized enrollment, payment, and trust descriptors while retaining
|
|
257
|
+
strict validation for recognized descriptors.
|
|
258
|
+
|
|
255
259
|
## Errors and validation
|
|
256
260
|
|
|
257
261
|
Each role exposes typed errors:
|
|
@@ -21,18 +21,30 @@ from offering_protocol.core import (
|
|
|
21
21
|
OfferingSearchRequest,
|
|
22
22
|
Operation,
|
|
23
23
|
Page,
|
|
24
|
+
ProblemDetails,
|
|
24
25
|
Representation,
|
|
25
26
|
ServiceDocument,
|
|
26
27
|
build_operation_url,
|
|
27
28
|
derive_service_origin,
|
|
28
|
-
|
|
29
|
-
parse_collection_page,
|
|
30
|
-
parse_offering,
|
|
31
|
-
parse_offering_page,
|
|
32
|
-
parse_problem_response,
|
|
33
|
-
parse_service_document,
|
|
29
|
+
parse_agent_service_document,
|
|
34
30
|
resolve_continuation,
|
|
35
31
|
)
|
|
32
|
+
from offering_protocol.core import (
|
|
33
|
+
parse_collection as parse_collection_strict,
|
|
34
|
+
)
|
|
35
|
+
from offering_protocol.core import (
|
|
36
|
+
parse_collection_page as parse_collection_page_strict,
|
|
37
|
+
)
|
|
38
|
+
from offering_protocol.core import (
|
|
39
|
+
parse_offering as parse_offering_strict,
|
|
40
|
+
)
|
|
41
|
+
from offering_protocol.core import (
|
|
42
|
+
parse_offering_page as parse_offering_page_strict,
|
|
43
|
+
)
|
|
44
|
+
from offering_protocol.core import (
|
|
45
|
+
parse_problem_response as parse_problem_response_strict,
|
|
46
|
+
)
|
|
47
|
+
from offering_protocol.core.validation import _normalize_agent_response
|
|
36
48
|
from offering_protocol.directory.transport import (
|
|
37
49
|
HttpRequest,
|
|
38
50
|
HttpResponse,
|
|
@@ -136,10 +148,10 @@ class ServiceClient:
|
|
|
136
148
|
b"",
|
|
137
149
|
_MAXIMUM_DOCUMENT_BYTES,
|
|
138
150
|
self._cache_fallbacks.service_document,
|
|
139
|
-
|
|
151
|
+
parse_agent_service_document,
|
|
140
152
|
)
|
|
141
153
|
return Inspection(
|
|
142
|
-
document=
|
|
154
|
+
document=parse_agent_service_document(response.body),
|
|
143
155
|
final_url=response.final_url,
|
|
144
156
|
freshness=response.freshness,
|
|
145
157
|
requested_url=requested_url,
|
|
@@ -571,6 +583,33 @@ def _operation_parser(operation: Operation) -> object:
|
|
|
571
583
|
return parse_offering_page
|
|
572
584
|
|
|
573
585
|
|
|
586
|
+
def parse_collection(data: bytes | str) -> Collection:
|
|
587
|
+
return parse_collection_strict(_normalize_body(data, "collection"))
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def parse_offering(data: bytes | str) -> Offering:
|
|
591
|
+
return parse_offering_strict(_normalize_body(data, "offering"))
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def parse_collection_page(data: bytes | str) -> Page[Collection]:
|
|
595
|
+
return parse_collection_page_strict(_normalize_body(data, "collection-page"))
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def parse_offering_page(data: bytes | str) -> OfferingPage[Offering]:
|
|
599
|
+
return parse_offering_page_strict(_normalize_body(data, "offering-page"))
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def parse_problem_response(data: bytes | str, status: int) -> ProblemDetails:
|
|
603
|
+
return parse_problem_response_strict(_normalize_body(data, "problem"), status)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _normalize_body(data: bytes | str, kind: str) -> str:
|
|
607
|
+
raw = json.loads(data)
|
|
608
|
+
if not isinstance(raw, dict):
|
|
609
|
+
return data.decode() if isinstance(data, bytes) else data
|
|
610
|
+
return json.dumps(_normalize_agent_response(raw, kind), separators=(",", ":"))
|
|
611
|
+
|
|
612
|
+
|
|
574
613
|
def _encode(value: object) -> bytes:
|
|
575
614
|
if not hasattr(value, "model_dump_json"):
|
|
576
615
|
raise TypeError("ODP model is not serializable")
|
|
@@ -16,6 +16,7 @@ from offering_protocol.core.references import (
|
|
|
16
16
|
from offering_protocol.core.validation import (
|
|
17
17
|
OdpValidationError,
|
|
18
18
|
ValidationIssue,
|
|
19
|
+
parse_agent_service_document,
|
|
19
20
|
parse_collection,
|
|
20
21
|
parse_collection_page,
|
|
21
22
|
parse_collection_search_request,
|
|
@@ -44,6 +45,7 @@ __all__ = [
|
|
|
44
45
|
"is_local_resource_identifier",
|
|
45
46
|
"operation_method",
|
|
46
47
|
"operation_path",
|
|
48
|
+
"parse_agent_service_document",
|
|
47
49
|
"parse_collection",
|
|
48
50
|
"parse_collection_page",
|
|
49
51
|
"parse_collection_search_request",
|
{offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/validation.py
RENAMED
|
@@ -82,6 +82,363 @@ def parse_service_document(data: bytes | str) -> ServiceDocument:
|
|
|
82
82
|
return value
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def parse_agent_service_document(data: bytes | str) -> ServiceDocument:
|
|
86
|
+
try:
|
|
87
|
+
raw = json.loads(data)
|
|
88
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
89
|
+
return parse_service_document(data)
|
|
90
|
+
if isinstance(raw, dict):
|
|
91
|
+
raw = _normalize_agent_response(raw, "service-document")
|
|
92
|
+
return parse_service_document(json.dumps(raw, separators=(",", ":")))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _normalize_agent_response(value: dict[str, Any], kind: str) -> dict[str, Any]:
|
|
96
|
+
normalized = dict(value)
|
|
97
|
+
if kind == "service-document":
|
|
98
|
+
_filter_agent_protocols(normalized)
|
|
99
|
+
protocols = normalized.get("protocols")
|
|
100
|
+
if isinstance(protocols, dict):
|
|
101
|
+
_filter_unknown_authentication(protocols, "payments")
|
|
102
|
+
_filter_named_list(
|
|
103
|
+
normalized,
|
|
104
|
+
"operations",
|
|
105
|
+
{
|
|
106
|
+
"get-collection",
|
|
107
|
+
"get-offering",
|
|
108
|
+
"list-collection-offerings",
|
|
109
|
+
"list-collections",
|
|
110
|
+
"list-offerings",
|
|
111
|
+
"search-collections",
|
|
112
|
+
"search-offerings",
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
_filter_unknown_authentication(normalized, "operations")
|
|
116
|
+
_filter_typed_list(normalized, "mcp", {"streamable-http"})
|
|
117
|
+
_filter_closed_object_list(normalized, "operations", {"authentication", "name"})
|
|
118
|
+
_filter_closed_object_list(normalized, "mcp", {"description", "name", "type", "url"})
|
|
119
|
+
_filter_payment_options(normalized)
|
|
120
|
+
_normalize_branding(normalized)
|
|
121
|
+
_normalize_search_capabilities(normalized)
|
|
122
|
+
elif kind in {"collection", "offering"}:
|
|
123
|
+
_filter_typed_list(
|
|
124
|
+
normalized,
|
|
125
|
+
"images",
|
|
126
|
+
{"image/avif", "image/jpeg", "image/png", "image/svg+xml", "image/webp"},
|
|
127
|
+
)
|
|
128
|
+
_strip_object_list(normalized, "images", {"alt", "height", "src", "type", "width"})
|
|
129
|
+
_normalize_search_capabilities(normalized)
|
|
130
|
+
if kind == "offering":
|
|
131
|
+
_normalize_offering(normalized)
|
|
132
|
+
elif kind in {"collection-page", "offering-page"} and isinstance(normalized.get("items"), list):
|
|
133
|
+
item_kind = "offering" if kind == "offering-page" else "collection"
|
|
134
|
+
normalized["items"] = [
|
|
135
|
+
_normalize_agent_response(item, item_kind) if isinstance(item, dict) else item
|
|
136
|
+
for item in normalized["items"]
|
|
137
|
+
]
|
|
138
|
+
elif kind in {"filter-page", "sort-page"} and isinstance(normalized.get("items"), list):
|
|
139
|
+
predicate = _known_filter if kind == "filter-page" else _known_sort
|
|
140
|
+
normalized["items"] = [item for item in normalized["items"] if predicate(item)]
|
|
141
|
+
elif kind == "problem" and isinstance(normalized.get("invalid_params"), list):
|
|
142
|
+
normalized["invalid_params"] = [
|
|
143
|
+
item
|
|
144
|
+
for item in normalized["invalid_params"]
|
|
145
|
+
if not (
|
|
146
|
+
isinstance(item, dict)
|
|
147
|
+
and isinstance(item.get("in"), str)
|
|
148
|
+
and item["in"] not in {"body", "header", "path", "query"}
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
return normalized
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _filter_named_list(value: dict[str, Any], member: str, recognized: set[str]) -> None:
|
|
155
|
+
items = value.get(member)
|
|
156
|
+
if not isinstance(items, list):
|
|
157
|
+
return
|
|
158
|
+
filtered = [
|
|
159
|
+
item
|
|
160
|
+
for item in items
|
|
161
|
+
if not (
|
|
162
|
+
isinstance(item, dict)
|
|
163
|
+
and isinstance(item.get("name"), str)
|
|
164
|
+
and item["name"] not in recognized
|
|
165
|
+
)
|
|
166
|
+
]
|
|
167
|
+
if filtered:
|
|
168
|
+
value[member] = filtered
|
|
169
|
+
else:
|
|
170
|
+
value.pop(member, None)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _filter_typed_list(value: dict[str, Any], member: str, recognized: set[str]) -> None:
|
|
174
|
+
items = value.get(member)
|
|
175
|
+
if not isinstance(items, list):
|
|
176
|
+
return
|
|
177
|
+
filtered = [
|
|
178
|
+
item
|
|
179
|
+
for item in items
|
|
180
|
+
if not (
|
|
181
|
+
isinstance(item, dict)
|
|
182
|
+
and isinstance(item.get("type"), str)
|
|
183
|
+
and item["type"] not in recognized
|
|
184
|
+
)
|
|
185
|
+
]
|
|
186
|
+
if filtered:
|
|
187
|
+
value[member] = filtered
|
|
188
|
+
else:
|
|
189
|
+
value.pop(member, None)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _filter_closed_object_list(value: dict[str, Any], member: str, allowed: set[str]) -> None:
|
|
193
|
+
items = value.get(member)
|
|
194
|
+
if not isinstance(items, list):
|
|
195
|
+
return
|
|
196
|
+
filtered = [
|
|
197
|
+
item for item in items if not isinstance(item, dict) or not set(item).difference(allowed)
|
|
198
|
+
]
|
|
199
|
+
if filtered:
|
|
200
|
+
value[member] = filtered
|
|
201
|
+
else:
|
|
202
|
+
value.pop(member, None)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _filter_unknown_authentication(value: dict[str, Any], member: str) -> None:
|
|
206
|
+
items = value.get(member)
|
|
207
|
+
if not isinstance(items, list):
|
|
208
|
+
return
|
|
209
|
+
filtered = [
|
|
210
|
+
item
|
|
211
|
+
for item in items
|
|
212
|
+
if not isinstance(item, dict) or not _has_unknown_authentication(item)
|
|
213
|
+
]
|
|
214
|
+
if filtered:
|
|
215
|
+
value[member] = filtered
|
|
216
|
+
else:
|
|
217
|
+
value.pop(member, None)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _has_unknown_authentication(value: dict[str, Any]) -> bool:
|
|
221
|
+
authentication = value.get("authentication")
|
|
222
|
+
return isinstance(authentication, str) and authentication not in {
|
|
223
|
+
"not-required",
|
|
224
|
+
"optional",
|
|
225
|
+
"required",
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _strip_object_list(value: dict[str, Any], member: str, allowed: set[str]) -> None:
|
|
230
|
+
items = value.get(member)
|
|
231
|
+
if not isinstance(items, list):
|
|
232
|
+
return
|
|
233
|
+
value[member] = [
|
|
234
|
+
{key: entry for key, entry in item.items() if key in allowed}
|
|
235
|
+
if isinstance(item, dict)
|
|
236
|
+
else item
|
|
237
|
+
for item in items
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _filter_payment_options(value: dict[str, Any]) -> None:
|
|
242
|
+
protocols = value.get("protocols")
|
|
243
|
+
if not isinstance(protocols, dict) or not isinstance(protocols.get("payments"), list):
|
|
244
|
+
return
|
|
245
|
+
recognized = {
|
|
246
|
+
"algorand",
|
|
247
|
+
"aptos",
|
|
248
|
+
"arbitrum",
|
|
249
|
+
"avalanche",
|
|
250
|
+
"base",
|
|
251
|
+
"card",
|
|
252
|
+
"ethereum",
|
|
253
|
+
"hedera",
|
|
254
|
+
"inflow",
|
|
255
|
+
"lightning",
|
|
256
|
+
"polygon",
|
|
257
|
+
"solana",
|
|
258
|
+
"stellar",
|
|
259
|
+
"stripe",
|
|
260
|
+
"tempo",
|
|
261
|
+
"ton",
|
|
262
|
+
}
|
|
263
|
+
for payment in protocols["payments"]:
|
|
264
|
+
if not isinstance(payment, dict) or not isinstance(payment.get("options"), list):
|
|
265
|
+
continue
|
|
266
|
+
options = [
|
|
267
|
+
option
|
|
268
|
+
for option in payment["options"]
|
|
269
|
+
if not isinstance(option, str) or option in recognized
|
|
270
|
+
]
|
|
271
|
+
if options:
|
|
272
|
+
payment["options"] = options
|
|
273
|
+
else:
|
|
274
|
+
payment.pop("options", None)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _normalize_branding(value: dict[str, Any]) -> None:
|
|
278
|
+
branding = value.get("branding")
|
|
279
|
+
if not isinstance(branding, dict):
|
|
280
|
+
return
|
|
281
|
+
normalized = {key: entry for key, entry in branding.items() if key in {"icon", "logo"}}
|
|
282
|
+
for member in ("icon", "logo"):
|
|
283
|
+
image = normalized.get(member)
|
|
284
|
+
if (
|
|
285
|
+
isinstance(image, dict)
|
|
286
|
+
and isinstance(image.get("type"), str)
|
|
287
|
+
and image["type"] not in {"image/png", "image/svg+xml", "image/webp"}
|
|
288
|
+
):
|
|
289
|
+
normalized.pop(member, None)
|
|
290
|
+
elif isinstance(image, dict):
|
|
291
|
+
normalized[member] = {
|
|
292
|
+
key: entry for key, entry in image.items() if key in {"src", "type"}
|
|
293
|
+
}
|
|
294
|
+
if normalized:
|
|
295
|
+
value["branding"] = normalized
|
|
296
|
+
else:
|
|
297
|
+
value.pop("branding", None)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _normalize_search_capabilities(value: dict[str, Any]) -> None:
|
|
301
|
+
capabilities = value.get("search_capabilities")
|
|
302
|
+
if not isinstance(capabilities, dict):
|
|
303
|
+
return
|
|
304
|
+
normalized = dict(capabilities)
|
|
305
|
+
for member, predicate in (("filters", _known_filter), ("sorts", _known_sort)):
|
|
306
|
+
source = normalized.get(member)
|
|
307
|
+
if not isinstance(source, dict) or not isinstance(source.get("inline"), list):
|
|
308
|
+
continue
|
|
309
|
+
items = [item for item in source["inline"] if predicate(item)]
|
|
310
|
+
if items:
|
|
311
|
+
normalized[member] = {**source, "inline": items}
|
|
312
|
+
else:
|
|
313
|
+
normalized.pop(member, None)
|
|
314
|
+
if normalized:
|
|
315
|
+
value["search_capabilities"] = normalized
|
|
316
|
+
else:
|
|
317
|
+
value.pop("search_capabilities", None)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _normalize_offering(value: dict[str, Any]) -> None:
|
|
321
|
+
schema = value.get("schema")
|
|
322
|
+
if isinstance(schema, dict) and set(schema).difference({"url"}):
|
|
323
|
+
value.pop("schema", None)
|
|
324
|
+
price = value.get("price")
|
|
325
|
+
if (
|
|
326
|
+
isinstance(price, dict)
|
|
327
|
+
and isinstance(price.get("type"), str)
|
|
328
|
+
and price["type"] not in {"fixed", "free", "metered", "quote", "range", "starting_at"}
|
|
329
|
+
):
|
|
330
|
+
value.pop("price", None)
|
|
331
|
+
actions = value.get("actions")
|
|
332
|
+
if not isinstance(actions, list):
|
|
333
|
+
return
|
|
334
|
+
filtered = []
|
|
335
|
+
for action in actions:
|
|
336
|
+
if isinstance(action, dict) and _has_unknown_authentication(action):
|
|
337
|
+
continue
|
|
338
|
+
if isinstance(action, dict) and set(action).difference(
|
|
339
|
+
{"authentication", "description", "http", "id", "openapi", "rel"}
|
|
340
|
+
):
|
|
341
|
+
continue
|
|
342
|
+
http = action.get("http") if isinstance(action, dict) else None
|
|
343
|
+
if isinstance(http, dict) and set(http).difference(
|
|
344
|
+
{"href", "method", "request", "response_content_types"}
|
|
345
|
+
):
|
|
346
|
+
continue
|
|
347
|
+
request = http.get("request") if isinstance(http, dict) else None
|
|
348
|
+
if isinstance(request, dict) and set(request).difference({"content_type", "schema"}):
|
|
349
|
+
continue
|
|
350
|
+
schema = request.get("schema") if isinstance(request, dict) else None
|
|
351
|
+
if isinstance(schema, dict) and set(schema).difference({"url"}):
|
|
352
|
+
continue
|
|
353
|
+
openapi = action.get("openapi") if isinstance(action, dict) else None
|
|
354
|
+
if isinstance(openapi, dict) and set(openapi).difference({"operation_id", "url"}):
|
|
355
|
+
continue
|
|
356
|
+
method = http.get("method") if isinstance(http, dict) else None
|
|
357
|
+
if isinstance(method, str) and method not in {"GET", "POST"}:
|
|
358
|
+
continue
|
|
359
|
+
filtered.append(action)
|
|
360
|
+
if filtered:
|
|
361
|
+
value["actions"] = filtered
|
|
362
|
+
else:
|
|
363
|
+
value.pop("actions", None)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _known_filter(value: Any) -> bool:
|
|
367
|
+
if not isinstance(value, dict):
|
|
368
|
+
return True
|
|
369
|
+
if isinstance(value.get("type"), str) and value["type"] not in {
|
|
370
|
+
"boolean",
|
|
371
|
+
"date",
|
|
372
|
+
"date-time",
|
|
373
|
+
"decimal",
|
|
374
|
+
"integer",
|
|
375
|
+
"number",
|
|
376
|
+
"string",
|
|
377
|
+
}:
|
|
378
|
+
return False
|
|
379
|
+
operators = value.get("operators")
|
|
380
|
+
if isinstance(operators, list) and any(
|
|
381
|
+
isinstance(operator, str)
|
|
382
|
+
and operator not in {"eq", "exists", "gt", "gte", "in", "lt", "lte"}
|
|
383
|
+
for operator in operators
|
|
384
|
+
):
|
|
385
|
+
return False
|
|
386
|
+
unit = value.get("unit")
|
|
387
|
+
return not (
|
|
388
|
+
isinstance(unit, dict)
|
|
389
|
+
and isinstance(unit.get("system"), str)
|
|
390
|
+
and unit["system"] not in {"service", "ucum"}
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _known_sort(value: Any) -> bool:
|
|
395
|
+
if not isinstance(value, dict) or not isinstance(value.get("keys"), list):
|
|
396
|
+
return True
|
|
397
|
+
return not any(
|
|
398
|
+
isinstance(key, dict)
|
|
399
|
+
and (
|
|
400
|
+
(
|
|
401
|
+
isinstance(key.get("direction"), str)
|
|
402
|
+
and key["direction"] not in {"ascending", "descending"}
|
|
403
|
+
)
|
|
404
|
+
or (isinstance(key.get("missing"), str) and key["missing"] not in {"first", "last"})
|
|
405
|
+
)
|
|
406
|
+
for key in value["keys"]
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _filter_agent_protocols(document: dict[str, Any]) -> None:
|
|
411
|
+
protocols = document.get("protocols")
|
|
412
|
+
if not isinstance(protocols, dict):
|
|
413
|
+
return
|
|
414
|
+
_filter_agent_protocol_category(protocols, "enrollment", {"aep"})
|
|
415
|
+
_filter_agent_protocol_category(protocols, "payments", {"mpp", "x402"})
|
|
416
|
+
_filter_agent_protocol_category(protocols, "trust", {"tap"})
|
|
417
|
+
if not protocols:
|
|
418
|
+
document.pop("protocols")
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _filter_agent_protocol_category(
|
|
422
|
+
protocols: dict[str, Any], category: str, recognized: set[str]
|
|
423
|
+
) -> None:
|
|
424
|
+
descriptors = protocols.get(category)
|
|
425
|
+
if not isinstance(descriptors, list):
|
|
426
|
+
return
|
|
427
|
+
filtered = [
|
|
428
|
+
descriptor
|
|
429
|
+
for descriptor in descriptors
|
|
430
|
+
if not (
|
|
431
|
+
isinstance(descriptor, dict)
|
|
432
|
+
and isinstance(descriptor.get("name"), str)
|
|
433
|
+
and descriptor["name"] not in recognized
|
|
434
|
+
)
|
|
435
|
+
]
|
|
436
|
+
if descriptors and not filtered:
|
|
437
|
+
protocols.pop(category)
|
|
438
|
+
else:
|
|
439
|
+
protocols[category] = filtered
|
|
440
|
+
|
|
441
|
+
|
|
85
442
|
def parse_collection(data: bytes | str) -> Collection:
|
|
86
443
|
value = _parse(data, "collection.schema.json", "Collection", Collection)
|
|
87
444
|
_validate_representation(
|
|
@@ -99,7 +456,14 @@ def parse_offering(data: bytes | str) -> Offering:
|
|
|
99
456
|
|
|
100
457
|
|
|
101
458
|
def parse_problem_details(data: bytes | str) -> ProblemDetails:
|
|
102
|
-
|
|
459
|
+
value = _parse(data, "problem-details.schema.json", "Problem Details", ProblemDetails)
|
|
460
|
+
expected_type = "https://offeringprotocol.org/problems/" + value.code.lower().replace("_", "-")
|
|
461
|
+
if value.problem_type != expected_type:
|
|
462
|
+
raise OdpValidationError(
|
|
463
|
+
"Problem Details",
|
|
464
|
+
[_issue("/type", "problem-type", "must correspond to the problem code")],
|
|
465
|
+
)
|
|
466
|
+
return value
|
|
103
467
|
|
|
104
468
|
|
|
105
469
|
def parse_problem_response(data: bytes | str, http_status: int) -> ProblemDetails:
|
{offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/directory/client.py
RENAMED
|
@@ -7,7 +7,11 @@ from urllib.parse import urlencode, urljoin
|
|
|
7
7
|
|
|
8
8
|
from pydantic import ValidationError as ModelValidationError
|
|
9
9
|
|
|
10
|
-
from offering_protocol.core import
|
|
10
|
+
from offering_protocol.core import (
|
|
11
|
+
OdpValidationError,
|
|
12
|
+
derive_service_origin,
|
|
13
|
+
parse_agent_service_document,
|
|
14
|
+
)
|
|
11
15
|
from offering_protocol.directory.models import (
|
|
12
16
|
DirectoryService,
|
|
13
17
|
Environment,
|
|
@@ -159,8 +163,18 @@ class DirectoryClient:
|
|
|
159
163
|
|
|
160
164
|
def _parse_search_page(body: bytes) -> SearchPage:
|
|
161
165
|
try:
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
raw = json.loads(body)
|
|
167
|
+
if isinstance(raw, dict) and isinstance(raw.get("items"), list):
|
|
168
|
+
for item in raw["items"]:
|
|
169
|
+
if isinstance(item, dict) and "protocols" in item:
|
|
170
|
+
_normalize_service_protocols(item)
|
|
171
|
+
page = SearchPage.model_validate(raw)
|
|
172
|
+
except (
|
|
173
|
+
UnicodeDecodeError,
|
|
174
|
+
json.JSONDecodeError,
|
|
175
|
+
ModelValidationError,
|
|
176
|
+
OdpValidationError,
|
|
177
|
+
) as error:
|
|
164
178
|
raise DirectoryError(f"invalid Directory response: {error}") from error
|
|
165
179
|
if len(page.items) > 100:
|
|
166
180
|
raise DirectoryError("Directory search page exceeds 100 Services")
|
|
@@ -170,6 +184,27 @@ def _parse_search_page(body: bytes) -> SearchPage:
|
|
|
170
184
|
return page
|
|
171
185
|
|
|
172
186
|
|
|
187
|
+
def _normalize_service_protocols(item: dict[str, object]) -> None:
|
|
188
|
+
candidate = {
|
|
189
|
+
"description": "Directory protocol validation",
|
|
190
|
+
"http": {"endpoint_base": "/"},
|
|
191
|
+
"language": "en",
|
|
192
|
+
"localizations": ["en"],
|
|
193
|
+
"name": "Directory Service",
|
|
194
|
+
"odp_version": "1.0",
|
|
195
|
+
"operations": [
|
|
196
|
+
{"authentication": "not-required", "name": "get-offering"},
|
|
197
|
+
{"authentication": "not-required", "name": "list-offerings"},
|
|
198
|
+
],
|
|
199
|
+
"protocols": item["protocols"],
|
|
200
|
+
}
|
|
201
|
+
document = parse_agent_service_document(json.dumps(candidate, separators=(",", ":")))
|
|
202
|
+
if document.protocols is None:
|
|
203
|
+
item.pop("protocols")
|
|
204
|
+
else:
|
|
205
|
+
item["protocols"] = document.protocols.model_dump(mode="json", exclude_defaults=True)
|
|
206
|
+
|
|
207
|
+
|
|
173
208
|
def _validate_search_request(request: SearchRequest) -> None:
|
|
174
209
|
if request.limit > 100:
|
|
175
210
|
raise DirectoryError("limit must be from 1 through 100")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/agent/capabilities.py
RENAMED
|
File without changes
|
|
File without changes
|
{offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/core/references.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/directory/__init__.py
RENAMED
|
File without changes
|
{offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/directory/models.py
RENAMED
|
File without changes
|
{offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/directory/transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/service/__init__.py
RENAMED
|
File without changes
|
{offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/service/service.py
RENAMED
|
File without changes
|
{offering_protocol-0.1.0 → offering_protocol-0.1.1}/src/offering_protocol/service/static_catalog.py
RENAMED
|
File without changes
|