seampact-fastapi 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.
@@ -0,0 +1,2 @@
1
+ """The FastAPI target of seampact (ADR 0026): the generator `seampact generate` finds through the `seampact.generators`
2
+ entry point, behind the `generate` extra, and the placement runtime the generated code imports."""
@@ -0,0 +1,85 @@
1
+ """The fastapi target (ADR 0026), which `seampact generate` finds through the `seampact.generators` entry point of
2
+ `seampact-fastapi`. Per api it writes one package: `schemas.py` (the schema models and each operation's input model,
3
+ through the shared Pydantic emitter), `operations.py` (the operation types), `contract.py` (the contract the runtime
4
+ reads, shared with every Python target), `routes.py` (the route names and URL
5
+ builders, unless `routeHelpers: false`), `fastapi.py` (the placement and `bind_<api>()`) and, with `nativeDocs`,
6
+ `native_docs.json` (seampact's operation objects for FastAPI's document). It refuses by name every
7
+ option, artefact and contract form it does not serve, all at once."""
8
+
9
+ from collections.abc import Sequence
10
+ from typing import Final
11
+
12
+ from seampact.codegen.contract_module import contract_module
13
+ from seampact.codegen.handler_forms import handler_warnings
14
+ from seampact.codegen.input_schemas import proof_models, with_input_schemas
15
+ from seampact.codegen.kept_forms import refuse_kept_forms
16
+ from seampact.codegen.matrix_row import refuse_missing_matrix_row
17
+ from seampact.codegen.names import api_names
18
+ from seampact.codegen.native_docs_file import FrameworkDocument, native_docs_file
19
+ from seampact.codegen.operation_types import operation_types_module
20
+ from seampact.codegen.printer import PythonModule
21
+ from seampact.codegen.pydantic_forms import refuse_unserved_forms
22
+ from seampact.codegen.pydantic_layout import api_layout
23
+ from seampact.codegen.pydantic_models import pydantic_schemas_module
24
+ from seampact.codegen.refusals import Refusals
25
+ from seampact.codegen.route_helpers import route_helpers_module
26
+ from seampact.codegen.routes import route_table
27
+ from seampact.fastapi.options import read_options
28
+ from seampact.fastapi.placement_module import placement_module
29
+ from seampact.ir.config.problem import ConfigProblem
30
+ from seampact.ir.model import ContractIR
31
+ from seampact.ir.read import IrSupport
32
+ from seampact.ir.run.generator import TargetEntry
33
+ from seampact.ir.write import GeneratedFile
34
+
35
+ TARGET: Final = "fastapi"
36
+ DISTRIBUTION: Final = "seampact-fastapi"
37
+ _HTTP_OPERATION: Final = "httpOperation"
38
+ _FASTAPI_DOCUMENT: Final = FrameworkDocument(TARGET, "FastAPI", ("ValidationError", "HTTPValidationError"))
39
+
40
+
41
+ class FastApiGenerator:
42
+ def ir_support(self) -> IrSupport:
43
+ return IrSupport.of_model(kinds=[_HTTP_OPERATION])
44
+
45
+ def framework_docs_off_fix(self) -> str | None:
46
+ return None
47
+
48
+ def handler_warnings(self, ir: ContractIR, entry: TargetEntry) -> Sequence[ConfigProblem]:
49
+ return handler_warnings(ir, entry.id, entry.api)
50
+
51
+ def generate(self, ir: ContractIR, entry: TargetEntry) -> Sequence[GeneratedFile]:
52
+ refusals = Refusals(entry.id, TARGET, DISTRIBUTION)
53
+ options = read_options(entry.options, entry.artefacts, refusals)
54
+
55
+ refuse_kept_forms(ir, refusals)
56
+
57
+ refuse_missing_matrix_row(entry, refusals, "FastAPI and Starlette")
58
+ refuse_unserved_forms(ir, refusals)
59
+ error_codes = sorted({error.code for operation in ir.operations for error in operation.errors})
60
+ layout = api_layout(ir, error_codes, refusals)
61
+ with_inputs = with_input_schemas(ir, layout)
62
+ names = api_names(with_inputs, refusals)
63
+ routes = route_table(ir, entry.mount or "", refusals)
64
+ files = [
65
+ GeneratedFile("schemas.py", pydantic_schemas_module(with_inputs, names, options.rules, refusals)),
66
+ GeneratedFile(
67
+ "__init__.py", PythonModule(f"The generated code of api '{ir.api.id}' for the FastAPI target.").text()
68
+ ),
69
+ GeneratedFile("operations.py", operation_types_module(with_inputs, names, layout, refusals)),
70
+ GeneratedFile("contract.py", contract_module(ir, names, entry.mount or "", refusals)),
71
+ GeneratedFile(
72
+ "fastapi.py", placement_module(ir, layout, proof_models(layout, names), routes, options, entry)
73
+ ),
74
+ ]
75
+
76
+ if options.route_helpers:
77
+ files.append(GeneratedFile("routes.py", route_helpers_module(ir, routes, refusals)))
78
+
79
+ if options.native_docs:
80
+ native_docs = native_docs_file(entry.native_docs, layout.operations, _FASTAPI_DOCUMENT, refusals)
81
+ files.extend([] if native_docs is None else [native_docs])
82
+
83
+ refusals.raise_if_any()
84
+
85
+ return sorted(files, key=lambda generated: generated.path)
@@ -0,0 +1,241 @@
1
+ """The options and artefact switches of a fastapi target entry (ADR 0026 "Options and registration" and "What the
2
+ shape-imposing features turn off with"), each unknown key or value outside its set refused by name: `handlers`
3
+ (`perResource` only), `indent` (the four spaces ruff writes), `rules`, and the run-time defaults `responses`,
4
+ `bodyValidation`, `frameworkEtag` and `catchAll`; the naming options and `layout` are refused. The artefacts are
5
+ `bootCheck`, `accessHooks` (`false` waits for a consumer), `routeHelpers` and `nativeDocs`."""
6
+
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass
9
+ from enum import StrEnum
10
+ from typing import Final
11
+
12
+ from seampact.codegen.boot_check_hooks import HOOKS_ERROR, read_boot_check_hooks
13
+ from seampact.codegen.pydantic_models import RulesMode
14
+ from seampact.codegen.refusals import Delivery, IrPlace, Refusals
15
+ from seampact.codegen.runtime.reply_verdict import FrameworkEtagMode, ResponsesMode
16
+ from seampact.fastapi.runtime.placement import BootCheckSwitches, FrameworkRoutesCheck, HooksCheck, TargetDefaults
17
+ from seampact.ir.config.json_value import JsonValue
18
+
19
+ INDENT_SPACES: Final = 4
20
+ _HANDLERS: Final = "handlers"
21
+ _PER_RESOURCE: Final = "perResource"
22
+ _INDENT: Final = "indent"
23
+ _RULES: Final = "rules"
24
+ _RESPONSES: Final = "responses"
25
+ _BODY_VALIDATION: Final = "bodyValidation"
26
+ _FRAMEWORK_ETAG: Final = "frameworkEtag"
27
+ _CATCH_ALL: Final = "catchAll"
28
+ _LAYOUT: Final = "layout"
29
+ _NAMING_KEYS: Final = ("suffixes", "escape", "enumCase")
30
+ _KNOWN_KEYS: Final = (
31
+ _HANDLERS,
32
+ _INDENT,
33
+ _RULES,
34
+ _RESPONSES,
35
+ _BODY_VALIDATION,
36
+ _FRAMEWORK_ETAG,
37
+ _CATCH_ALL,
38
+ _LAYOUT,
39
+ *_NAMING_KEYS,
40
+ )
41
+ _BOOT_CHECK: Final = "bootCheck"
42
+ _ACCESS_HOOKS: Final = "accessHooks"
43
+ _ROUTE_HELPERS: Final = "routeHelpers"
44
+ _NATIVE_DOCS: Final = "nativeDocs"
45
+ _ARTEFACTS: Final = (_BOOT_CHECK, _ACCESS_HOOKS, _ROUTE_HELPERS, _NATIVE_DOCS)
46
+ _STRICT_ROUTES: Final = "strict"
47
+ _FRAMEWORK_ROUTES: Final = "frameworkRoutes"
48
+ _HOOKS: Final = "hooks"
49
+ _BOOT_CHECK_KEYS: Final = (_FRAMEWORK_ROUTES, _HOOKS)
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class FastApiOptions:
54
+ rules: RulesMode
55
+ defaults: TargetDefaults
56
+ boot_check: BootCheckSwitches
57
+ route_helpers: bool
58
+ native_docs: bool
59
+
60
+
61
+ def read_options(
62
+ options: Mapping[str, JsonValue], artefacts: Mapping[str, JsonValue], refusals: Refusals
63
+ ) -> FastApiOptions:
64
+ """The entry's options and artefacts; every refusal goes to `refusals`, and a refused value reads as its default."""
65
+ values = _OptionValues(options, refusals)
66
+ values.refuse_unknown()
67
+ values.refuse_unsupported()
68
+
69
+ return FastApiOptions(
70
+ rules=values.choice(_RULES, RulesMode.NATIVE),
71
+ defaults=TargetDefaults(
72
+ responses=values.choice(_RESPONSES, ResponsesMode.STRICT),
73
+ body_validation=values.flag(_BODY_VALIDATION, default=False),
74
+ framework_etag=values.choice(_FRAMEWORK_ETAG, FrameworkEtagMode.STRIP),
75
+ catch_all=values.flag(_CATCH_ALL, default=True),
76
+ ),
77
+ boot_check=_boot_check(artefacts.get(_BOOT_CHECK, True), refusals),
78
+ route_helpers=_read_artefacts(artefacts, refusals),
79
+ native_docs=artefacts.get(_NATIVE_DOCS) is True,
80
+ )
81
+
82
+
83
+ class _OptionValues:
84
+ def __init__(self, options: Mapping[str, JsonValue], refusals: Refusals) -> None:
85
+ self._options = options
86
+ self._refusals = refusals
87
+
88
+ def refuse_unknown(self) -> None:
89
+ for key in self._options:
90
+ if key not in _KNOWN_KEYS:
91
+ self._refuse(
92
+ "options.unknown",
93
+ key,
94
+ f"The option '{key}' is not one the fastapi target knows; it takes "
95
+ f"'{"', '".join(_KNOWN_KEYS[:7])}'.",
96
+ f"Remove '{key}' from the target's `options`.",
97
+ )
98
+
99
+ def refuse_unsupported(self) -> None:
100
+ handlers = self._options.get(_HANDLERS, _PER_RESOURCE)
101
+
102
+ if handlers != _PER_RESOURCE:
103
+ self._refuse(
104
+ "options.handlers.unsupported",
105
+ _HANDLERS,
106
+ f"The option 'handlers' is {handlers!r}; the fastapi target groups operations per resource only (ADR "
107
+ "0007 'perResource'), and 'perOperation' waits for an app that needs it.",
108
+ "Set 'handlers' to 'perResource', or leave it out.",
109
+ )
110
+
111
+ indent = self._options.get(_INDENT, INDENT_SPACES)
112
+
113
+ if indent != INDENT_SPACES or isinstance(indent, bool):
114
+ self._refuse(
115
+ "options.indent.unsupported",
116
+ _INDENT,
117
+ f"The option 'indent' is {indent!r}; the fastapi target writes the four spaces ruff formats with.",
118
+ f"Set 'indent' to {INDENT_SPACES}, or leave it out.",
119
+ )
120
+
121
+ if _LAYOUT in self._options:
122
+ self._refuse(
123
+ "options.layout.unsupported",
124
+ _LAYOUT,
125
+ "The option 'layout' is set; the fastapi target writes one package per api, the only layout it takes.",
126
+ "Remove 'layout' from the target's `options`.",
127
+ )
128
+
129
+ for key in _NAMING_KEYS:
130
+ if key in self._options:
131
+ self._refuse(
132
+ "options.naming.unsupported",
133
+ key,
134
+ f"The naming option '{key}' is not taken by the fastapi target yet: it names Python with the "
135
+ "defaults of ADR 0007, which the shared naming vectors test.",
136
+ f"Remove '{key}' from the target's `options`.",
137
+ )
138
+
139
+ def choice[E: StrEnum](self, key: str, default: E) -> E:
140
+ value = self._options.get(key, default.value)
141
+
142
+ for case in type(default):
143
+ if case.value == value:
144
+ return case
145
+
146
+ allowed = "', '".join(case.value for case in type(default))
147
+ self._refuse(
148
+ f"options.{key}.invalid",
149
+ key,
150
+ f"The option '{key}' is {value!r}, not one of '{allowed}'.",
151
+ f"Set '{key}' to '{default.value}' (the default) or another listed value.",
152
+ )
153
+
154
+ return default
155
+
156
+ def flag(self, key: str, *, default: bool) -> bool:
157
+ value = self._options.get(key, default)
158
+
159
+ if isinstance(value, bool):
160
+ return value
161
+
162
+ self._refuse(
163
+ f"options.{key}.invalid",
164
+ key,
165
+ f"The option '{key}' is {value!r}, not true or false.",
166
+ f"Set '{key}' to true or false (the default is {str(default).lower()}).",
167
+ )
168
+
169
+ return default
170
+
171
+ def _refuse(self, code: str, key: str, message: str, fix: str) -> None:
172
+ self._refusals.add(code, message, fix, IrPlace(f"/options/{key}"))
173
+
174
+
175
+ def _boot_check(value: JsonValue, refusals: Refusals) -> BootCheckSwitches:
176
+ """`bootCheck`: a boolean, or `{ frameworkRoutes, hooks }`; a value outside its set is refused and reads as the
177
+ default."""
178
+ at = IrPlace(f"/artefacts/{_BOOT_CHECK}")
179
+
180
+ if not isinstance(value, Mapping):
181
+ if not isinstance(value, bool):
182
+ _refuse_boot_check_value(_BOOT_CHECK, value, "true, false or an object", at, refusals)
183
+
184
+ enabled = value is not False
185
+ on = FrameworkRoutesCheck.ON if enabled else FrameworkRoutesCheck.OFF
186
+
187
+ return BootCheckSwitches(enabled=enabled, framework_routes=on, hooks=HooksCheck.ERROR)
188
+
189
+ for key in value:
190
+ if key not in _BOOT_CHECK_KEYS:
191
+ _refuse_boot_check_value(f"{_BOOT_CHECK}.{key}", value[key], "absent", at.below(key), refusals)
192
+
193
+ framework_routes = value.get(_FRAMEWORK_ROUTES, True)
194
+ routes_check = _framework_routes_check(framework_routes)
195
+ hooks_error = read_boot_check_hooks(value.get(_HOOKS, HOOKS_ERROR), at.below(_HOOKS), refusals)
196
+ hooks_check = HooksCheck.ERROR if hooks_error else HooksCheck.OFF
197
+
198
+ if routes_check is None:
199
+ allowed = f"true, false or '{_STRICT_ROUTES}'"
200
+ _refuse_boot_check_value(
201
+ f"{_BOOT_CHECK}.{_FRAMEWORK_ROUTES}", framework_routes, allowed, at.below(_FRAMEWORK_ROUTES), refusals
202
+ )
203
+
204
+ return BootCheckSwitches(enabled=True, framework_routes=routes_check or FrameworkRoutesCheck.ON, hooks=hooks_check)
205
+
206
+
207
+ def _framework_routes_check(value: JsonValue) -> FrameworkRoutesCheck | None:
208
+ if value is True:
209
+ return FrameworkRoutesCheck.ON
210
+
211
+ if value is False:
212
+ return FrameworkRoutesCheck.OFF
213
+
214
+ return FrameworkRoutesCheck.STRICT if value == _STRICT_ROUTES else None
215
+
216
+
217
+ def _refuse_boot_check_value(name: str, value: JsonValue, allowed: str, at: IrPlace, refusals: Refusals) -> None:
218
+ refusals.add(
219
+ f"artefacts.{name}.invalid",
220
+ f"The artefact '{name}' is {value!r}, not {allowed}.",
221
+ f"Set '{name}' to one of the listed values, or remove it for its default.",
222
+ at,
223
+ )
224
+
225
+
226
+ def _read_artefacts(artefacts: Mapping[str, JsonValue], refusals: Refusals) -> bool:
227
+ """Refuses the artefacts the target does not take yet; returns whether the route helpers are on."""
228
+ for key, value in artefacts.items():
229
+ at = IrPlace(f"/artefacts/{key}")
230
+
231
+ if key not in _ARTEFACTS:
232
+ refusals.add(
233
+ "artefacts.unknown",
234
+ f"The artefact '{key}' is not one the fastapi target declares; it takes '{"', '".join(_ARTEFACTS)}'.",
235
+ f"Remove '{key}' from the entry's `artefacts`.",
236
+ at,
237
+ )
238
+ elif key == _ACCESS_HOOKS and value is False:
239
+ refusals.waiting("artefacts.accessHooks", "The artefact switch 'accessHooks: false'", at, Delivery.CONSUMER)
240
+
241
+ return artefacts.get(_ROUTE_HELPERS, True) is not False
@@ -0,0 +1,287 @@
1
+ """The `fastapi` module of an api's package (ADR 0026 "Placement" and "Routes"): `GENERATED_BY`, the api's placement
2
+ (its contract routes in registration order, literal segments first, under the entry's `mount`; its contract exception
3
+ base; the run-time defaults of the target options; the boot check switches and the entry's framework routes), the
4
+ `<Api>HandlerFactories` the app fills with one FastAPI dependency per endpoints protocol, one coroutine per operation
5
+ calling its handler method, and `bind_<api>()`, which declares each contract route on the api's router and hands it
6
+ to the runtime."""
7
+
8
+ from collections.abc import Mapping, Sequence
9
+ from typing import Final
10
+
11
+ from seampact.codegen.native_docs_file import NATIVE_DOCS_FILE
12
+ from seampact.codegen.placement_lines import (
13
+ add_operation_call,
14
+ credentials_types,
15
+ operation_types_arguments,
16
+ tuple_literal,
17
+ unexpected_error_listener,
18
+ )
19
+ from seampact.codegen.printer import INDENT, PythonModule, bracketed, string_literal
20
+ from seampact.codegen.pydantic_layout import ApiLayout
21
+ from seampact.codegen.pydantic_models import PYDANTIC_MAJOR, PYDANTIC_PACKAGE
22
+ from seampact.codegen.routes import RouteEntry
23
+ from seampact.codegen.runtime import RUNTIME_VERSION
24
+ from seampact.codegen.source_lines import with_local_imports
25
+ from seampact.fastapi.options import FastApiOptions
26
+ from seampact.ir.model import ContractIR
27
+ from seampact.ir.run.generator import FrameworkDocs, TargetEntry
28
+
29
+ _RUNTIME: Final = "_runtime"
30
+ _HOOKS: Final = "_hooks"
31
+ _ABC: Final = "_abc"
32
+ # Each operation's coroutine: no module alias starts so, whatever the operation's name.
33
+ _CALL_PREFIX: Final = "_call_"
34
+ _CONTRACT_MODULE: Final = "contract"
35
+ _SCHEMAS_MODULE: Final = "schemas"
36
+ _BIND_DOC: Final = "Places api '{api}' in the app; call it last, after its own middleware and exception handlers."
37
+
38
+
39
+ def placement_module(
40
+ ir: ContractIR,
41
+ layout: ApiLayout,
42
+ proof_models: Mapping[str, str],
43
+ routes: Sequence[RouteEntry],
44
+ options: FastApiOptions,
45
+ entry: TargetEntry,
46
+ ) -> str:
47
+ """`proof_models` names the `<Op>Proofs` class of the schemas module for each operation whose body has proofs."""
48
+ module = PythonModule(f"The FastAPI placement of api '{ir.api.id}'.")
49
+ module.imported("collections.abc", _ABC)
50
+ module.imported("dataclasses", "_dataclasses")
51
+
52
+ if options.native_docs:
53
+ module.imported("pathlib", "_pathlib")
54
+
55
+ module.imported("typing", "_typing")
56
+ module.imported("seampact.fastapi.runtime", _RUNTIME)
57
+ module.imported("seampact.hooks", _HOOKS)
58
+ root_names = [layout.contract_error, *(credentials.class_name for credentials in layout.credentials.values())]
59
+ operations = [operation for operation in ir.operations if operation.id in layout.operations]
60
+
61
+ for operation in operations:
62
+ names = layout.operations[operation.id]
63
+ root_names += [names.endpoints, names.request, names.result, names.errors, *names.responses.values()]
64
+
65
+ head = [f"GENERATED_BY: _typing.Final = {string_literal(RUNTIME_VERSION)}"]
66
+ local_modules = [_CONTRACT_MODULE, _SCHEMAS_MODULE] if proof_models else [_CONTRACT_MODULE]
67
+ module.add_block(with_local_imports(local_modules, root_names, head))
68
+ module.add_block(_placement(ir, layout, routes, options, entry))
69
+ module.add_block(_handler_factories(layout))
70
+ module.add_block(unexpected_error_listener(ir.api.id, _HOOKS, layout))
71
+
72
+ for operation in operations:
73
+ names = layout.operations[operation.id]
74
+ module.add_block(
75
+ [
76
+ *bracketed(
77
+ "",
78
+ f"async def {_CALL_PREFIX}{names.method}",
79
+ [f"endpoints: {names.endpoints}", f"request: {names.request}"],
80
+ f" -> {names.result}:",
81
+ ),
82
+ f"{INDENT}return await {_RUNTIME}.call_handler(endpoints.{names.method}, request)",
83
+ ]
84
+ )
85
+
86
+ module.add_block(_bind(ir, layout, proof_models, routes))
87
+
88
+ return module.text()
89
+
90
+
91
+ def _placement(
92
+ ir: ContractIR, layout: ApiLayout, routes: Sequence[RouteEntry], options: FastApiOptions, entry: TargetEntry
93
+ ) -> list[str]:
94
+ route_lines: list[str] = []
95
+
96
+ for route in routes:
97
+ methods = tuple_literal(route.methods)
98
+ arguments = [
99
+ string_literal(route.operation_id),
100
+ string_literal(route.name),
101
+ string_literal(route.path()),
102
+ methods,
103
+ ]
104
+ route_lines += bracketed(INDENT * 2, f"{_RUNTIME}.OperationRoute", arguments, ",")
105
+
106
+ framework_routes: list[str] = []
107
+
108
+ for allowed in entry.framework_routes:
109
+ arguments = [
110
+ f"path={string_literal(allowed.path)}",
111
+ f"method={'None' if allowed.method is None else string_literal(allowed.method)}",
112
+ f"reason={string_literal(allowed.reason)}",
113
+ ]
114
+ framework_routes += bracketed(INDENT * 2, f"{_RUNTIME}.FrameworkRouteRule", arguments, ",")
115
+
116
+ boot = options.boot_check
117
+ defaults = options.defaults
118
+
119
+ return [
120
+ f"PLACEMENT: _typing.Final = {_RUNTIME}.ApiPlacement(",
121
+ f"{INDENT}api={string_literal(ir.api.id)},",
122
+ f"{INDENT}generated_by=GENERATED_BY,",
123
+ f"{INDENT}mount={string_literal(entry.mount or '')},",
124
+ f"{INDENT}routes=(",
125
+ *route_lines,
126
+ f"{INDENT}),",
127
+ f"{INDENT}error_base={layout.contract_error},",
128
+ *bracketed(
129
+ INDENT,
130
+ f"defaults={_RUNTIME}.TargetDefaults",
131
+ [
132
+ f"responses={_RUNTIME}.ResponsesMode.{defaults.responses.name}",
133
+ f"body_validation={defaults.body_validation}",
134
+ f"framework_etag={_RUNTIME}.FrameworkEtagMode.{defaults.framework_etag.name}",
135
+ f"catch_all={defaults.catch_all}",
136
+ ],
137
+ ",",
138
+ ),
139
+ *bracketed(
140
+ INDENT,
141
+ f"boot_check={_RUNTIME}.BootCheckSwitches",
142
+ [
143
+ f"enabled={boot.enabled}",
144
+ f"framework_routes={_RUNTIME}.FrameworkRoutesCheck.{boot.framework_routes.name}",
145
+ f"hooks={_RUNTIME}.HooksCheck.{boot.hooks.name}",
146
+ ],
147
+ ",",
148
+ ),
149
+ *(
150
+ [f"{INDENT}framework_routes=(),"]
151
+ if not framework_routes
152
+ else [f"{INDENT}framework_routes=(", *framework_routes, f"{INDENT}),"]
153
+ ),
154
+ f"{INDENT}frameworks=(",
155
+ *_accepted_frameworks(entry),
156
+ f"{INDENT}),",
157
+ *credentials_types(_RUNTIME, layout),
158
+ *_framework_document(options, entry),
159
+ ")",
160
+ ]
161
+
162
+
163
+ def _framework_document(options: FastApiOptions, entry: TargetEntry) -> list[str]:
164
+ """`frameworkDocs: off` and `nativeDocs`, each written only when the entry asks for it."""
165
+ lines: list[str] = []
166
+
167
+ if entry.framework_docs is FrameworkDocs.OFF:
168
+ lines.append(f"{INDENT}framework_docs={_RUNTIME}.FrameworkDocs.OFF,")
169
+
170
+ if options.native_docs:
171
+ native_docs_path = f"_pathlib.Path(__file__).with_name({string_literal(NATIVE_DOCS_FILE)})"
172
+ lines.append(f"{INDENT}native_docs={_RUNTIME}.read_native_docs({native_docs_path}),")
173
+
174
+ return lines
175
+
176
+
177
+ def _accepted_frameworks(entry: TargetEntry) -> list[str]:
178
+ """The matrix row's frameworks (FastAPI by minor, Starlette by major) with the entry's `allowUntested` units, then
179
+ Pydantic 2, which the generated models are written for."""
180
+ accepted = [
181
+ (framework.package, framework.unit.name, framework.accepted_units(entry.allow_untested))
182
+ for framework in (entry.matrix.frameworks if entry.matrix is not None else ())
183
+ ]
184
+ accepted.append((PYDANTIC_PACKAGE, "MAJOR", (PYDANTIC_MAJOR,)))
185
+ lines: list[str] = []
186
+
187
+ for package, unit, units in accepted:
188
+ arguments = [
189
+ f"package={string_literal(package)}",
190
+ f"unit={_RUNTIME}.VersionUnit.{unit}",
191
+ f"accepted={tuple_literal(units)}",
192
+ ]
193
+ lines += bracketed(INDENT * 2, f"{_RUNTIME}.AcceptedFramework", arguments, ",")
194
+
195
+ return lines
196
+
197
+
198
+ def _handler_factories(layout: ApiLayout) -> list[str]:
199
+ lines = [
200
+ "@_dataclasses.dataclass(frozen=True, slots=True, kw_only=True)",
201
+ f"class {layout.handler_factories}:",
202
+ f'{INDENT}"""The FastAPI dependency that builds each endpoints protocol\'s handlers; None fails the boot."""',
203
+ "",
204
+ ]
205
+ lines += [
206
+ f"{INDENT}{attribute}: {_RUNTIME}.Factory[{endpoints}] | None"
207
+ for endpoints, attribute in layout.endpoints_owners().items()
208
+ ]
209
+
210
+ return lines
211
+
212
+
213
+ def _bind(
214
+ ir: ContractIR, layout: ApiLayout, proof_models: Mapping[str, str], routes: Sequence[RouteEntry]
215
+ ) -> list[str]:
216
+ def factories(hook: str) -> str:
217
+ return f"{_ABC}.Mapping[str, {_RUNTIME}.Factory[{_HOOKS}.{hook}]] | None = None"
218
+
219
+ parameters = [
220
+ f"target: {_RUNTIME}.BindTarget",
221
+ "*",
222
+ f"handlers: {layout.handler_factories}",
223
+ f"app: {_RUNTIME}.App | None = None",
224
+ f"authenticate: {factories('AuthenticateHook')}",
225
+ f"deciders: {factories('DecideHook')}",
226
+ f"scope_resolvers: {factories('ResolveScopeHook')}",
227
+ f"idempotency_store: {_RUNTIME}.Factory[{_HOOKS}.IdempotencyStore] | None = None",
228
+ f"on_unexpected_error: {_RUNTIME}.Factory[{layout.unexpected_error_listener}] | None = None",
229
+ f"responses: {_RUNTIME}.ResponsesMode | None = None",
230
+ "body_validation: bool | None = None",
231
+ f"operations: {_ABC}.Mapping[str, {_RUNTIME}.OperationSettings] | None = None",
232
+ "public_origin: str | None = None",
233
+ ]
234
+ head = [
235
+ *bracketed("", f"def {layout.bind_function}", parameters, " -> None:"),
236
+ f'{INDENT}"""{_BIND_DOC.format(api=ir.api.id)}"""',
237
+ ]
238
+ lines: list[str] = []
239
+
240
+ for route in routes:
241
+ lines += _add_operation(layout, route.operation_id, proof_models.get(route.operation_id))
242
+
243
+ hooks = [
244
+ "authenticate=authenticate or {}",
245
+ "deciders=deciders or {}",
246
+ "scope_resolvers=scope_resolvers or {}",
247
+ "idempotency_store=idempotency_store",
248
+ "on_unexpected_error=on_unexpected_error",
249
+ ]
250
+ settings = [
251
+ "responses=responses",
252
+ "body_validation=body_validation",
253
+ "operations=operations or {}",
254
+ "public_origin=public_origin",
255
+ ]
256
+
257
+ return [
258
+ *head,
259
+ f"{INDENT}binding = {_RUNTIME}.api_binding(",
260
+ f"{INDENT * 2}PLACEMENT,",
261
+ f"{INDENT * 2}_{_CONTRACT_MODULE}.CONTRACT,",
262
+ f"{INDENT * 2}target,",
263
+ f"{INDENT * 2}app=app,",
264
+ *bracketed(INDENT * 2, f"hooks={_RUNTIME}.HookFactories", hooks, ","),
265
+ *bracketed(INDENT * 2, f"settings={_RUNTIME}.RunSettings", settings, ","),
266
+ f"{INDENT})",
267
+ "",
268
+ *lines,
269
+ "",
270
+ f"{INDENT}{_RUNTIME}.bind_api(binding)",
271
+ ]
272
+
273
+
274
+ def _add_operation(layout: ApiLayout, operation_id: str, proof_model: str | None) -> list[str]:
275
+ """One `add_operation` call, split as ruff splits it: the call on one line when it fits, else one argument per
276
+ line, the operation's types a nested call split the same way."""
277
+ names = layout.operations[operation_id]
278
+ proofs = None if proof_model is None else f"_{_SCHEMAS_MODULE}.{proof_model}"
279
+ types = operation_types_arguments(layout, operation_id, proofs)
280
+ arguments = [
281
+ "binding",
282
+ string_literal(operation_id),
283
+ f"handlers.{names.owner_attribute}",
284
+ f"{_CALL_PREFIX}{names.method}",
285
+ ]
286
+
287
+ return add_operation_call(_RUNTIME, arguments, types)
File without changes
@@ -0,0 +1,63 @@
1
+ """The FastAPI placement runtime the generated code imports (ADR 0026 "What the generated code depends on"): the
2
+ declarations of one api's placement, its binding, its contract routes with the access and input dependencies, the
3
+ boundary middleware, the exception map and the boot check. The framework-neutral steps come from
4
+ `seampact.codegen.runtime`."""
5
+
6
+ from seampact.codegen.runtime.access_steps import CredentialsType
7
+ from seampact.codegen.runtime.framework_routes import FrameworkRouteRule
8
+ from seampact.codegen.runtime.framework_versions import AcceptedFramework, VersionUnit
9
+ from seampact.codegen.runtime.native_docs import NativeDocsError, read_native_docs
10
+ from seampact.codegen.runtime.reply_verdict import FrameworkEtagMode, ResponsesMode
11
+ from seampact.codegen.runtime.run_settings import OperationSettings, OperationTypes, RunSettings
12
+ from seampact.fastapi.runtime.api_binding import ApiBinding, BindingError, api_binding
13
+ from seampact.fastapi.runtime.binding import bind_api
14
+ from seampact.fastapi.runtime.boot_check import BootCheckError
15
+ from seampact.fastapi.runtime.boundary import ContractBoundary
16
+ from seampact.fastapi.runtime.handler_call import call_handler
17
+ from seampact.fastapi.runtime.native_docs import FrameworkDocs
18
+ from seampact.fastapi.runtime.placement import (
19
+ ApiPlacement,
20
+ App,
21
+ BindTarget,
22
+ BootCheckSwitches,
23
+ Factory,
24
+ FrameworkRoutesCheck,
25
+ HookFactories,
26
+ HooksCheck,
27
+ OperationRoute,
28
+ TargetDefaults,
29
+ )
30
+ from seampact.fastapi.runtime.routes import add_operation
31
+
32
+ __all__ = [
33
+ "AcceptedFramework",
34
+ "ApiBinding",
35
+ "ApiPlacement",
36
+ "App",
37
+ "BindTarget",
38
+ "BindingError",
39
+ "BootCheckError",
40
+ "BootCheckSwitches",
41
+ "ContractBoundary",
42
+ "CredentialsType",
43
+ "Factory",
44
+ "FrameworkDocs",
45
+ "FrameworkEtagMode",
46
+ "FrameworkRouteRule",
47
+ "FrameworkRoutesCheck",
48
+ "HookFactories",
49
+ "HooksCheck",
50
+ "NativeDocsError",
51
+ "OperationRoute",
52
+ "OperationSettings",
53
+ "OperationTypes",
54
+ "ResponsesMode",
55
+ "RunSettings",
56
+ "TargetDefaults",
57
+ "VersionUnit",
58
+ "add_operation",
59
+ "api_binding",
60
+ "bind_api",
61
+ "call_handler",
62
+ "read_native_docs",
63
+ ]