seampact-codegen 1.0.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.
- seampact/codegen/__init__.py +2 -0
- seampact/codegen/boot_check_hooks.py +38 -0
- seampact/codegen/contract_module.py +146 -0
- seampact/codegen/credentials_classes.py +99 -0
- seampact/codegen/handler_forms.py +132 -0
- seampact/codegen/input_schemas.py +130 -0
- seampact/codegen/kept_forms.py +522 -0
- seampact/codegen/matrix_row.py +23 -0
- seampact/codegen/names.py +146 -0
- seampact/codegen/native_docs_file.py +169 -0
- seampact/codegen/operation_types.py +425 -0
- seampact/codegen/placement_lines.py +79 -0
- seampact/codegen/printer.py +196 -0
- seampact/codegen/py.typed +0 -0
- seampact/codegen/pydantic_forms.py +53 -0
- seampact/codegen/pydantic_layout.py +178 -0
- seampact/codegen/pydantic_models.py +405 -0
- seampact/codegen/redacted_members.py +47 -0
- seampact/codegen/refusals.py +84 -0
- seampact/codegen/request_deadline.py +68 -0
- seampact/codegen/response_header_forms.py +73 -0
- seampact/codegen/restricted_sort_keys.py +18 -0
- seampact/codegen/route_helpers.py +109 -0
- seampact/codegen/routes.py +139 -0
- seampact/codegen/runtime/__init__.py +7 -0
- seampact/codegen/runtime/access_kinds.py +24 -0
- seampact/codegen/runtime/access_steps.py +520 -0
- seampact/codegen/runtime/body_reader.py +64 -0
- seampact/codegen/runtime/cache_headers.py +89 -0
- seampact/codegen/runtime/contract.py +232 -0
- seampact/codegen/runtime/cors_preflight.py +20 -0
- seampact/codegen/runtime/deadline.py +72 -0
- seampact/codegen/runtime/digests.py +98 -0
- seampact/codegen/runtime/failures.py +33 -0
- seampact/codegen/runtime/field_rights.py +193 -0
- seampact/codegen/runtime/framework_routes.py +36 -0
- seampact/codegen/runtime/framework_versions.py +91 -0
- seampact/codegen/runtime/handler_call.py +31 -0
- seampact/codegen/runtime/handler_reply.py +148 -0
- seampact/codegen/runtime/hinted_values.py +48 -0
- seampact/codegen/runtime/hook_check.py +77 -0
- seampact/codegen/runtime/idempotency.py +279 -0
- seampact/codegen/runtime/input_decoding.py +226 -0
- seampact/codegen/runtime/ir_view.py +47 -0
- seampact/codegen/runtime/native_docs.py +130 -0
- seampact/codegen/runtime/page_facts.py +33 -0
- seampact/codegen/runtime/page_reply.py +396 -0
- seampact/codegen/runtime/pattern.py +69 -0
- seampact/codegen/runtime/principal_members.py +41 -0
- seampact/codegen/runtime/public_origin.py +102 -0
- seampact/codegen/runtime/pydantic/__init__.py +165 -0
- seampact/codegen/runtime/pydantic/checked_input.py +52 -0
- seampact/codegen/runtime/pydantic/contract_input.py +183 -0
- seampact/codegen/runtime/pydantic/issues.py +142 -0
- seampact/codegen/runtime/raw_target.py +103 -0
- seampact/codegen/runtime/replies.py +144 -0
- seampact/codegen/runtime/reply_verdict.py +215 -0
- seampact/codegen/runtime/requirement_params.py +170 -0
- seampact/codegen/runtime/response_header_entries.py +226 -0
- seampact/codegen/runtime/response_headers.py +114 -0
- seampact/codegen/runtime/route_match.py +88 -0
- seampact/codegen/runtime/run_settings.py +35 -0
- seampact/codegen/runtime/signature_header.py +92 -0
- seampact/codegen/runtime/sync_driver.py +35 -0
- seampact/codegen/runtime/templates.py +132 -0
- seampact/codegen/runtime/uuid_text.py +7 -0
- seampact/codegen/runtime/value_rules.py +218 -0
- seampact/codegen/runtime/version.py +59 -0
- seampact/codegen/runtime/version_token.py +108 -0
- seampact/codegen/runtime/wire_codec.py +229 -0
- seampact/codegen/runtime/wire_values.py +260 -0
- seampact/codegen/source_lines.py +35 -0
- seampact_codegen-1.0.0.dist-info/METADATA +18 -0
- seampact_codegen-1.0.0.dist-info/RECORD +76 -0
- seampact_codegen-1.0.0.dist-info/WHEEL +4 -0
- seampact_codegen-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""The `operations` module of an api's package (ADR 0026 "Placement"): the api's contract exception base and one
|
|
2
|
+
exception per error code; the credentials class of each identity, one `str | None` per scheme, hidden from `repr`; per
|
|
3
|
+
operation its scopes, its request (the typed input and who is calling), one frozen class per declared success, its
|
|
4
|
+
page, its typed errors and its result union; and the endpoints protocol of each resource, whose methods a plain `def`
|
|
5
|
+
or an `async def` satisfies."""
|
|
6
|
+
|
|
7
|
+
from typing import Final
|
|
8
|
+
|
|
9
|
+
from seampact.codegen.credentials_classes import credentials_class
|
|
10
|
+
from seampact.codegen.input_schemas import INPUT_PARTS, input_schema_id
|
|
11
|
+
from seampact.codegen.names import ApiNames, member_identifier
|
|
12
|
+
from seampact.codegen.printer import INDENT, PythonModule, bracketed, string_literal
|
|
13
|
+
from seampact.codegen.pydantic_layout import ApiLayout, OperationNames
|
|
14
|
+
from seampact.codegen.redacted_members import redacted_dataclass_reprs, redacted_marker_block
|
|
15
|
+
from seampact.codegen.refusals import Delivery, IrPlace, Refusals
|
|
16
|
+
from seampact.codegen.request_deadline import deadline_property
|
|
17
|
+
from seampact.codegen.runtime.page_facts import PAGE_FACTS
|
|
18
|
+
from seampact.codegen.source_lines import with_local_imports
|
|
19
|
+
from seampact.ir.model import (
|
|
20
|
+
ContractIR,
|
|
21
|
+
CredentialIdentity,
|
|
22
|
+
HandlerResultMember,
|
|
23
|
+
IdentityAccess,
|
|
24
|
+
Operation,
|
|
25
|
+
OptionalAccess,
|
|
26
|
+
RefTypeUse,
|
|
27
|
+
ResponseShape,
|
|
28
|
+
SchemaPrincipal,
|
|
29
|
+
SignedIdentity,
|
|
30
|
+
TypeUse,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_FROZEN: Final = "@_dataclasses.dataclass(frozen=True, slots=True)"
|
|
34
|
+
_FROZEN_KEYWORDS: Final = "@_dataclasses.dataclass(frozen=True, slots=True, kw_only=True)"
|
|
35
|
+
# The fields of an `<Op>Errors` class: an error factory's name never starts with `_` and a letter.
|
|
36
|
+
_ERRORS_CODE_FIELD: Final = "_code"
|
|
37
|
+
_ERRORS_PARAMS_FIELD: Final = "_params"
|
|
38
|
+
# The members of an `<Op>Request` and the body member of an `<Op>Input`.
|
|
39
|
+
_INPUT: Final = "input"
|
|
40
|
+
_PRINCIPAL: Final = "principal"
|
|
41
|
+
_SCOPES: Final = "scopes"
|
|
42
|
+
_BODY: Final = "body"
|
|
43
|
+
_PAGE_ITEMS: Final = "items"
|
|
44
|
+
_ENDPOINTS_DOC: Final = "The handlers of these operations: each returns a declared success or error, or raises one."
|
|
45
|
+
_PAGE_FACT_TYPES: Final = {
|
|
46
|
+
HandlerResultMember.NEXT_CURSOR: "str",
|
|
47
|
+
HandlerResultMember.PREVIOUS_CURSOR: "str",
|
|
48
|
+
HandlerResultMember.NEXT_SINCE: "str",
|
|
49
|
+
HandlerResultMember.TOTAL: "int",
|
|
50
|
+
HandlerResultMember.MORE: "bool",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _Emission:
|
|
55
|
+
def __init__(self, ir: ContractIR, names: ApiNames, layout: ApiLayout, refusals: Refusals) -> None:
|
|
56
|
+
self.ir = ir
|
|
57
|
+
self.names = names
|
|
58
|
+
self.layout = layout
|
|
59
|
+
self.refusals = refusals
|
|
60
|
+
self.module = PythonModule(f"The operation types of api '{ir.api.id}'.")
|
|
61
|
+
self.blocks: list[list[str]] = []
|
|
62
|
+
self._redacted_declared = False
|
|
63
|
+
|
|
64
|
+
def text(self) -> str:
|
|
65
|
+
self.module.imported("dataclasses", "_dataclasses")
|
|
66
|
+
self.module.imported("typing", "_typing")
|
|
67
|
+
self.module.imported("pydantic", "_pydantic")
|
|
68
|
+
self._errors()
|
|
69
|
+
self._credentials()
|
|
70
|
+
|
|
71
|
+
for index, operation in enumerate(self.ir.operations):
|
|
72
|
+
names = self.layout.operations.get(operation.id)
|
|
73
|
+
|
|
74
|
+
if names is not None:
|
|
75
|
+
self._operation(operation, names, IrPlace(f"/operations/{index}", operation.id))
|
|
76
|
+
|
|
77
|
+
self._endpoints()
|
|
78
|
+
|
|
79
|
+
first, *rest = self.blocks
|
|
80
|
+
self.module.add_block(with_local_imports(["schemas"], [], first))
|
|
81
|
+
|
|
82
|
+
for block in rest:
|
|
83
|
+
self.module.add_block(block)
|
|
84
|
+
|
|
85
|
+
return self.module.text()
|
|
86
|
+
|
|
87
|
+
def _errors(self) -> None:
|
|
88
|
+
base = self.layout.contract_error
|
|
89
|
+
api = self.ir.api.id
|
|
90
|
+
self.blocks.append(
|
|
91
|
+
[
|
|
92
|
+
f"class {base}(Exception):",
|
|
93
|
+
f'{INDENT}"""A declared error of api {api!r}, raised by a handler."""',
|
|
94
|
+
"",
|
|
95
|
+
f"{INDENT}CODE: _typing.ClassVar[str]",
|
|
96
|
+
f"{INDENT}STATUS: _typing.ClassVar[int]",
|
|
97
|
+
"",
|
|
98
|
+
f"{INDENT}def __init__(self, params: _pydantic.BaseModel | None = None) -> None:",
|
|
99
|
+
f'{INDENT * 2}super().__init__(f"The declared error {{self.CODE!r}} of api {api!r}.")',
|
|
100
|
+
f"{INDENT * 2}self.params = params",
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
for code, class_name in self.layout.error_classes.items():
|
|
105
|
+
error = self.ir.errors.get(code)
|
|
106
|
+
|
|
107
|
+
if error is None:
|
|
108
|
+
self.refusals.inconsistent(
|
|
109
|
+
"error.undeclared", f"The error '{code}' is not declared", IrPlace("/errors")
|
|
110
|
+
)
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
lines = [
|
|
114
|
+
f"class {class_name}({base}):",
|
|
115
|
+
f'{INDENT}"""The error {code!r}."""',
|
|
116
|
+
"",
|
|
117
|
+
f"{INDENT}CODE = {string_literal(code)}",
|
|
118
|
+
f"{INDENT}STATUS = {error.http.status}",
|
|
119
|
+
]
|
|
120
|
+
params = self._params_class(code)
|
|
121
|
+
|
|
122
|
+
if params is not None:
|
|
123
|
+
lines += [
|
|
124
|
+
"",
|
|
125
|
+
f"{INDENT}def __init__(self, params: {params}) -> None:",
|
|
126
|
+
f"{INDENT * 2}super().__init__(params)",
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
self.blocks.append(lines)
|
|
130
|
+
|
|
131
|
+
def _params_class(self, code: str) -> str | None:
|
|
132
|
+
error = self.ir.errors.get(code)
|
|
133
|
+
|
|
134
|
+
if error is None or error.params is None:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
name = self.names.schema_classes.get(error.params)
|
|
138
|
+
|
|
139
|
+
if name is None:
|
|
140
|
+
self.refusals.inconsistent(
|
|
141
|
+
"error.params.missing",
|
|
142
|
+
f"The params schema '{error.params}' of error '{code}' is not an object",
|
|
143
|
+
IrPlace(f"/errors/{code}/params"),
|
|
144
|
+
)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
return f"_schemas.{name}"
|
|
148
|
+
|
|
149
|
+
def _operation(self, operation: Operation, names: OperationNames, at: IrPlace) -> None:
|
|
150
|
+
if names.scopes is not None:
|
|
151
|
+
self._scopes(operation, names.scopes)
|
|
152
|
+
|
|
153
|
+
self._input(operation, names, at)
|
|
154
|
+
self._request(operation, names, at)
|
|
155
|
+
|
|
156
|
+
if names.page is not None:
|
|
157
|
+
self._page(operation, names.page, at)
|
|
158
|
+
|
|
159
|
+
for index, response in enumerate(operation.responses):
|
|
160
|
+
self._response(operation, names, response.status, index, at)
|
|
161
|
+
|
|
162
|
+
self._typed_errors(operation, names)
|
|
163
|
+
alternatives = [*names.responses.values(), names.errors]
|
|
164
|
+
self.blocks.append([f"type {names.result} = {' | '.join(alternatives)}"])
|
|
165
|
+
|
|
166
|
+
def _credentials(self) -> None:
|
|
167
|
+
for identity, credentials in self.layout.credentials.items():
|
|
168
|
+
self.blocks.append(credentials_class(_FROZEN_KEYWORDS, identity, credentials))
|
|
169
|
+
|
|
170
|
+
def _scopes(self, operation: Operation, class_name: str) -> None:
|
|
171
|
+
hooks = self.module.imported("seampact.hooks", "_hooks")
|
|
172
|
+
lines = [
|
|
173
|
+
_FROZEN_KEYWORDS,
|
|
174
|
+
f"class {class_name}:",
|
|
175
|
+
f'{INDENT}"""The scopes operation {operation.id!r} resolved, one per scope."""',
|
|
176
|
+
"",
|
|
177
|
+
]
|
|
178
|
+
lines += [f"{INDENT}{member_identifier(scope.scope)}: {hooks}.ResolvedScope" for scope in operation.scopes]
|
|
179
|
+
self.blocks.append(lines)
|
|
180
|
+
|
|
181
|
+
def _input(self, operation: Operation, names: OperationNames, at: IrPlace) -> None:
|
|
182
|
+
lines = [
|
|
183
|
+
_FROZEN_KEYWORDS,
|
|
184
|
+
f"class {names.input}:",
|
|
185
|
+
f'{INDENT}"""The input of operation {operation.id!r}, as its handler reads it."""',
|
|
186
|
+
]
|
|
187
|
+
annotations = {
|
|
188
|
+
part: f"_schemas.{self.names.schema_classes[input_schema_id(operation.id, part)]}"
|
|
189
|
+
for part, _ in INPUT_PARTS
|
|
190
|
+
if input_schema_id(operation.id, part) in self.names.schema_classes
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if operation.input is not None:
|
|
194
|
+
body_at = at.below("input", "schema")
|
|
195
|
+
annotations[_BODY] = self._type_of(
|
|
196
|
+
operation.input.schema, body_at, "A request body that is not an object schema"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if not annotations:
|
|
200
|
+
self.blocks.append(lines)
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
lines += ["", *(f"{INDENT}{part}: {annotation}" for part, annotation in annotations.items())]
|
|
204
|
+
|
|
205
|
+
if _BODY in annotations and self._is_signed(operation):
|
|
206
|
+
lines += self._redacted_reprs(names.input, list(annotations), {_BODY})
|
|
207
|
+
|
|
208
|
+
self.blocks.append(lines)
|
|
209
|
+
|
|
210
|
+
def _request(self, operation: Operation, names: OperationNames, at: IrPlace) -> None:
|
|
211
|
+
lines = [
|
|
212
|
+
_FROZEN_KEYWORDS,
|
|
213
|
+
f"class {names.request}:",
|
|
214
|
+
f'{INDENT}"""What the handler of operation {operation.id!r} reads: its input and who is calling."""',
|
|
215
|
+
"",
|
|
216
|
+
f"{INDENT}{_INPUT}: {names.input}",
|
|
217
|
+
]
|
|
218
|
+
principal = self._principal(operation, at)
|
|
219
|
+
|
|
220
|
+
if principal is not None:
|
|
221
|
+
lines.append(f"{INDENT}{_PRINCIPAL}: {principal}")
|
|
222
|
+
|
|
223
|
+
if names.scopes is not None:
|
|
224
|
+
lines.append(f"{INDENT}{_SCOPES}: {names.scopes}")
|
|
225
|
+
|
|
226
|
+
attributes = [_INPUT, *([_PRINCIPAL] if principal is not None else []), *([_SCOPES] if names.scopes else [])]
|
|
227
|
+
|
|
228
|
+
lines += deadline_property(self.module, operation, at, self.refusals)
|
|
229
|
+
|
|
230
|
+
if len(attributes) > 1:
|
|
231
|
+
lines += self._redacted_reprs(names.request, attributes, {_PRINCIPAL, _SCOPES})
|
|
232
|
+
|
|
233
|
+
self.blocks.append(lines)
|
|
234
|
+
|
|
235
|
+
def _is_signed(self, operation: Operation) -> bool:
|
|
236
|
+
access = operation.access
|
|
237
|
+
|
|
238
|
+
return isinstance(access, IdentityAccess | OptionalAccess) and isinstance(
|
|
239
|
+
self.ir.identities.get(access.identity), SignedIdentity
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def _redacted_reprs(self, class_name: str, attributes: list[str], redacted: set[str]) -> list[str]:
|
|
243
|
+
"""Who is calling and a signed body are printed `[redacted]` (ADR 0023 "Redaction")."""
|
|
244
|
+
if not self._redacted_declared:
|
|
245
|
+
self._redacted_declared = True
|
|
246
|
+
self.blocks.append(redacted_marker_block())
|
|
247
|
+
|
|
248
|
+
abc = self.module.imported("collections.abc", "_abc")
|
|
249
|
+
|
|
250
|
+
return redacted_dataclass_reprs(class_name, attributes, redacted, abc)
|
|
251
|
+
|
|
252
|
+
def _principal(self, operation: Operation, at: IrPlace) -> str | None:
|
|
253
|
+
access = operation.access
|
|
254
|
+
|
|
255
|
+
if not isinstance(access, IdentityAccess | OptionalAccess):
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
identity = self.ir.identities.get(access.identity)
|
|
259
|
+
principal = identity.principal if isinstance(identity, CredentialIdentity | SignedIdentity) else None
|
|
260
|
+
|
|
261
|
+
if not isinstance(principal, SchemaPrincipal):
|
|
262
|
+
self.refusals.waiting(
|
|
263
|
+
"identity.principal", "A principal that is not one schema", at.below("access"), Delivery.CONSUMER
|
|
264
|
+
)
|
|
265
|
+
return "object"
|
|
266
|
+
|
|
267
|
+
name = self.names.schema_classes.get(principal.schema)
|
|
268
|
+
|
|
269
|
+
if name is None:
|
|
270
|
+
self.refusals.inconsistent("principal.schema.missing", f"No object schema '{principal.schema}'", at)
|
|
271
|
+
return "object"
|
|
272
|
+
|
|
273
|
+
return f"_schemas.{name}" if isinstance(access, IdentityAccess) else f"_schemas.{name} | None"
|
|
274
|
+
|
|
275
|
+
def _response(self, operation: Operation, names: OperationNames, status: int, index: int, at: IrPlace) -> None:
|
|
276
|
+
response = operation.responses[index]
|
|
277
|
+
lines = [
|
|
278
|
+
_FROZEN,
|
|
279
|
+
f"class {names.responses[status]}:",
|
|
280
|
+
f'{INDENT}"""The {status} reply of operation {operation.id!r}."""',
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
if response.body is None:
|
|
284
|
+
if response.shape is not ResponseShape.EMPTY:
|
|
285
|
+
self.refusals.inconsistent("response.body.missing", "A response with a shape has no body", at)
|
|
286
|
+
|
|
287
|
+
self.blocks.append(lines)
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
body = self._type_of(response.body, at.below("responses", index, "body"))
|
|
291
|
+
|
|
292
|
+
if response.shape is ResponseShape.TABLE:
|
|
293
|
+
body = f"dict[str, {body}]"
|
|
294
|
+
elif response.shape is ResponseShape.LIST:
|
|
295
|
+
body = f"list[{body}]"
|
|
296
|
+
elif response.shape is ResponseShape.PAGE and names.page is not None:
|
|
297
|
+
body = names.page
|
|
298
|
+
|
|
299
|
+
self.blocks.append([*lines, "", f"{INDENT}body: {body}"])
|
|
300
|
+
|
|
301
|
+
def _type_of(
|
|
302
|
+
self, type_use: TypeUse, at: IrPlace, description: str = "A response body that is not an object schema"
|
|
303
|
+
) -> str:
|
|
304
|
+
name = self.names.schema_classes.get(type_use.schema) if isinstance(type_use, RefTypeUse) else None
|
|
305
|
+
|
|
306
|
+
if name is None:
|
|
307
|
+
self.refusals.waiting("body.kind", description, at, Delivery.CONSUMER)
|
|
308
|
+
return "object"
|
|
309
|
+
|
|
310
|
+
return f"_schemas.{name}"
|
|
311
|
+
|
|
312
|
+
def _page(self, operation: Operation, class_name: str, at: IrPlace) -> None:
|
|
313
|
+
page = next(response for response in operation.responses if response.shape is ResponseShape.PAGE)
|
|
314
|
+
item = "object" if page.body is None else self._type_of(page.body, at.below("responses"))
|
|
315
|
+
lines = [
|
|
316
|
+
_FROZEN_KEYWORDS,
|
|
317
|
+
f"class {class_name}:",
|
|
318
|
+
f'{INDENT}"""The page of operation {operation.id!r}: its items and the facts its paging style reads."""',
|
|
319
|
+
"",
|
|
320
|
+
f"{INDENT}{_PAGE_ITEMS}: list[{item}]",
|
|
321
|
+
]
|
|
322
|
+
style = self.ir.pagings.get(operation.paging.style or "") if operation.paging is not None else None
|
|
323
|
+
facts = {_PAGE_ITEMS: _PAGE_ITEMS}
|
|
324
|
+
|
|
325
|
+
if style is None:
|
|
326
|
+
self.refusals.inconsistent(
|
|
327
|
+
"paging.style.undeclared", "The paging style is not declared", at.below("paging")
|
|
328
|
+
)
|
|
329
|
+
else:
|
|
330
|
+
for member in style.handler_result:
|
|
331
|
+
lines += self._page_fact(member, facts, at.below("paging", "style"))
|
|
332
|
+
|
|
333
|
+
table = ", ".join(f"{string_literal(attribute)}: {string_literal(fact)}" for attribute, fact in facts.items())
|
|
334
|
+
lines += ["", f"{INDENT}{PAGE_FACTS}: _typing.ClassVar[dict[str, str]] = {{{table}}}"]
|
|
335
|
+
self.blocks.append(lines)
|
|
336
|
+
|
|
337
|
+
def _page_fact(self, member: object, facts: dict[str, str], at: IrPlace) -> list[str]:
|
|
338
|
+
"""The attribute of one page fact, entered in the page's fact table; a fact without an attribute name of its
|
|
339
|
+
own is refused by name."""
|
|
340
|
+
fact_type = _PAGE_FACT_TYPES.get(member) if isinstance(member, HandlerResultMember) else None
|
|
341
|
+
|
|
342
|
+
if fact_type is None:
|
|
343
|
+
self.refusals.waiting("paging.handlerResult", f"The page fact '{member}'", at, Delivery.LATER_THAN_V1)
|
|
344
|
+
return []
|
|
345
|
+
|
|
346
|
+
attribute = member_identifier(str(member))
|
|
347
|
+
|
|
348
|
+
if attribute is None or attribute in facts:
|
|
349
|
+
self.refusals.inconsistent(
|
|
350
|
+
"paging.handlerResult.attribute",
|
|
351
|
+
f"The page fact '{member}' has no attribute name of its own on the page class",
|
|
352
|
+
at,
|
|
353
|
+
)
|
|
354
|
+
return []
|
|
355
|
+
|
|
356
|
+
facts[attribute] = str(member)
|
|
357
|
+
|
|
358
|
+
return [f"{INDENT}{attribute}: {fact_type} | None = None"]
|
|
359
|
+
|
|
360
|
+
def _typed_errors(self, operation: Operation, names: OperationNames) -> None:
|
|
361
|
+
lines = [
|
|
362
|
+
_FROZEN_KEYWORDS,
|
|
363
|
+
f"class {names.errors}:",
|
|
364
|
+
f'{INDENT}"""A declared error of operation {operation.id!r}, returned by its handler instead of raised."""',
|
|
365
|
+
"",
|
|
366
|
+
f"{INDENT}{_ERRORS_CODE_FIELD}: str",
|
|
367
|
+
f"{INDENT}{_ERRORS_PARAMS_FIELD}: _pydantic.BaseModel | None",
|
|
368
|
+
]
|
|
369
|
+
listed: set[str] = set()
|
|
370
|
+
|
|
371
|
+
for error in operation.errors:
|
|
372
|
+
factory = self.layout.error_factories.get(error.code)
|
|
373
|
+
|
|
374
|
+
if factory is None or error.code in listed:
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
listed.add(error.code)
|
|
378
|
+
params = self._params_class(error.code)
|
|
379
|
+
arguments = ["cls"] if params is None else ["cls", f"params: {params}"]
|
|
380
|
+
lines += [
|
|
381
|
+
"",
|
|
382
|
+
f"{INDENT}@classmethod",
|
|
383
|
+
*bracketed(INDENT, f"def {factory}", arguments, " -> _typing.Self:"),
|
|
384
|
+
*bracketed(
|
|
385
|
+
INDENT * 2,
|
|
386
|
+
"return cls",
|
|
387
|
+
[
|
|
388
|
+
f"{_ERRORS_CODE_FIELD}={string_literal(error.code)}",
|
|
389
|
+
f"{_ERRORS_PARAMS_FIELD}={'None' if params is None else 'params'}",
|
|
390
|
+
],
|
|
391
|
+
"",
|
|
392
|
+
),
|
|
393
|
+
]
|
|
394
|
+
|
|
395
|
+
self.blocks.append(lines)
|
|
396
|
+
|
|
397
|
+
def _endpoints(self) -> None:
|
|
398
|
+
protocols: dict[str, list[str]] = {}
|
|
399
|
+
|
|
400
|
+
for operation in self.ir.operations:
|
|
401
|
+
names = self.layout.operations.get(operation.id)
|
|
402
|
+
|
|
403
|
+
if names is None:
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
abc = self.module.imported("collections.abc", "_abc")
|
|
407
|
+
lines = protocols.setdefault(
|
|
408
|
+
names.endpoints,
|
|
409
|
+
[
|
|
410
|
+
f"class {names.endpoints}(_typing.Protocol):",
|
|
411
|
+
f'{INDENT}"""{_ENDPOINTS_DOC}"""',
|
|
412
|
+
"",
|
|
413
|
+
],
|
|
414
|
+
)
|
|
415
|
+
awaitable = f"{names.result} | {abc}.Awaitable[{names.result}]"
|
|
416
|
+
lines += bracketed(
|
|
417
|
+
INDENT, f"def {names.method}", ["self", f"request: {names.request}", "/"], f" -> {awaitable}: ..."
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
self.blocks += protocols.values()
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def operation_types_module(ir: ContractIR, names: ApiNames, layout: ApiLayout, refusals: Refusals) -> str:
|
|
424
|
+
"""The `operations` module of the api's package; `ir` holds the input schemas, `names` their classes."""
|
|
425
|
+
return _Emission(ir, names, layout, refusals).text()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""The lines every Pydantic target's placement module writes alike: the credentials types of its placement, the
|
|
2
|
+
api's unexpected-error listener protocol, a tuple of strings, and each operation's `add_operation` call with the
|
|
3
|
+
generated types it hands the runtime."""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
from seampact.codegen.printer import INDENT, bracketed, string_literal
|
|
9
|
+
from seampact.codegen.pydantic_layout import ApiLayout
|
|
10
|
+
|
|
11
|
+
_LISTENER_DOC: Final = "Hears of each failure api '{api}' answers with its 500."
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def tuple_literal(items: Sequence[str]) -> str:
|
|
15
|
+
texts = [string_literal(item) for item in items]
|
|
16
|
+
|
|
17
|
+
return f"({texts[0]},)" if len(texts) == 1 else f"({', '.join(texts)})"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def credentials_types(runtime: str, layout: ApiLayout) -> list[str]:
|
|
21
|
+
"""The placement's `credentials=` argument: each identity's credentials class and its attribute per scheme."""
|
|
22
|
+
if not layout.credentials:
|
|
23
|
+
return [f"{INDENT}credentials={{}},"]
|
|
24
|
+
|
|
25
|
+
lines = [f"{INDENT}credentials={{"]
|
|
26
|
+
|
|
27
|
+
for identity, credentials in layout.credentials.items():
|
|
28
|
+
attributes = ", ".join(
|
|
29
|
+
f"{string_literal(scheme)}: {string_literal(attribute)}"
|
|
30
|
+
for scheme, attribute in credentials.attributes.items()
|
|
31
|
+
)
|
|
32
|
+
lines += bracketed(
|
|
33
|
+
INDENT * 2,
|
|
34
|
+
f"{string_literal(identity)}: {runtime}.CredentialsType",
|
|
35
|
+
[credentials.class_name, "{" + attributes + "}"],
|
|
36
|
+
",",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return [*lines, f"{INDENT}}},"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def unexpected_error_listener(api: str, hooks: str, layout: ApiLayout) -> list[str]:
|
|
43
|
+
return [
|
|
44
|
+
f"class {layout.unexpected_error_listener}({hooks}.UnexpectedErrorListener, _typing.Protocol):",
|
|
45
|
+
f'{INDENT}"""{_LISTENER_DOC.format(api=api)}"""',
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def operation_types_arguments(layout: ApiLayout, operation_id: str, proofs_model: str | None) -> list[str]:
|
|
50
|
+
"""The arguments of one operation's `OperationTypes`; `proofs_model` is the qualified `<Op>Proofs` class."""
|
|
51
|
+
names = layout.operations[operation_id]
|
|
52
|
+
responses = ", ".join(f"({status}, {name})" for status, name in sorted(names.responses.items()))
|
|
53
|
+
arguments = [
|
|
54
|
+
f"request={names.request}",
|
|
55
|
+
f"responses=({responses}{',' if len(names.responses) == 1 else ''})",
|
|
56
|
+
f"errors={names.errors}",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
if proofs_model is not None:
|
|
60
|
+
arguments.append(f"proofs={proofs_model}")
|
|
61
|
+
|
|
62
|
+
return arguments
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def add_operation_call(runtime: str, arguments: Sequence[str], types: Sequence[str]) -> list[str]:
|
|
66
|
+
"""One `add_operation` call in `bind_<api>()`, split as ruff splits it: on one line when it fits, else one argument
|
|
67
|
+
per line, the operation's `OperationTypes` a nested call split the same way."""
|
|
68
|
+
head = f"{runtime}.add_operation"
|
|
69
|
+
single = bracketed(INDENT, head, [*arguments, f"{runtime}.OperationTypes({', '.join(types)})"], "")
|
|
70
|
+
|
|
71
|
+
if len(single) == 1:
|
|
72
|
+
return single
|
|
73
|
+
|
|
74
|
+
return [
|
|
75
|
+
f"{INDENT}{head}(",
|
|
76
|
+
*(f"{INDENT * 2}{argument}," for argument in arguments),
|
|
77
|
+
*bracketed(INDENT * 2, f"{runtime}.OperationTypes", types, ","),
|
|
78
|
+
f"{INDENT})",
|
|
79
|
+
]
|