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,475 @@
|
|
|
1
|
+
"""Typed Offering Discovery Protocol models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Generic, TypeVar, cast
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, JsonValue
|
|
9
|
+
|
|
10
|
+
VERSION = "1.0"
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"VERSION",
|
|
14
|
+
"Action",
|
|
15
|
+
"ActionRelation",
|
|
16
|
+
"ActionRequest",
|
|
17
|
+
"AuthenticationRequirement",
|
|
18
|
+
"CapabilityLink",
|
|
19
|
+
"Collection",
|
|
20
|
+
"CollectionSearchRequest",
|
|
21
|
+
"EnrollmentProtocol",
|
|
22
|
+
"FilterCapabilitySource",
|
|
23
|
+
"FilterDefinition",
|
|
24
|
+
"FilterExpression",
|
|
25
|
+
"FilterOperator",
|
|
26
|
+
"FilterType",
|
|
27
|
+
"FilterUnit",
|
|
28
|
+
"HttpActionTarget",
|
|
29
|
+
"HttpConfiguration",
|
|
30
|
+
"InvalidParameter",
|
|
31
|
+
"McpEndpoint",
|
|
32
|
+
"McpEndpointType",
|
|
33
|
+
"MissingPlacement",
|
|
34
|
+
"OdpModel",
|
|
35
|
+
"Offering",
|
|
36
|
+
"OfferingPage",
|
|
37
|
+
"OfferingSearchRequest",
|
|
38
|
+
"OpenApiActionTarget",
|
|
39
|
+
"Operation",
|
|
40
|
+
"OperationDescriptor",
|
|
41
|
+
"Page",
|
|
42
|
+
"PaymentOption",
|
|
43
|
+
"PaymentProtocol",
|
|
44
|
+
"PricePreview",
|
|
45
|
+
"PriceType",
|
|
46
|
+
"ProblemDetails",
|
|
47
|
+
"Protocol",
|
|
48
|
+
"RefinementBucket",
|
|
49
|
+
"RefinementGroup",
|
|
50
|
+
"Representation",
|
|
51
|
+
"ResourceIdentity",
|
|
52
|
+
"ResourceImage",
|
|
53
|
+
"ResourceImageType",
|
|
54
|
+
"ResourceType",
|
|
55
|
+
"SchemaReference",
|
|
56
|
+
"SearchCapabilities",
|
|
57
|
+
"ServiceBranding",
|
|
58
|
+
"ServiceBrandingImage",
|
|
59
|
+
"ServiceBrandingImageType",
|
|
60
|
+
"ServiceDocument",
|
|
61
|
+
"ServiceOpenApi",
|
|
62
|
+
"ServiceProtocols",
|
|
63
|
+
"SortCapabilitySource",
|
|
64
|
+
"SortDefinition",
|
|
65
|
+
"SortDirection",
|
|
66
|
+
"SortKey",
|
|
67
|
+
"TrustProtocol",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class OdpModel(BaseModel):
|
|
72
|
+
"""Base model that preserves additive protocol members."""
|
|
73
|
+
|
|
74
|
+
model_config = ConfigDict(extra="allow", frozen=True, populate_by_name=True)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def additional(self) -> dict[str, JsonValue]:
|
|
78
|
+
return dict(self.model_extra or {})
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> dict[str, JsonValue]:
|
|
81
|
+
return cast(
|
|
82
|
+
dict[str, JsonValue],
|
|
83
|
+
self.model_dump(mode="json", by_alias=True, exclude_unset=True),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Representation(StrEnum):
|
|
88
|
+
TERSE = "terse"
|
|
89
|
+
FULL = "full"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PriceType(StrEnum):
|
|
93
|
+
FIXED = "fixed"
|
|
94
|
+
FREE = "free"
|
|
95
|
+
METERED = "metered"
|
|
96
|
+
QUOTE = "quote"
|
|
97
|
+
RANGE = "range"
|
|
98
|
+
STARTING_AT = "starting_at"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ActionRelation(StrEnum):
|
|
102
|
+
DOWNLOAD = "download"
|
|
103
|
+
INVOKE = "invoke"
|
|
104
|
+
PURCHASE = "purchase"
|
|
105
|
+
QUOTE = "quote"
|
|
106
|
+
RESERVE = "reserve"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ResourceType(StrEnum):
|
|
110
|
+
COLLECTION = "collection"
|
|
111
|
+
OFFERING = "offering"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Operation(StrEnum):
|
|
115
|
+
GET_COLLECTION = "get-collection"
|
|
116
|
+
GET_OFFERING = "get-offering"
|
|
117
|
+
LIST_COLLECTION_OFFERINGS = "list-collection-offerings"
|
|
118
|
+
LIST_COLLECTIONS = "list-collections"
|
|
119
|
+
LIST_OFFERINGS = "list-offerings"
|
|
120
|
+
SEARCH_COLLECTIONS = "search-collections"
|
|
121
|
+
SEARCH_OFFERINGS = "search-offerings"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AuthenticationRequirement(StrEnum):
|
|
125
|
+
NOT_REQUIRED = "not-required"
|
|
126
|
+
OPTIONAL = "optional"
|
|
127
|
+
REQUIRED = "required"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class Protocol(StrEnum):
|
|
131
|
+
AEP = "aep"
|
|
132
|
+
MPP = "mpp"
|
|
133
|
+
TAP = "tap"
|
|
134
|
+
X402 = "x402"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PaymentOption(StrEnum):
|
|
138
|
+
ALGORAND = "algorand"
|
|
139
|
+
APTOS = "aptos"
|
|
140
|
+
ARBITRUM = "arbitrum"
|
|
141
|
+
AVALANCHE = "avalanche"
|
|
142
|
+
BASE = "base"
|
|
143
|
+
CARD = "card"
|
|
144
|
+
ETHEREUM = "ethereum"
|
|
145
|
+
HEDERA = "hedera"
|
|
146
|
+
INFLOW = "inflow"
|
|
147
|
+
LIGHTNING = "lightning"
|
|
148
|
+
POLYGON = "polygon"
|
|
149
|
+
SOLANA = "solana"
|
|
150
|
+
STELLAR = "stellar"
|
|
151
|
+
STRIPE = "stripe"
|
|
152
|
+
TEMPO = "tempo"
|
|
153
|
+
TON = "ton"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class FilterType(StrEnum):
|
|
157
|
+
BOOLEAN = "boolean"
|
|
158
|
+
DATE = "date"
|
|
159
|
+
DATE_TIME = "date-time"
|
|
160
|
+
DECIMAL = "decimal"
|
|
161
|
+
INTEGER = "integer"
|
|
162
|
+
NUMBER = "number"
|
|
163
|
+
STRING = "string"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class FilterOperator(StrEnum):
|
|
167
|
+
EQUAL = "eq"
|
|
168
|
+
EXISTS = "exists"
|
|
169
|
+
GREATER_THAN = "gt"
|
|
170
|
+
GREATER_THAN_OR_EQUAL = "gte"
|
|
171
|
+
IN = "in"
|
|
172
|
+
LESS_THAN = "lt"
|
|
173
|
+
LESS_THAN_OR_EQUAL = "lte"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class SortDirection(StrEnum):
|
|
177
|
+
ASCENDING = "ascending"
|
|
178
|
+
DESCENDING = "descending"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class MissingPlacement(StrEnum):
|
|
182
|
+
FIRST = "first"
|
|
183
|
+
LAST = "last"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class ServiceBrandingImageType(StrEnum):
|
|
187
|
+
PNG = "image/png"
|
|
188
|
+
SVG = "image/svg+xml"
|
|
189
|
+
WEBP = "image/webp"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ResourceImageType(StrEnum):
|
|
193
|
+
AVIF = "image/avif"
|
|
194
|
+
JPEG = "image/jpeg"
|
|
195
|
+
PNG = "image/png"
|
|
196
|
+
SVG = "image/svg+xml"
|
|
197
|
+
WEBP = "image/webp"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class McpEndpointType(StrEnum):
|
|
201
|
+
STREAMABLE_HTTP = "streamable-http"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class OperationDescriptor(OdpModel):
|
|
205
|
+
authentication: AuthenticationRequirement
|
|
206
|
+
name: Operation
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class EnrollmentProtocol(OdpModel):
|
|
210
|
+
name: Protocol
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class PaymentProtocol(OdpModel):
|
|
214
|
+
authentication: AuthenticationRequirement
|
|
215
|
+
name: Protocol
|
|
216
|
+
options: list[PaymentOption] = Field(default_factory=list)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class TrustProtocol(OdpModel):
|
|
220
|
+
name: Protocol
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ServiceProtocols(OdpModel):
|
|
224
|
+
enrollment: list[EnrollmentProtocol] = Field(default_factory=list)
|
|
225
|
+
payments: list[PaymentProtocol] = Field(default_factory=list)
|
|
226
|
+
trust: list[TrustProtocol] = Field(default_factory=list)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class ServiceOpenApi(OdpModel):
|
|
230
|
+
url: str
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class HttpConfiguration(OdpModel):
|
|
234
|
+
endpoint_base: str
|
|
235
|
+
openapi: ServiceOpenApi | None = None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class CapabilityLink(OdpModel):
|
|
239
|
+
href: str
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class FilterUnit(OdpModel):
|
|
243
|
+
code: str
|
|
244
|
+
system: str
|
|
245
|
+
title: str = ""
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class FilterDefinition(OdpModel):
|
|
249
|
+
description: str
|
|
250
|
+
id: str
|
|
251
|
+
operators: list[FilterOperator]
|
|
252
|
+
refinable: bool = False
|
|
253
|
+
title: str
|
|
254
|
+
filter_type: FilterType = Field(alias="type")
|
|
255
|
+
unit: FilterUnit | None = None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class SortKey(OdpModel):
|
|
259
|
+
direction: SortDirection
|
|
260
|
+
filter_id: str
|
|
261
|
+
missing: MissingPlacement
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class SortDefinition(OdpModel):
|
|
265
|
+
description: str
|
|
266
|
+
id: str
|
|
267
|
+
keys: list[SortKey]
|
|
268
|
+
title: str
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class FilterCapabilitySource(OdpModel):
|
|
272
|
+
inline: list[FilterDefinition] = Field(default_factory=list)
|
|
273
|
+
linked: CapabilityLink | None = None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class SortCapabilitySource(OdpModel):
|
|
277
|
+
inline: list[SortDefinition] = Field(default_factory=list)
|
|
278
|
+
linked: CapabilityLink | None = None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class SearchCapabilities(OdpModel):
|
|
282
|
+
filters: FilterCapabilitySource | None = None
|
|
283
|
+
sorts: SortCapabilitySource | None = None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class ServiceBrandingImage(OdpModel):
|
|
287
|
+
src: str
|
|
288
|
+
media_type: ServiceBrandingImageType | None = Field(default=None, alias="type")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class ServiceBranding(OdpModel):
|
|
292
|
+
icon: ServiceBrandingImage
|
|
293
|
+
logo: ServiceBrandingImage
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class McpEndpoint(OdpModel):
|
|
297
|
+
description: str = ""
|
|
298
|
+
name: str = ""
|
|
299
|
+
endpoint_type: McpEndpointType = Field(alias="type")
|
|
300
|
+
url: str
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class ServiceDocument(OdpModel):
|
|
304
|
+
branding: ServiceBranding | None = None
|
|
305
|
+
description: str
|
|
306
|
+
documentation_url: str = ""
|
|
307
|
+
http: HttpConfiguration
|
|
308
|
+
keywords: list[str] = Field(default_factory=list)
|
|
309
|
+
language: str
|
|
310
|
+
localizations: list[str]
|
|
311
|
+
mcp: list[McpEndpoint] = Field(default_factory=list)
|
|
312
|
+
name: str
|
|
313
|
+
odp_version: str
|
|
314
|
+
operations: list[OperationDescriptor]
|
|
315
|
+
payment_origins: list[str] = Field(default_factory=list)
|
|
316
|
+
protocols: ServiceProtocols | None = None
|
|
317
|
+
search_capabilities: SearchCapabilities | None = None
|
|
318
|
+
status_url: str = ""
|
|
319
|
+
support_url: str = ""
|
|
320
|
+
website_url: str = ""
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class ResourceImage(OdpModel):
|
|
324
|
+
alt: str = ""
|
|
325
|
+
height: int = 0
|
|
326
|
+
src: str
|
|
327
|
+
media_type: ResourceImageType | None = Field(default=None, alias="type")
|
|
328
|
+
width: int = 0
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class Collection(OdpModel):
|
|
332
|
+
auth_expands: bool = False
|
|
333
|
+
description: str = ""
|
|
334
|
+
detail_fields: list[str] = Field(default_factory=list)
|
|
335
|
+
id: str
|
|
336
|
+
images: list[ResourceImage] = Field(default_factory=list)
|
|
337
|
+
language: str = ""
|
|
338
|
+
localizations: list[str] = Field(default_factory=list)
|
|
339
|
+
name: str
|
|
340
|
+
odp_version: str = ""
|
|
341
|
+
parent_ids: list[str] = Field(default_factory=list)
|
|
342
|
+
search_capabilities: SearchCapabilities | None = None
|
|
343
|
+
web_url: str = ""
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class SchemaReference(OdpModel):
|
|
347
|
+
url: str
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class PricePreview(OdpModel):
|
|
351
|
+
amount: str = ""
|
|
352
|
+
currency: str = ""
|
|
353
|
+
maximum: str = ""
|
|
354
|
+
minimum: str = ""
|
|
355
|
+
price_type: PriceType = Field(alias="type")
|
|
356
|
+
unit: str = ""
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class ActionRequest(OdpModel):
|
|
360
|
+
content_type: str = ""
|
|
361
|
+
schema_: SchemaReference | None = Field(default=None, alias="schema")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class HttpActionTarget(OdpModel):
|
|
365
|
+
href: str
|
|
366
|
+
method: str
|
|
367
|
+
request: ActionRequest | None = None
|
|
368
|
+
response_content_types: list[str] = Field(default_factory=list)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class OpenApiActionTarget(OdpModel):
|
|
372
|
+
operation_id: str
|
|
373
|
+
url: str = ""
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class Action(OdpModel):
|
|
377
|
+
authentication: AuthenticationRequirement
|
|
378
|
+
description: str = ""
|
|
379
|
+
http: HttpActionTarget | None = None
|
|
380
|
+
id: str
|
|
381
|
+
openapi: OpenApiActionTarget | None = None
|
|
382
|
+
rel: ActionRelation
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class Offering(OdpModel):
|
|
386
|
+
actions: list[Action] = Field(default_factory=list)
|
|
387
|
+
auth_expands: bool = False
|
|
388
|
+
attributes: dict[str, JsonValue] = Field(default_factory=dict)
|
|
389
|
+
collection_ids: list[str] = Field(default_factory=list)
|
|
390
|
+
description: str = ""
|
|
391
|
+
detail_fields: list[str] = Field(default_factory=list)
|
|
392
|
+
id: str
|
|
393
|
+
images: list[ResourceImage] = Field(default_factory=list)
|
|
394
|
+
language: str = ""
|
|
395
|
+
localizations: list[str] = Field(default_factory=list)
|
|
396
|
+
name: str
|
|
397
|
+
odp_version: str = ""
|
|
398
|
+
price: PricePreview | None = None
|
|
399
|
+
schema_: SchemaReference | None = Field(default=None, alias="schema")
|
|
400
|
+
web_url: str = ""
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class InvalidParameter(OdpModel):
|
|
404
|
+
location: str = Field(alias="in")
|
|
405
|
+
name: str
|
|
406
|
+
reason: str
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class ProblemDetails(OdpModel):
|
|
410
|
+
code: str
|
|
411
|
+
detail: str = ""
|
|
412
|
+
instance: str = ""
|
|
413
|
+
invalid_params: list[InvalidParameter] = Field(default_factory=list)
|
|
414
|
+
status: int
|
|
415
|
+
title: str
|
|
416
|
+
problem_type: str = Field(alias="type")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class ResourceIdentity(OdpModel):
|
|
420
|
+
id: str
|
|
421
|
+
service: str
|
|
422
|
+
resource_type: ResourceType = Field(alias="type")
|
|
423
|
+
|
|
424
|
+
@property
|
|
425
|
+
def key(self) -> str:
|
|
426
|
+
return f"{self.service}\0{self.resource_type.value}\0{self.id}"
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class CollectionSearchRequest(OdpModel):
|
|
430
|
+
limit: int = 0
|
|
431
|
+
odp_version: str = VERSION
|
|
432
|
+
parent_id: str | None = None
|
|
433
|
+
query: str = ""
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class FilterExpression(OdpModel):
|
|
437
|
+
id: str
|
|
438
|
+
operator: FilterOperator
|
|
439
|
+
value: JsonValue
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class OfferingSearchRequest(OdpModel):
|
|
443
|
+
collection_id: str = ""
|
|
444
|
+
filters: list[FilterExpression] = Field(default_factory=list)
|
|
445
|
+
include_descendants: bool = False
|
|
446
|
+
limit: int = 0
|
|
447
|
+
odp_version: str = VERSION
|
|
448
|
+
query: str = ""
|
|
449
|
+
refinements: list[str] = Field(default_factory=list)
|
|
450
|
+
sort: str = ""
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class RefinementBucket(OdpModel):
|
|
454
|
+
count: int
|
|
455
|
+
count_relation: str = ""
|
|
456
|
+
value: JsonValue
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class RefinementGroup(OdpModel):
|
|
460
|
+
filter_id: str
|
|
461
|
+
values: list[RefinementBucket]
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
Item = TypeVar("Item")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class Page(OdpModel, Generic[Item]):
|
|
468
|
+
auth_expands: bool = False
|
|
469
|
+
items: list[Item]
|
|
470
|
+
next: str = ""
|
|
471
|
+
odp_version: str
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class OfferingPage(Page[Item], Generic[Item]):
|
|
475
|
+
refinements: list[RefinementGroup] = Field(default_factory=list)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""ODP URL, reference, operation-path, and identity helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ipaddress import ip_address
|
|
6
|
+
from typing import cast
|
|
7
|
+
from urllib.parse import SplitResult, urljoin, urlsplit, urlunsplit
|
|
8
|
+
|
|
9
|
+
from offering_protocol.core.models import Operation, ResourceIdentity, ResourceType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ReferenceError(ValueError):
|
|
13
|
+
"""Raised when an ODP URL or reference violates protocol constraints."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_RESOURCE_OPERATIONS = {
|
|
17
|
+
Operation.GET_COLLECTION,
|
|
18
|
+
Operation.GET_OFFERING,
|
|
19
|
+
Operation.LIST_COLLECTION_OFFERINGS,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_OPERATION_PATHS = {
|
|
23
|
+
Operation.LIST_COLLECTIONS: "/collections",
|
|
24
|
+
Operation.SEARCH_COLLECTIONS: "/collections/search",
|
|
25
|
+
Operation.LIST_OFFERINGS: "/offerings",
|
|
26
|
+
Operation.SEARCH_OFFERINGS: "/offerings/search",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_local_resource_identifier(value: str) -> bool:
|
|
31
|
+
return (
|
|
32
|
+
bool(value)
|
|
33
|
+
and value not in {".", ".."}
|
|
34
|
+
and len(value) <= 128
|
|
35
|
+
and all(
|
|
36
|
+
character.isascii() and (character.isalnum() or character in "._~-")
|
|
37
|
+
for character in value
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def operation_method(operation: Operation) -> str:
|
|
43
|
+
if operation in {Operation.SEARCH_COLLECTIONS, Operation.SEARCH_OFFERINGS}:
|
|
44
|
+
return "POST"
|
|
45
|
+
return "GET"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def derive_service_origin(service_url: str) -> str:
|
|
49
|
+
parsed = _parse_secure_url(service_url)
|
|
50
|
+
hostname = cast(str, parsed.hostname)
|
|
51
|
+
host = f"[{hostname}]" if ":" in hostname else hostname
|
|
52
|
+
port = parsed.port
|
|
53
|
+
if port is not None and not (parsed.scheme == "https" and port == 443):
|
|
54
|
+
host = f"{host}:{port}"
|
|
55
|
+
return urlunsplit((parsed.scheme, host, "", "", ""))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_resource_reference(reference: str, service_origin: str) -> str:
|
|
59
|
+
if reference.startswith("//"):
|
|
60
|
+
raise ReferenceError("ODP resource reference cannot be scheme-relative")
|
|
61
|
+
if not reference.startswith(
|
|
62
|
+
("/", "https://", "http://localhost", "http://127.0.0.1", "http://[::1]")
|
|
63
|
+
):
|
|
64
|
+
raise ReferenceError(
|
|
65
|
+
"ODP resource reference must be an origin-relative absolute path or secure absolute URL"
|
|
66
|
+
)
|
|
67
|
+
resolved = _parse_secure_url(urljoin(f"{derive_service_origin(service_origin)}/", reference))
|
|
68
|
+
if resolved.fragment:
|
|
69
|
+
raise ReferenceError("ODP resource reference cannot contain a fragment")
|
|
70
|
+
return resolved.geturl()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def resolve_continuation(reference: str, service_origin: str) -> str:
|
|
74
|
+
resolved = resolve_resource_reference(reference, service_origin)
|
|
75
|
+
if derive_service_origin(resolved) != derive_service_origin(service_origin):
|
|
76
|
+
raise ReferenceError("ODP continuation reference must remain on the Service origin")
|
|
77
|
+
return resolved
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_operation_url(
|
|
81
|
+
endpoint_base: str,
|
|
82
|
+
operation: Operation,
|
|
83
|
+
service_origin: str,
|
|
84
|
+
resource_id: str | None = None,
|
|
85
|
+
) -> str:
|
|
86
|
+
if not endpoint_base.startswith("/") or endpoint_base.startswith("//"):
|
|
87
|
+
raise ReferenceError("ODP endpoint base must be an origin-relative absolute path")
|
|
88
|
+
path = operation_path(operation, resource_id)
|
|
89
|
+
return resolve_resource_reference(f"{endpoint_base.rstrip('/')}{path}", service_origin)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def operation_path(operation: Operation, resource_id: str | None = None) -> str:
|
|
93
|
+
if operation in _RESOURCE_OPERATIONS:
|
|
94
|
+
if resource_id is None or not is_local_resource_identifier(resource_id):
|
|
95
|
+
raise ReferenceError(f"{operation.value} requires a valid local resource identifier")
|
|
96
|
+
elif resource_id is not None:
|
|
97
|
+
raise ReferenceError(f"{operation.value} does not accept a resource identifier")
|
|
98
|
+
|
|
99
|
+
if operation is Operation.GET_COLLECTION:
|
|
100
|
+
return f"/collections/{resource_id}"
|
|
101
|
+
if operation is Operation.LIST_COLLECTION_OFFERINGS:
|
|
102
|
+
return f"/collections/{resource_id}/offerings"
|
|
103
|
+
if operation is Operation.GET_OFFERING:
|
|
104
|
+
return f"/offerings/{resource_id}"
|
|
105
|
+
return _OPERATION_PATHS[operation]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def create_resource_identity(
|
|
109
|
+
service_document_url: str, resource_type: ResourceType, resource_id: str
|
|
110
|
+
) -> ResourceIdentity:
|
|
111
|
+
if not is_local_resource_identifier(resource_id):
|
|
112
|
+
raise ReferenceError(f"{resource_type.value} requires a valid local resource identifier")
|
|
113
|
+
return ResourceIdentity(
|
|
114
|
+
id=resource_id,
|
|
115
|
+
service=derive_service_origin(service_document_url),
|
|
116
|
+
type=resource_type,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _parse_secure_url(value: str) -> SplitResult:
|
|
121
|
+
parsed = urlsplit(value)
|
|
122
|
+
if parsed.username is not None or parsed.password is not None:
|
|
123
|
+
raise ReferenceError("ODP URL cannot contain user information")
|
|
124
|
+
hostname = parsed.hostname
|
|
125
|
+
if hostname is None:
|
|
126
|
+
raise ReferenceError("ODP URL must include a host")
|
|
127
|
+
loopback = hostname.lower() == "localhost"
|
|
128
|
+
if not loopback:
|
|
129
|
+
try:
|
|
130
|
+
loopback = ip_address(hostname).is_loopback
|
|
131
|
+
except ValueError:
|
|
132
|
+
loopback = False
|
|
133
|
+
if parsed.scheme != "https" and not (parsed.scheme == "http" and loopback):
|
|
134
|
+
raise ReferenceError("ODP URL must use HTTPS except on loopback hosts")
|
|
135
|
+
return parsed
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://offeringprotocol.org/schemas/action-relation.schema.json",
|
|
4
|
+
"title": "ODP Action relation",
|
|
5
|
+
"type": "string",
|
|
6
|
+
"minLength": 1,
|
|
7
|
+
"maxLength": 64,
|
|
8
|
+
"pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
|
|
9
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://offeringprotocol.org/schemas/action-request.schema.json",
|
|
4
|
+
"title": "ODP compact Action request body",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"minProperties": 1,
|
|
8
|
+
"properties": {
|
|
9
|
+
"content_type": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"minLength": 1,
|
|
12
|
+
"maxLength": 255
|
|
13
|
+
},
|
|
14
|
+
"schema": {
|
|
15
|
+
"$ref": "schema-reference.schema.json"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://offeringprotocol.org/schemas/action.schema.json",
|
|
4
|
+
"title": "ODP Offering Action",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"authentication",
|
|
9
|
+
"id",
|
|
10
|
+
"rel"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"authentication": {
|
|
14
|
+
"$ref": "authentication-requirement.schema.json"
|
|
15
|
+
},
|
|
16
|
+
"id": {
|
|
17
|
+
"$ref": "local-resource-identifier.schema.json"
|
|
18
|
+
},
|
|
19
|
+
"rel": {
|
|
20
|
+
"$ref": "action-relation.schema.json"
|
|
21
|
+
},
|
|
22
|
+
"description": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"minLength": 1,
|
|
25
|
+
"maxLength": 512
|
|
26
|
+
},
|
|
27
|
+
"http": {
|
|
28
|
+
"$ref": "http-action-target.schema.json"
|
|
29
|
+
},
|
|
30
|
+
"openapi": {
|
|
31
|
+
"$ref": "openapi-action-target.schema.json"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"oneOf": [
|
|
35
|
+
{
|
|
36
|
+
"required": [
|
|
37
|
+
"http"
|
|
38
|
+
],
|
|
39
|
+
"not": {
|
|
40
|
+
"required": [
|
|
41
|
+
"openapi"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"required": [
|
|
47
|
+
"openapi"
|
|
48
|
+
],
|
|
49
|
+
"not": {
|
|
50
|
+
"required": [
|
|
51
|
+
"http"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|