millwork-solver 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.
- millwork_solver/__init__.py +24 -0
- millwork_solver/_contract.json +292 -0
- millwork_solver/_core/__init__.py +3 -0
- millwork_solver/_core/boundary.py +110 -0
- millwork_solver/_core/catalog.py +218 -0
- millwork_solver/_core/clients.py +264 -0
- millwork_solver/_core/errors.py +11 -0
- millwork_solver/_core/transport.py +274 -0
- millwork_solver/client.py +251 -0
- millwork_solver/errors.py +62 -0
- millwork_solver/py.typed +1 -0
- millwork_solver/types.py +15 -0
- millwork_solver-0.1.0.dist-info/METADATA +32 -0
- millwork_solver-0.1.0.dist-info/RECORD +17 -0
- millwork_solver-0.1.0.dist-info/WHEEL +5 -0
- millwork_solver-0.1.0.dist-info/licenses/LICENSE +201 -0
- millwork_solver-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Public Millwork Solver Python client facade."""
|
|
2
|
+
|
|
3
|
+
from .client import AsyncSolver, Solver
|
|
4
|
+
from .errors import (
|
|
5
|
+
ClientClosedError,
|
|
6
|
+
FieldError,
|
|
7
|
+
NetworkError,
|
|
8
|
+
OperationBoundaryError,
|
|
9
|
+
ProblemError,
|
|
10
|
+
ProtocolError,
|
|
11
|
+
)
|
|
12
|
+
from .types import Page
|
|
13
|
+
|
|
14
|
+
__all__ = (
|
|
15
|
+
"AsyncSolver",
|
|
16
|
+
"ClientClosedError",
|
|
17
|
+
"FieldError",
|
|
18
|
+
"NetworkError",
|
|
19
|
+
"OperationBoundaryError",
|
|
20
|
+
"Page",
|
|
21
|
+
"ProblemError",
|
|
22
|
+
"ProtocolError",
|
|
23
|
+
"Solver",
|
|
24
|
+
)
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"generated_from": {
|
|
4
|
+
"python_operation_view": "contracts/developer-experience/python-operation-view.json",
|
|
5
|
+
"operation_manifest_version": "1.0.0",
|
|
6
|
+
"openapi_sha256": "ad959a037922a16bda51221ed989d00ea0ae6b70eb86b5f2aef07eccee91d3c6",
|
|
7
|
+
"retry_fixture": "contracts/developer-experience/fixtures/retry-error-idempotency.json"
|
|
8
|
+
},
|
|
9
|
+
"operations": [
|
|
10
|
+
{
|
|
11
|
+
"operation_id": "deleteArmsByArmId",
|
|
12
|
+
"method": "DELETE",
|
|
13
|
+
"path": "/v1/arms/{armId}"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"operation_id": "deleteSourceConnectionsByConnectionId",
|
|
17
|
+
"method": "DELETE",
|
|
18
|
+
"path": "/v1/source-connections/{connectionId}"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"operation_id": "deleteVerifiersByVerifierId",
|
|
22
|
+
"method": "DELETE",
|
|
23
|
+
"path": "/v1/verifiers/{verifierId}"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"operation_id": "getAccount",
|
|
27
|
+
"method": "GET",
|
|
28
|
+
"path": "/v1/account"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"operation_id": "getApiKeys",
|
|
32
|
+
"method": "GET",
|
|
33
|
+
"path": "/v1/api-keys"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"operation_id": "getArms",
|
|
37
|
+
"method": "GET",
|
|
38
|
+
"path": "/v1/arms"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"operation_id": "getArmsByArmId",
|
|
42
|
+
"method": "GET",
|
|
43
|
+
"path": "/v1/arms/{armId}"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"operation_id": "getComplianceExport",
|
|
47
|
+
"method": "GET",
|
|
48
|
+
"path": "/v1/compliance-export"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"operation_id": "getCredentials",
|
|
52
|
+
"method": "GET",
|
|
53
|
+
"path": "/v1/credentials"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"operation_id": "getEvalSummary",
|
|
57
|
+
"method": "GET",
|
|
58
|
+
"path": "/v1/eval-summary"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"operation_id": "getExecutions",
|
|
62
|
+
"method": "GET",
|
|
63
|
+
"path": "/v1/executions"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"operation_id": "getExecutionsByExecutionId",
|
|
67
|
+
"method": "GET",
|
|
68
|
+
"path": "/v1/executions/{executionId}"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"operation_id": "getExecutionsByExecutionIdEvents",
|
|
72
|
+
"method": "GET",
|
|
73
|
+
"path": "/v1/executions/{executionId}/events"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"operation_id": "getExecutionsByExecutionIdResult",
|
|
77
|
+
"method": "GET",
|
|
78
|
+
"path": "/v1/executions/{executionId}/result"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"operation_id": "getModelCatalog",
|
|
82
|
+
"method": "GET",
|
|
83
|
+
"path": "/v1/model-catalog"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"operation_id": "getModelDefinitions",
|
|
87
|
+
"method": "GET",
|
|
88
|
+
"path": "/v1/model-definitions"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"operation_id": "getModelDeployments",
|
|
92
|
+
"method": "GET",
|
|
93
|
+
"path": "/v1/model-deployments"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"operation_id": "getModelDeploymentsByModelDeploymentId",
|
|
97
|
+
"method": "GET",
|
|
98
|
+
"path": "/v1/model-deployments/{modelDeploymentId}"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"operation_id": "getModelSourceProfiles",
|
|
102
|
+
"method": "GET",
|
|
103
|
+
"path": "/v1/model-source-profiles"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"operation_id": "getProposals",
|
|
107
|
+
"method": "GET",
|
|
108
|
+
"path": "/v1/proposals"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"operation_id": "getProposalsByProposalId",
|
|
112
|
+
"method": "GET",
|
|
113
|
+
"path": "/v1/proposals/{proposalId}"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"operation_id": "getReceipts",
|
|
117
|
+
"method": "GET",
|
|
118
|
+
"path": "/v1/receipts"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"operation_id": "getReceiptsByExecutionId",
|
|
122
|
+
"method": "GET",
|
|
123
|
+
"path": "/v1/receipts/{executionId}"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"operation_id": "getSourceConnections",
|
|
127
|
+
"method": "GET",
|
|
128
|
+
"path": "/v1/source-connections"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"operation_id": "getSourceConnectionsByConnectionId",
|
|
132
|
+
"method": "GET",
|
|
133
|
+
"path": "/v1/source-connections/{connectionId}"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"operation_id": "getSourceCredentialHandoffsByHandoffIntentId",
|
|
137
|
+
"method": "GET",
|
|
138
|
+
"path": "/v1/source-credential-handoffs/{handoffIntentId}"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"operation_id": "getUsage",
|
|
142
|
+
"method": "GET",
|
|
143
|
+
"path": "/v1/usage"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"operation_id": "getVerifiers",
|
|
147
|
+
"method": "GET",
|
|
148
|
+
"path": "/v1/verifiers"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"operation_id": "getVerifiersByVerifierId",
|
|
152
|
+
"method": "GET",
|
|
153
|
+
"path": "/v1/verifiers/{verifierId}"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"operation_id": "getWebhooks",
|
|
157
|
+
"method": "GET",
|
|
158
|
+
"path": "/v1/webhooks"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"operation_id": "getWebhooksByWebhookIdDeliveries",
|
|
162
|
+
"method": "GET",
|
|
163
|
+
"path": "/v1/webhooks/{webhookId}/deliveries"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"operation_id": "patchApiKeysByApiKeyId",
|
|
167
|
+
"method": "PATCH",
|
|
168
|
+
"path": "/v1/api-keys/{apiKeyId}"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"operation_id": "patchArmsByArmId",
|
|
172
|
+
"method": "PATCH",
|
|
173
|
+
"path": "/v1/arms/{armId}"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"operation_id": "patchTenantsByTenantId",
|
|
177
|
+
"method": "PATCH",
|
|
178
|
+
"path": "/v1/tenants/{tenantId}"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"operation_id": "patchVerifiersByVerifierId",
|
|
182
|
+
"method": "PATCH",
|
|
183
|
+
"path": "/v1/verifiers/{verifierId}"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"operation_id": "postApiKeys",
|
|
187
|
+
"method": "POST",
|
|
188
|
+
"path": "/v1/api-keys"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"operation_id": "postApiKeysByApiKeyIdRevoke",
|
|
192
|
+
"method": "POST",
|
|
193
|
+
"path": "/v1/api-keys/{apiKeyId}/revoke"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"operation_id": "postArms",
|
|
197
|
+
"method": "POST",
|
|
198
|
+
"path": "/v1/arms"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"operation_id": "postCredentials",
|
|
202
|
+
"method": "POST",
|
|
203
|
+
"path": "/v1/credentials"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"operation_id": "postExecutions",
|
|
207
|
+
"method": "POST",
|
|
208
|
+
"path": "/v1/executions"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"operation_id": "postExecutionsByExecutionIdCancel",
|
|
212
|
+
"method": "POST",
|
|
213
|
+
"path": "/v1/executions/{executionId}/cancel"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"operation_id": "postProposalsByProposalIdApprove",
|
|
217
|
+
"method": "POST",
|
|
218
|
+
"path": "/v1/proposals/{proposalId}/approve"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"operation_id": "postProposalsByProposalIdReject",
|
|
222
|
+
"method": "POST",
|
|
223
|
+
"path": "/v1/proposals/{proposalId}/reject"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"operation_id": "postSourceConnections",
|
|
227
|
+
"method": "POST",
|
|
228
|
+
"path": "/v1/source-connections"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"operation_id": "postSourceConnectionsByConnectionIdDeploymentsSync",
|
|
232
|
+
"method": "POST",
|
|
233
|
+
"path": "/v1/source-connections/{connectionId}/deployments/sync"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"operation_id": "postSourceConnectionsByConnectionIdRotate",
|
|
237
|
+
"method": "POST",
|
|
238
|
+
"path": "/v1/source-connections/{connectionId}/rotate"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"operation_id": "postSourceConnectionsByConnectionIdTest",
|
|
242
|
+
"method": "POST",
|
|
243
|
+
"path": "/v1/source-connections/{connectionId}/test"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"operation_id": "postSourceCredentialHandoffs",
|
|
247
|
+
"method": "POST",
|
|
248
|
+
"path": "/v1/source-credential-handoffs"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"operation_id": "postTenants",
|
|
252
|
+
"method": "POST",
|
|
253
|
+
"path": "/v1/tenants"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"operation_id": "postVerifiers",
|
|
257
|
+
"method": "POST",
|
|
258
|
+
"path": "/v1/verifiers"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"operation_id": "postVerifiersByVerifierIdTest",
|
|
262
|
+
"method": "POST",
|
|
263
|
+
"path": "/v1/verifiers/{verifierId}/test"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"operation_id": "postWebhooks",
|
|
267
|
+
"method": "POST",
|
|
268
|
+
"path": "/v1/webhooks"
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"operation_id": "postWebhooksByWebhookIdRotateSecret",
|
|
272
|
+
"method": "POST",
|
|
273
|
+
"path": "/v1/webhooks/{webhookId}/rotate-secret"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"operation_id": "postWebhooksDeliveriesByDeliveryIdReplay",
|
|
277
|
+
"method": "POST",
|
|
278
|
+
"path": "/v1/webhooks/deliveries/{deliveryId}/replay"
|
|
279
|
+
}
|
|
280
|
+
],
|
|
281
|
+
"retry_policy": {
|
|
282
|
+
"safe_methods": [
|
|
283
|
+
"GET",
|
|
284
|
+
"HEAD"
|
|
285
|
+
],
|
|
286
|
+
"retryable_failures": [
|
|
287
|
+
"network_error",
|
|
288
|
+
"http_5xx"
|
|
289
|
+
],
|
|
290
|
+
"maximum_retries": 2
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""The sole name-neutral seam for a future PY0-selected public API.
|
|
2
|
+
|
|
3
|
+
This repository-internal module deliberately exposes no distribution, import,
|
|
4
|
+
public client-class, environment-variable, runtime-support, or lifecycle name.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Awaitable, Callable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from .catalog import _OperationCatalog
|
|
17
|
+
from .clients import _AsyncClient, _SyncClient
|
|
18
|
+
from .transport import _AsyncTransport, _SyncTransport, _TransportPolicy
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _create_sync_client(
|
|
22
|
+
*,
|
|
23
|
+
repository_root: Path | None = None,
|
|
24
|
+
snapshot_path: Path | None = None,
|
|
25
|
+
credential: str,
|
|
26
|
+
base_url: str,
|
|
27
|
+
timeout: float | httpx.Timeout,
|
|
28
|
+
maximum_retries: int | None = None,
|
|
29
|
+
backoff_seconds: float = 0.0,
|
|
30
|
+
transport: httpx.BaseTransport | None = None,
|
|
31
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
32
|
+
) -> _SyncClient:
|
|
33
|
+
if (repository_root is None) == (snapshot_path is None):
|
|
34
|
+
raise ValueError("exactly one of repository_root or snapshot_path is required")
|
|
35
|
+
if repository_root is not None:
|
|
36
|
+
catalog = _OperationCatalog.from_repository(repository_root)
|
|
37
|
+
policy = _TransportPolicy.from_repository(
|
|
38
|
+
repository_root,
|
|
39
|
+
backoff_seconds=backoff_seconds,
|
|
40
|
+
maximum_retries=maximum_retries,
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
assert snapshot_path is not None
|
|
44
|
+
catalog = _OperationCatalog.from_snapshot(snapshot_path)
|
|
45
|
+
policy = _TransportPolicy.from_snapshot(
|
|
46
|
+
snapshot_path,
|
|
47
|
+
backoff_seconds=backoff_seconds,
|
|
48
|
+
maximum_retries=maximum_retries,
|
|
49
|
+
)
|
|
50
|
+
http_client = httpx.Client(
|
|
51
|
+
base_url=base_url,
|
|
52
|
+
headers={"authorization": f"Bearer {credential}"},
|
|
53
|
+
timeout=timeout,
|
|
54
|
+
transport=transport,
|
|
55
|
+
follow_redirects=False,
|
|
56
|
+
trust_env=False,
|
|
57
|
+
)
|
|
58
|
+
return _SyncClient(
|
|
59
|
+
catalog,
|
|
60
|
+
policy,
|
|
61
|
+
_SyncTransport(http_client, policy, sleep),
|
|
62
|
+
close_transport=http_client.close,
|
|
63
|
+
sleep=sleep,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _create_async_client(
|
|
68
|
+
*,
|
|
69
|
+
repository_root: Path | None = None,
|
|
70
|
+
snapshot_path: Path | None = None,
|
|
71
|
+
credential: str,
|
|
72
|
+
base_url: str,
|
|
73
|
+
timeout: float | httpx.Timeout,
|
|
74
|
+
maximum_retries: int | None = None,
|
|
75
|
+
backoff_seconds: float = 0.0,
|
|
76
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
77
|
+
sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
|
|
78
|
+
) -> _AsyncClient:
|
|
79
|
+
if (repository_root is None) == (snapshot_path is None):
|
|
80
|
+
raise ValueError("exactly one of repository_root or snapshot_path is required")
|
|
81
|
+
if repository_root is not None:
|
|
82
|
+
catalog = _OperationCatalog.from_repository(repository_root)
|
|
83
|
+
policy = _TransportPolicy.from_repository(
|
|
84
|
+
repository_root,
|
|
85
|
+
backoff_seconds=backoff_seconds,
|
|
86
|
+
maximum_retries=maximum_retries,
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
assert snapshot_path is not None
|
|
90
|
+
catalog = _OperationCatalog.from_snapshot(snapshot_path)
|
|
91
|
+
policy = _TransportPolicy.from_snapshot(
|
|
92
|
+
snapshot_path,
|
|
93
|
+
backoff_seconds=backoff_seconds,
|
|
94
|
+
maximum_retries=maximum_retries,
|
|
95
|
+
)
|
|
96
|
+
http_client = httpx.AsyncClient(
|
|
97
|
+
base_url=base_url,
|
|
98
|
+
headers={"authorization": f"Bearer {credential}"},
|
|
99
|
+
timeout=timeout,
|
|
100
|
+
transport=transport,
|
|
101
|
+
follow_redirects=False,
|
|
102
|
+
trust_env=False,
|
|
103
|
+
)
|
|
104
|
+
return _AsyncClient(
|
|
105
|
+
catalog,
|
|
106
|
+
policy,
|
|
107
|
+
_AsyncTransport(http_client, policy, sleep),
|
|
108
|
+
close_transport=http_client.aclose,
|
|
109
|
+
sleep=sleep,
|
|
110
|
+
)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Resolve the reachable wire operations through PY1's pointers into F1."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import MappingProxyType
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
from urllib.parse import quote, unquote
|
|
12
|
+
|
|
13
|
+
from .errors import _OperationBoundaryError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_HTTP_METHODS = frozenset({"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"})
|
|
17
|
+
_PATH_PARAMETER = re.compile(r"\{([^{}]+)\}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class _Operation:
|
|
22
|
+
operation_id: str
|
|
23
|
+
method: str
|
|
24
|
+
path_template: str
|
|
25
|
+
|
|
26
|
+
def bind_path(self, path_parameters: Mapping[str, object] | None) -> str:
|
|
27
|
+
parameters = dict(path_parameters or {})
|
|
28
|
+
expected = set(_PATH_PARAMETER.findall(self.path_template))
|
|
29
|
+
if set(parameters) != expected:
|
|
30
|
+
raise _OperationBoundaryError(
|
|
31
|
+
f"{self.operation_id} path parameters must be exactly {sorted(expected)}"
|
|
32
|
+
)
|
|
33
|
+
return _PATH_PARAMETER.sub(
|
|
34
|
+
lambda match: quote(str(parameters[match.group(1)]), safe=""),
|
|
35
|
+
self.path_template,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def match_path(self, concrete_path: str) -> Mapping[str, str] | None:
|
|
39
|
+
names: list[str] = []
|
|
40
|
+
pieces: list[str] = []
|
|
41
|
+
position = 0
|
|
42
|
+
for parameter in _PATH_PARAMETER.finditer(self.path_template):
|
|
43
|
+
pieces.append(re.escape(self.path_template[position : parameter.start()]))
|
|
44
|
+
pieces.append(r"([^/]+)")
|
|
45
|
+
names.append(parameter.group(1))
|
|
46
|
+
position = parameter.end()
|
|
47
|
+
pieces.append(re.escape(self.path_template[position:]))
|
|
48
|
+
expression = "^" + "".join(pieces) + "$"
|
|
49
|
+
matched = re.match(expression, concrete_path)
|
|
50
|
+
if matched is None:
|
|
51
|
+
return None
|
|
52
|
+
return MappingProxyType({name: unquote(value) for name, value in zip(names, matched.groups())})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load_json(path: Path) -> Any:
|
|
56
|
+
with path.open("r", encoding="utf-8") as file:
|
|
57
|
+
return json.load(file)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _resolve_pointer(repository_root: Path, source: str) -> Any:
|
|
61
|
+
relative_file, separator, fragment = source.partition("#")
|
|
62
|
+
if separator != "#" or not fragment.startswith("/"):
|
|
63
|
+
raise _OperationBoundaryError(f"PY1 source is not a JSON pointer: {source}")
|
|
64
|
+
absolute_file = (repository_root / relative_file).resolve()
|
|
65
|
+
try:
|
|
66
|
+
absolute_file.relative_to(repository_root.resolve())
|
|
67
|
+
except ValueError as error:
|
|
68
|
+
raise _OperationBoundaryError("PY1 source escapes the repository root") from error
|
|
69
|
+
value = _load_json(absolute_file)
|
|
70
|
+
for encoded_token in fragment.removeprefix("/").split("/"):
|
|
71
|
+
token = encoded_token.replace("~1", "/").replace("~0", "~")
|
|
72
|
+
if isinstance(value, list):
|
|
73
|
+
value = value[int(token)]
|
|
74
|
+
else:
|
|
75
|
+
value = value[token]
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class _OperationCatalog:
|
|
80
|
+
def __init__(self, operations: Mapping[str, _Operation], event_operation_id: str) -> None:
|
|
81
|
+
self._operations = MappingProxyType(dict(operations))
|
|
82
|
+
self.event_operation_id = event_operation_id
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def from_repository(cls, repository_root: Path) -> "_OperationCatalog":
|
|
86
|
+
repository_root = repository_root.resolve()
|
|
87
|
+
view_path = repository_root / "contracts/developer-experience/python-operation-view.json"
|
|
88
|
+
view = _load_json(view_path)
|
|
89
|
+
sources = view.get("sources")
|
|
90
|
+
expected_counts = view.get("expected_counts")
|
|
91
|
+
if not isinstance(sources, dict) or expected_counts != {
|
|
92
|
+
"operations": 70,
|
|
93
|
+
"included": 54,
|
|
94
|
+
"excluded": 16,
|
|
95
|
+
}:
|
|
96
|
+
raise _OperationBoundaryError("PY1 pointer/count contract drifted")
|
|
97
|
+
|
|
98
|
+
included_ids = _resolve_pointer(repository_root, sources["included_operation_ids"])
|
|
99
|
+
classifications = _resolve_pointer(repository_root, sources["operation_classifications"])
|
|
100
|
+
wire_paths = _resolve_pointer(repository_root, sources["wire_contract"])
|
|
101
|
+
if not isinstance(included_ids, list) or not isinstance(classifications, list):
|
|
102
|
+
raise _OperationBoundaryError("PY1/F1 operation pointers must resolve to arrays")
|
|
103
|
+
|
|
104
|
+
wire_operations: dict[str, tuple[str, str]] = {}
|
|
105
|
+
for path, path_item in wire_paths.items():
|
|
106
|
+
for method, operation in path_item.items():
|
|
107
|
+
normalized_method = method.upper()
|
|
108
|
+
if normalized_method not in _HTTP_METHODS:
|
|
109
|
+
continue
|
|
110
|
+
operation_id = operation.get("operationId")
|
|
111
|
+
if not isinstance(operation_id, str) or operation_id in wire_operations:
|
|
112
|
+
raise _OperationBoundaryError("OpenAPI operation IDs must be present and unique")
|
|
113
|
+
wire_operations[operation_id] = (normalized_method, path)
|
|
114
|
+
|
|
115
|
+
classification_by_id: dict[str, Mapping[str, Any]] = {}
|
|
116
|
+
for classification in classifications:
|
|
117
|
+
operation_id = classification.get("operation_id")
|
|
118
|
+
if not isinstance(operation_id, str) or operation_id in classification_by_id:
|
|
119
|
+
raise _OperationBoundaryError("F1 operation IDs must be present and unique")
|
|
120
|
+
classification_by_id[operation_id] = classification
|
|
121
|
+
|
|
122
|
+
included_set = set(included_ids)
|
|
123
|
+
f1_included = {
|
|
124
|
+
operation_id
|
|
125
|
+
for operation_id, classification in classification_by_id.items()
|
|
126
|
+
if classification.get("disposition") == "include"
|
|
127
|
+
}
|
|
128
|
+
f1_excluded = {
|
|
129
|
+
operation_id
|
|
130
|
+
for operation_id, classification in classification_by_id.items()
|
|
131
|
+
if classification.get("disposition") == "exclude"
|
|
132
|
+
}
|
|
133
|
+
if len(wire_operations) != 70 or len(f1_included) != 54 or len(f1_excluded) != 16:
|
|
134
|
+
raise _OperationBoundaryError("F1 54/16 operation partition drifted")
|
|
135
|
+
if included_set != f1_included or len(included_ids) != len(included_set):
|
|
136
|
+
raise _OperationBoundaryError("PY1 pointer does not resolve exactly to F1's 54 includes")
|
|
137
|
+
if set(classification_by_id) != set(wire_operations):
|
|
138
|
+
raise _OperationBoundaryError("F1 classifications do not cover OpenAPI exactly")
|
|
139
|
+
|
|
140
|
+
allowed: dict[str, _Operation] = {}
|
|
141
|
+
for operation_id in included_ids:
|
|
142
|
+
classification = classification_by_id[operation_id]
|
|
143
|
+
method_and_path = wire_operations.get(operation_id)
|
|
144
|
+
if method_and_path != (classification.get("method"), classification.get("path")):
|
|
145
|
+
raise _OperationBoundaryError(f"{operation_id} F1/OpenAPI mapping drifted")
|
|
146
|
+
allowed[operation_id] = _Operation(operation_id, *method_and_path)
|
|
147
|
+
|
|
148
|
+
event_operations = [
|
|
149
|
+
operation.operation_id
|
|
150
|
+
for operation in allowed.values()
|
|
151
|
+
if operation.method == "GET" and operation.path_template.endswith("/events")
|
|
152
|
+
]
|
|
153
|
+
if len(event_operations) != 1:
|
|
154
|
+
raise _OperationBoundaryError("F1 must expose exactly one execution-events operation")
|
|
155
|
+
return cls(allowed, event_operations[0])
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def from_snapshot(cls, snapshot_path: Path) -> "_OperationCatalog":
|
|
159
|
+
"""Load the generated, F1-derived catalog bundled in an installed artifact."""
|
|
160
|
+
|
|
161
|
+
snapshot = _load_json(snapshot_path)
|
|
162
|
+
if snapshot.get("schema_version") != 1:
|
|
163
|
+
raise _OperationBoundaryError("installed Python wire snapshot version drifted")
|
|
164
|
+
operations = snapshot.get("operations")
|
|
165
|
+
if not isinstance(operations, list) or len(operations) != 54:
|
|
166
|
+
raise _OperationBoundaryError("installed Python wire snapshot must contain 54 operations")
|
|
167
|
+
|
|
168
|
+
allowed: dict[str, _Operation] = {}
|
|
169
|
+
for item in operations:
|
|
170
|
+
if not isinstance(item, dict) or set(item) != {"operation_id", "method", "path"}:
|
|
171
|
+
raise _OperationBoundaryError("installed Python wire snapshot operation shape drifted")
|
|
172
|
+
operation_id = item.get("operation_id")
|
|
173
|
+
method = item.get("method")
|
|
174
|
+
path = item.get("path")
|
|
175
|
+
if (
|
|
176
|
+
not isinstance(operation_id, str)
|
|
177
|
+
or not isinstance(method, str)
|
|
178
|
+
or method not in _HTTP_METHODS
|
|
179
|
+
or not isinstance(path, str)
|
|
180
|
+
or operation_id in allowed
|
|
181
|
+
):
|
|
182
|
+
raise _OperationBoundaryError("installed Python wire snapshot operation is invalid")
|
|
183
|
+
allowed[operation_id] = _Operation(operation_id, method, path)
|
|
184
|
+
|
|
185
|
+
event_operations = [
|
|
186
|
+
operation.operation_id
|
|
187
|
+
for operation in allowed.values()
|
|
188
|
+
if operation.method == "GET" and operation.path_template.endswith("/events")
|
|
189
|
+
]
|
|
190
|
+
if len(event_operations) != 1:
|
|
191
|
+
raise _OperationBoundaryError(
|
|
192
|
+
"installed Python wire snapshot must expose one execution-events operation"
|
|
193
|
+
)
|
|
194
|
+
return cls(allowed, event_operations[0])
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def operation_ids(self) -> frozenset[str]:
|
|
198
|
+
return frozenset(self._operations)
|
|
199
|
+
|
|
200
|
+
def operation(self, operation_id: str) -> _Operation:
|
|
201
|
+
operation = self._operations.get(operation_id)
|
|
202
|
+
if operation is None:
|
|
203
|
+
raise _OperationBoundaryError(f"operation is outside the F1 Python boundary: {operation_id}")
|
|
204
|
+
return operation
|
|
205
|
+
|
|
206
|
+
def match_fixture_request(self, method: str, concrete_path: str) -> tuple[str, Mapping[str, str]]:
|
|
207
|
+
matches: list[tuple[str, Mapping[str, str]]] = []
|
|
208
|
+
for operation in self._operations.values():
|
|
209
|
+
if operation.method != method:
|
|
210
|
+
continue
|
|
211
|
+
path_parameters = operation.match_path(concrete_path)
|
|
212
|
+
if path_parameters is not None:
|
|
213
|
+
matches.append((operation.operation_id, path_parameters))
|
|
214
|
+
if len(matches) != 1:
|
|
215
|
+
raise _OperationBoundaryError(
|
|
216
|
+
f"fixture request must match exactly one included operation: {method} {concrete_path}"
|
|
217
|
+
)
|
|
218
|
+
return matches[0]
|