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.
Files changed (80) hide show
  1. offering_protocol/__init__.py +7 -0
  2. offering_protocol/agent/__init__.py +66 -0
  3. offering_protocol/agent/agent.py +135 -0
  4. offering_protocol/agent/cache.py +51 -0
  5. offering_protocol/agent/capabilities.py +262 -0
  6. offering_protocol/agent/client.py +667 -0
  7. offering_protocol/agent/details.py +227 -0
  8. offering_protocol/agent/schema.py +129 -0
  9. offering_protocol/core/__init__.py +64 -0
  10. offering_protocol/core/models.py +475 -0
  11. offering_protocol/core/references.py +135 -0
  12. offering_protocol/core/schemas/action-relation.schema.json +9 -0
  13. offering_protocol/core/schemas/action-request.schema.json +18 -0
  14. offering_protocol/core/schemas/action.schema.json +56 -0
  15. offering_protocol/core/schemas/attribute-schema-reference.schema.json +6 -0
  16. offering_protocol/core/schemas/authentication-requirement.schema.json +11 -0
  17. offering_protocol/core/schemas/capability-identifier.schema.json +9 -0
  18. offering_protocol/core/schemas/capability-link.schema.json +14 -0
  19. offering_protocol/core/schemas/collection-search-request.schema.json +45 -0
  20. offering_protocol/core/schemas/collection.schema.json +72 -0
  21. offering_protocol/core/schemas/detail-fields.schema.json +16 -0
  22. offering_protocol/core/schemas/enrollment-protocol.schema.json +15 -0
  23. offering_protocol/core/schemas/filter-capability-source.schema.json +41 -0
  24. offering_protocol/core/schemas/filter-definition-page.schema.json +25 -0
  25. offering_protocol/core/schemas/filter-definition.schema.json +63 -0
  26. offering_protocol/core/schemas/filter-expression.schema.json +45 -0
  27. offering_protocol/core/schemas/filter-operator.schema.json +14 -0
  28. offering_protocol/core/schemas/filter-type.schema.json +14 -0
  29. offering_protocol/core/schemas/filter-unit.schema.json +45 -0
  30. offering_protocol/core/schemas/http-action-target.schema.json +36 -0
  31. offering_protocol/core/schemas/invalid-parameter.schema.json +55 -0
  32. offering_protocol/core/schemas/local-resource-identifier-list.schema.json +10 -0
  33. offering_protocol/core/schemas/local-resource-identifier.schema.json +10 -0
  34. offering_protocol/core/schemas/mcp-endpoint.schema.json +29 -0
  35. offering_protocol/core/schemas/offering-search-request.schema.json +68 -0
  36. offering_protocol/core/schemas/offering-search-response.schema.json +20 -0
  37. offering_protocol/core/schemas/offering.schema.json +92 -0
  38. offering_protocol/core/schemas/openapi-action-target.schema.json +20 -0
  39. offering_protocol/core/schemas/operation-descriptor.schema.json +27 -0
  40. offering_protocol/core/schemas/page-envelope.schema.json +25 -0
  41. offering_protocol/core/schemas/page-limit.schema.json +8 -0
  42. offering_protocol/core/schemas/payment-option.schema.json +24 -0
  43. offering_protocol/core/schemas/payment-protocol.schema.json +34 -0
  44. offering_protocol/core/schemas/price-preview.schema.json +133 -0
  45. offering_protocol/core/schemas/problem-code.schema.json +9 -0
  46. offering_protocol/core/schemas/problem-details.schema.json +66 -0
  47. offering_protocol/core/schemas/protocol-version.schema.json +8 -0
  48. offering_protocol/core/schemas/refinement-bucket.schema.json +28 -0
  49. offering_protocol/core/schemas/refinement-group.schema.json +24 -0
  50. offering_protocol/core/schemas/representation.schema.json +11 -0
  51. offering_protocol/core/schemas/resource-identity.schema.json +27 -0
  52. offering_protocol/core/schemas/resource-image.schema.json +40 -0
  53. offering_protocol/core/schemas/resource-reference.schema.json +19 -0
  54. offering_protocol/core/schemas/schema-reference.schema.json +15 -0
  55. offering_protocol/core/schemas/search-capabilities.schema.json +26 -0
  56. offering_protocol/core/schemas/service-branding-image.schema.json +23 -0
  57. offering_protocol/core/schemas/service-branding.schema.json +19 -0
  58. offering_protocol/core/schemas/service-document.schema.json +337 -0
  59. offering_protocol/core/schemas/service-openapi.schema.json +15 -0
  60. offering_protocol/core/schemas/service-origin.schema.json +9 -0
  61. offering_protocol/core/schemas/service-protocols.schema.json +101 -0
  62. offering_protocol/core/schemas/sort-capability-source.schema.json +41 -0
  63. offering_protocol/core/schemas/sort-definition-page.schema.json +25 -0
  64. offering_protocol/core/schemas/sort-definition.schema.json +35 -0
  65. offering_protocol/core/schemas/sort-key.schema.json +28 -0
  66. offering_protocol/core/schemas/top-level-document.schema.json +16 -0
  67. offering_protocol/core/schemas/trust-protocol.schema.json +15 -0
  68. offering_protocol/core/validation.py +390 -0
  69. offering_protocol/directory/__init__.py +51 -0
  70. offering_protocol/directory/client.py +206 -0
  71. offering_protocol/directory/models.py +103 -0
  72. offering_protocol/directory/transport.py +145 -0
  73. offering_protocol/py.typed +1 -0
  74. offering_protocol/service/__init__.py +32 -0
  75. offering_protocol/service/service.py +444 -0
  76. offering_protocol/service/static_catalog.py +258 -0
  77. offering_protocol-0.1.0.dist-info/METADATA +362 -0
  78. offering_protocol-0.1.0.dist-info/RECORD +80 -0
  79. offering_protocol-0.1.0.dist-info/WHEEL +4 -0
  80. 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
+ ]