seampact-fastapi 1.0.0__tar.gz
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_fastapi-1.0.0/.gitignore +6 -0
- seampact_fastapi-1.0.0/LICENSE +21 -0
- seampact_fastapi-1.0.0/PKG-INFO +20 -0
- seampact_fastapi-1.0.0/README.md +1 -0
- seampact_fastapi-1.0.0/pyproject.toml +44 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/__init__.py +2 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/generator.py +85 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/options.py +241 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/placement_module.py +287 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/py.typed +0 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/__init__.py +63 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/api_binding.py +178 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/binding.py +23 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/boot_check.py +336 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/boundary.py +472 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/exception_map.py +111 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/exception_roles.py +71 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/framework_document.py +54 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/handler_call.py +16 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/input_features.py +54 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/native_docs.py +12 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/placement.py +113 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/request_state.py +69 -0
- seampact_fastapi-1.0.0/src/seampact/fastapi/runtime/routes.py +297 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Julien Metral
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: seampact-fastapi
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The FastAPI target of seampact: its generator and the placement runtime of the generated code.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Framework :: FastAPI
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Typing :: Typed
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Requires-Dist: fastapi>=0.162
|
|
13
|
+
Requires-Dist: seampact-codegen[pydantic]==1.0.0
|
|
14
|
+
Requires-Dist: seampact-hooks==1.0.0
|
|
15
|
+
Provides-Extra: generate
|
|
16
|
+
Requires-Dist: seampact-codegen[generate]==1.0.0; extra == 'generate'
|
|
17
|
+
Requires-Dist: seampact-ir==1.0.0; extra == 'generate'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
The FastAPI target of seampact: the generator that `seampact generate` finds through its entry point, and the runtime the generated placement imports.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
The FastAPI target of seampact: the generator that `seampact generate` finds through its entry point, and the runtime the generated placement imports.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling==1.33.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "seampact-fastapi"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "The FastAPI target of seampact: its generator and the placement runtime of the generated code."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.12"
|
|
13
|
+
# FastAPI breaks in minors: 0.162 is the matrix floor, a later minor is untested and refused at boot unless the app
|
|
14
|
+
# config allows it. Starlette and Pydantic come through FastAPI's own ranges; Pydantic 2.9 is FastAPI 0.162's floor.
|
|
15
|
+
dependencies = [
|
|
16
|
+
"fastapi>=0.162",
|
|
17
|
+
"seampact-codegen[pydantic]==1.0.0",
|
|
18
|
+
"seampact-hooks==1.0.0",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Framework :: FastAPI",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
# What the generator itself needs; a production install of an app leaves it out.
|
|
29
|
+
generate = ["seampact-codegen[generate]==1.0.0", "seampact-ir==1.0.0"]
|
|
30
|
+
|
|
31
|
+
[project.entry-points."seampact.generators"]
|
|
32
|
+
fastapi = "seampact.fastapi.generator:FastApiGenerator"
|
|
33
|
+
|
|
34
|
+
# PEP 420: `seampact` is a namespace shared by every seampact distribution, so it has no `__init__.py`.
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/seampact"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.sdist]
|
|
39
|
+
include = ["src/seampact", "LICENSE"]
|
|
40
|
+
|
|
41
|
+
[tool.uv.sources]
|
|
42
|
+
seampact-codegen = { workspace = true }
|
|
43
|
+
seampact-hooks = { workspace = true }
|
|
44
|
+
seampact-ir = { workspace = true }
|
|
@@ -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
|