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.
Files changed (76) hide show
  1. seampact/codegen/__init__.py +2 -0
  2. seampact/codegen/boot_check_hooks.py +38 -0
  3. seampact/codegen/contract_module.py +146 -0
  4. seampact/codegen/credentials_classes.py +99 -0
  5. seampact/codegen/handler_forms.py +132 -0
  6. seampact/codegen/input_schemas.py +130 -0
  7. seampact/codegen/kept_forms.py +522 -0
  8. seampact/codegen/matrix_row.py +23 -0
  9. seampact/codegen/names.py +146 -0
  10. seampact/codegen/native_docs_file.py +169 -0
  11. seampact/codegen/operation_types.py +425 -0
  12. seampact/codegen/placement_lines.py +79 -0
  13. seampact/codegen/printer.py +196 -0
  14. seampact/codegen/py.typed +0 -0
  15. seampact/codegen/pydantic_forms.py +53 -0
  16. seampact/codegen/pydantic_layout.py +178 -0
  17. seampact/codegen/pydantic_models.py +405 -0
  18. seampact/codegen/redacted_members.py +47 -0
  19. seampact/codegen/refusals.py +84 -0
  20. seampact/codegen/request_deadline.py +68 -0
  21. seampact/codegen/response_header_forms.py +73 -0
  22. seampact/codegen/restricted_sort_keys.py +18 -0
  23. seampact/codegen/route_helpers.py +109 -0
  24. seampact/codegen/routes.py +139 -0
  25. seampact/codegen/runtime/__init__.py +7 -0
  26. seampact/codegen/runtime/access_kinds.py +24 -0
  27. seampact/codegen/runtime/access_steps.py +520 -0
  28. seampact/codegen/runtime/body_reader.py +64 -0
  29. seampact/codegen/runtime/cache_headers.py +89 -0
  30. seampact/codegen/runtime/contract.py +232 -0
  31. seampact/codegen/runtime/cors_preflight.py +20 -0
  32. seampact/codegen/runtime/deadline.py +72 -0
  33. seampact/codegen/runtime/digests.py +98 -0
  34. seampact/codegen/runtime/failures.py +33 -0
  35. seampact/codegen/runtime/field_rights.py +193 -0
  36. seampact/codegen/runtime/framework_routes.py +36 -0
  37. seampact/codegen/runtime/framework_versions.py +91 -0
  38. seampact/codegen/runtime/handler_call.py +31 -0
  39. seampact/codegen/runtime/handler_reply.py +148 -0
  40. seampact/codegen/runtime/hinted_values.py +48 -0
  41. seampact/codegen/runtime/hook_check.py +77 -0
  42. seampact/codegen/runtime/idempotency.py +279 -0
  43. seampact/codegen/runtime/input_decoding.py +226 -0
  44. seampact/codegen/runtime/ir_view.py +47 -0
  45. seampact/codegen/runtime/native_docs.py +130 -0
  46. seampact/codegen/runtime/page_facts.py +33 -0
  47. seampact/codegen/runtime/page_reply.py +396 -0
  48. seampact/codegen/runtime/pattern.py +69 -0
  49. seampact/codegen/runtime/principal_members.py +41 -0
  50. seampact/codegen/runtime/public_origin.py +102 -0
  51. seampact/codegen/runtime/pydantic/__init__.py +165 -0
  52. seampact/codegen/runtime/pydantic/checked_input.py +52 -0
  53. seampact/codegen/runtime/pydantic/contract_input.py +183 -0
  54. seampact/codegen/runtime/pydantic/issues.py +142 -0
  55. seampact/codegen/runtime/raw_target.py +103 -0
  56. seampact/codegen/runtime/replies.py +144 -0
  57. seampact/codegen/runtime/reply_verdict.py +215 -0
  58. seampact/codegen/runtime/requirement_params.py +170 -0
  59. seampact/codegen/runtime/response_header_entries.py +226 -0
  60. seampact/codegen/runtime/response_headers.py +114 -0
  61. seampact/codegen/runtime/route_match.py +88 -0
  62. seampact/codegen/runtime/run_settings.py +35 -0
  63. seampact/codegen/runtime/signature_header.py +92 -0
  64. seampact/codegen/runtime/sync_driver.py +35 -0
  65. seampact/codegen/runtime/templates.py +132 -0
  66. seampact/codegen/runtime/uuid_text.py +7 -0
  67. seampact/codegen/runtime/value_rules.py +218 -0
  68. seampact/codegen/runtime/version.py +59 -0
  69. seampact/codegen/runtime/version_token.py +108 -0
  70. seampact/codegen/runtime/wire_codec.py +229 -0
  71. seampact/codegen/runtime/wire_values.py +260 -0
  72. seampact/codegen/source_lines.py +35 -0
  73. seampact_codegen-1.0.0.dist-info/METADATA +18 -0
  74. seampact_codegen-1.0.0.dist-info/RECORD +76 -0
  75. seampact_codegen-1.0.0.dist-info/WHEEL +4 -0
  76. 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
+ ]