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,103 @@
|
|
|
1
|
+
"""Canonical Directory request and response models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from offering_protocol.core.models import (
|
|
10
|
+
AuthenticationRequirement,
|
|
11
|
+
EnrollmentProtocol,
|
|
12
|
+
OdpModel,
|
|
13
|
+
Operation,
|
|
14
|
+
OperationDescriptor,
|
|
15
|
+
PaymentOption,
|
|
16
|
+
Protocol,
|
|
17
|
+
ServiceProtocols,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Environment(StrEnum):
|
|
22
|
+
PRODUCTION = "production"
|
|
23
|
+
SANDBOX = "sandbox"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def origin(self) -> str:
|
|
27
|
+
if self is Environment.PRODUCTION:
|
|
28
|
+
return "https://api.inflowpay.ai"
|
|
29
|
+
return "https://sandbox.inflowpay.ai"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class OperationFilter(OdpModel):
|
|
33
|
+
authentication: AuthenticationRequirement | None = None
|
|
34
|
+
name: Operation
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PaymentFilter(OdpModel):
|
|
38
|
+
authentication: AuthenticationRequirement | None = None
|
|
39
|
+
name: Protocol
|
|
40
|
+
options: list[PaymentOption] = Field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ServiceFilters(OdpModel):
|
|
44
|
+
enrollment: list[EnrollmentProtocol] = Field(default_factory=list)
|
|
45
|
+
keywords: list[str] = Field(default_factory=list)
|
|
46
|
+
operations: list[OperationFilter] = Field(default_factory=list)
|
|
47
|
+
payments: list[PaymentFilter] = Field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SearchRequest(OdpModel):
|
|
51
|
+
filters: ServiceFilters | None = None
|
|
52
|
+
limit: int = 0
|
|
53
|
+
query: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DirectoryService(OdpModel):
|
|
57
|
+
description: str
|
|
58
|
+
documentation_url: str = ""
|
|
59
|
+
indexed_at: str
|
|
60
|
+
keywords: list[str] = Field(default_factory=list)
|
|
61
|
+
language: str
|
|
62
|
+
localizations: list[str]
|
|
63
|
+
name: str
|
|
64
|
+
operations: list[OperationDescriptor]
|
|
65
|
+
protocols: ServiceProtocols | None = None
|
|
66
|
+
service_origin: str
|
|
67
|
+
status_url: str = ""
|
|
68
|
+
support_url: str = ""
|
|
69
|
+
website_url: str = ""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PaymentOptionFacetValue(OdpModel):
|
|
73
|
+
name: Protocol
|
|
74
|
+
option: PaymentOption
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Facet(OdpModel):
|
|
78
|
+
count: int
|
|
79
|
+
value: object
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Facets(OdpModel):
|
|
83
|
+
enrollment: list[Facet] = Field(default_factory=list)
|
|
84
|
+
keywords: list[Facet] = Field(default_factory=list)
|
|
85
|
+
operations: list[Facet] = Field(default_factory=list)
|
|
86
|
+
payment_options: list[Facet] = Field(default_factory=list)
|
|
87
|
+
payments: list[Facet] = Field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class SearchPage(OdpModel):
|
|
91
|
+
facets: Facets | None = None
|
|
92
|
+
items: list[DirectoryService]
|
|
93
|
+
next: str = ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SuggestionRequest(OdpModel):
|
|
97
|
+
limit: int = 0
|
|
98
|
+
prefix: str
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class IterationOptions(OdpModel):
|
|
102
|
+
max_items: int = 0
|
|
103
|
+
max_pages: int = 0
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Injectable asynchronous HTTP transport."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import socket
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from ipaddress import IPv4Address, IPv6Address, ip_address
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class HttpRequest:
|
|
17
|
+
method: str
|
|
18
|
+
url: str
|
|
19
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
20
|
+
body: bytes = b""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class HttpResponse:
|
|
25
|
+
status: int
|
|
26
|
+
headers: dict[str, str]
|
|
27
|
+
body: bytes
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TransportError(RuntimeError):
|
|
31
|
+
"""Raised when the HTTP transport cannot complete a request."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Transport(Protocol):
|
|
35
|
+
async def send(self, request: HttpRequest) -> HttpResponse: ...
|
|
36
|
+
|
|
37
|
+
async def aclose(self) -> None: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class HttpxTransport:
|
|
41
|
+
def __init__(
|
|
42
|
+
self, client: httpx.AsyncClient | None = None, *, allow_local_network: bool = False
|
|
43
|
+
) -> None:
|
|
44
|
+
self._allow_local_network = allow_local_network
|
|
45
|
+
self._client = client
|
|
46
|
+
self._clients: dict[tuple[str, str, int, str], httpx.AsyncClient] = {}
|
|
47
|
+
|
|
48
|
+
async def send(self, request: HttpRequest) -> HttpResponse:
|
|
49
|
+
try:
|
|
50
|
+
target, hostname, host_header = await _pinned_target(
|
|
51
|
+
request.url, self._allow_local_network
|
|
52
|
+
)
|
|
53
|
+
headers = {
|
|
54
|
+
name: value for name, value in request.headers.items() if name.lower() != "host"
|
|
55
|
+
}
|
|
56
|
+
headers["host"] = host_header
|
|
57
|
+
client = self._client
|
|
58
|
+
if client is None:
|
|
59
|
+
parsed_target = urlsplit(target)
|
|
60
|
+
key = (
|
|
61
|
+
parsed_target.scheme,
|
|
62
|
+
hostname,
|
|
63
|
+
parsed_target.port or (443 if parsed_target.scheme == "https" else 80),
|
|
64
|
+
str(parsed_target.hostname),
|
|
65
|
+
)
|
|
66
|
+
client = self._clients.get(key)
|
|
67
|
+
if client is None:
|
|
68
|
+
client = httpx.AsyncClient(follow_redirects=False, trust_env=False)
|
|
69
|
+
self._clients[key] = client
|
|
70
|
+
else:
|
|
71
|
+
headers["connection"] = "close"
|
|
72
|
+
outgoing = httpx.Request(
|
|
73
|
+
request.method,
|
|
74
|
+
target,
|
|
75
|
+
headers=headers,
|
|
76
|
+
content=request.body,
|
|
77
|
+
extensions={"sni_hostname": hostname},
|
|
78
|
+
)
|
|
79
|
+
response = await client.send(outgoing, follow_redirects=False)
|
|
80
|
+
except (httpx.HTTPError, OSError, ValueError) as error:
|
|
81
|
+
raise TransportError(f"HTTP transport failed: {error}") from error
|
|
82
|
+
return HttpResponse(
|
|
83
|
+
status=response.status_code,
|
|
84
|
+
headers={name.lower(): value for name, value in response.headers.items()},
|
|
85
|
+
body=response.content,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
async def aclose(self) -> None:
|
|
89
|
+
if self._client is not None:
|
|
90
|
+
await self._client.aclose()
|
|
91
|
+
for client in self._clients.values():
|
|
92
|
+
await client.aclose()
|
|
93
|
+
self._clients.clear()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
IPAddress = IPv4Address | IPv6Address
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def _pinned_target(url: str, allow_local_network: bool) -> tuple[str, str, str]:
|
|
100
|
+
parsed = urlsplit(url)
|
|
101
|
+
if parsed.hostname is None or parsed.username is not None or parsed.password is not None:
|
|
102
|
+
raise ValueError("ODP request URL must contain a host without credentials")
|
|
103
|
+
hostname = parsed.hostname.encode("idna").decode("ascii")
|
|
104
|
+
local_hostname = hostname.lower() in {"localhost", "127.0.0.1", "::1"}
|
|
105
|
+
if parsed.scheme != "https" and not (
|
|
106
|
+
parsed.scheme == "http" and allow_local_network and local_hostname
|
|
107
|
+
):
|
|
108
|
+
raise ValueError("ODP requests require HTTPS unless local development is enabled")
|
|
109
|
+
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
110
|
+
addresses = await _resolve_addresses(hostname, port)
|
|
111
|
+
if not addresses:
|
|
112
|
+
raise ValueError("ODP request host did not resolve")
|
|
113
|
+
if local_hostname and allow_local_network:
|
|
114
|
+
if any(not address.is_loopback for address in addresses):
|
|
115
|
+
raise ValueError("ODP local-development host resolved outside the loopback network")
|
|
116
|
+
elif any(not address.is_global for address in addresses):
|
|
117
|
+
raise ValueError("ODP request host resolved to a non-public address")
|
|
118
|
+
address = addresses[0]
|
|
119
|
+
pinned_host = f"[{address}]" if address.version == 6 else str(address)
|
|
120
|
+
explicit_port = parsed.port is not None
|
|
121
|
+
pinned_netloc = f"{pinned_host}:{port}" if explicit_port else pinned_host
|
|
122
|
+
original_host = (
|
|
123
|
+
f"[{hostname}]" if address.version == 6 and hostname == str(address) else hostname
|
|
124
|
+
)
|
|
125
|
+
default_port = (parsed.scheme == "https" and port == 443) or (
|
|
126
|
+
parsed.scheme == "http" and port == 80
|
|
127
|
+
)
|
|
128
|
+
host_header = original_host if default_port and not explicit_port else f"{original_host}:{port}"
|
|
129
|
+
return (
|
|
130
|
+
urlunsplit((parsed.scheme, pinned_netloc, parsed.path, parsed.query, parsed.fragment)),
|
|
131
|
+
hostname,
|
|
132
|
+
host_header,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
async def _resolve_addresses(hostname: str, port: int) -> tuple[IPAddress, ...]:
|
|
137
|
+
records = await asyncio.get_running_loop().getaddrinfo(
|
|
138
|
+
hostname, port, family=socket.AF_UNSPEC, type=socket.SOCK_STREAM
|
|
139
|
+
)
|
|
140
|
+
values: list[IPAddress] = []
|
|
141
|
+
for record in records:
|
|
142
|
+
value = ip_address(record[4][0])
|
|
143
|
+
if value not in values:
|
|
144
|
+
values.append(value)
|
|
145
|
+
return tuple(values)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Service-side Offering Discovery Protocol integration."""
|
|
2
|
+
|
|
3
|
+
from offering_protocol.service.service import (
|
|
4
|
+
MEDIA_TYPE,
|
|
5
|
+
PROBLEM_MEDIA_TYPE,
|
|
6
|
+
Catalog,
|
|
7
|
+
CatalogError,
|
|
8
|
+
CatalogRequest,
|
|
9
|
+
Request,
|
|
10
|
+
RequestError,
|
|
11
|
+
Response,
|
|
12
|
+
Service,
|
|
13
|
+
ServiceBuilder,
|
|
14
|
+
ServiceError,
|
|
15
|
+
)
|
|
16
|
+
from offering_protocol.service.static_catalog import StaticCatalog, StaticCatalogOptions
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"MEDIA_TYPE",
|
|
20
|
+
"PROBLEM_MEDIA_TYPE",
|
|
21
|
+
"Catalog",
|
|
22
|
+
"CatalogError",
|
|
23
|
+
"CatalogRequest",
|
|
24
|
+
"Request",
|
|
25
|
+
"RequestError",
|
|
26
|
+
"Response",
|
|
27
|
+
"Service",
|
|
28
|
+
"ServiceBuilder",
|
|
29
|
+
"ServiceError",
|
|
30
|
+
"StaticCatalog",
|
|
31
|
+
"StaticCatalogOptions",
|
|
32
|
+
]
|