seampact-drf 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_drf-1.0.0/.gitignore +6 -0
- seampact_drf-1.0.0/LICENSE +21 -0
- seampact_drf-1.0.0/PKG-INFO +21 -0
- seampact_drf-1.0.0/README.md +1 -0
- seampact_drf-1.0.0/pyproject.toml +40 -0
- seampact_drf-1.0.0/src/seampact/drf/__init__.py +3 -0
- seampact_drf-1.0.0/src/seampact/drf/api_layout.py +179 -0
- seampact_drf-1.0.0/src/seampact/drf/dataclass_schemas.py +131 -0
- seampact_drf-1.0.0/src/seampact/drf/drf_module.py +319 -0
- seampact_drf-1.0.0/src/seampact/drf/expressions.py +79 -0
- seampact_drf-1.0.0/src/seampact/drf/generator.py +134 -0
- seampact_drf-1.0.0/src/seampact/drf/operation_types.py +400 -0
- seampact_drf-1.0.0/src/seampact/drf/py.typed +0 -0
- seampact_drf-1.0.0/src/seampact/drf/python_types.py +139 -0
- seampact_drf-1.0.0/src/seampact/drf/runtime/__init__.py +27 -0
- seampact_drf-1.0.0/src/seampact/drf/runtime/access_classes.py +121 -0
- seampact_drf-1.0.0/src/seampact/drf/runtime/admission.py +348 -0
- seampact_drf-1.0.0/src/seampact/drf/runtime/boundary.py +13 -0
- seampact_drf-1.0.0/src/seampact/drf/runtime/decoded_body_parser.py +44 -0
- seampact_drf-1.0.0/src/seampact/drf/runtime/exception_handler.py +119 -0
- seampact_drf-1.0.0/src/seampact/drf/runtime/serializers.py +89 -0
- seampact_drf-1.0.0/src/seampact/drf/runtime/session_csrf.py +32 -0
- seampact_drf-1.0.0/src/seampact/drf/runtime/typed_input.py +256 -0
- seampact_drf-1.0.0/src/seampact/drf/runtime/validators.py +64 -0
- seampact_drf-1.0.0/src/seampact/drf/runtime/views.py +119 -0
- seampact_drf-1.0.0/src/seampact/drf/serializer_fields.py +258 -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,21 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: seampact-drf
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The seampact Django REST Framework target: its generator and the runtime of the code it generates.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Framework :: Django
|
|
8
|
+
Classifier: Framework :: Django :: 5.2
|
|
9
|
+
Classifier: Framework :: Django :: 6.0
|
|
10
|
+
Classifier: Framework :: Django :: 6.1
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Requires-Dist: djangorestframework>=3.18
|
|
16
|
+
Requires-Dist: seampact-django==1.0.0
|
|
17
|
+
Provides-Extra: generate
|
|
18
|
+
Requires-Dist: seampact-django[generate]==1.0.0; extra == 'generate'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
The seampact Django REST Framework target: the `seampact-drf` generator, found through the `seampact.generators` entry point, and the DRF runtime of the code it generates.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
The seampact Django REST Framework target: the `seampact-drf` generator, found through the `seampact.generators` entry point, and the DRF runtime of the code it generates.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling==1.33.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "seampact-drf"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "The seampact Django REST Framework target: its generator and the runtime of the code it generates."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.12"
|
|
13
|
+
dependencies = ["djangorestframework>=3.18", "seampact-django==1.0.0"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Framework :: Django",
|
|
16
|
+
"Framework :: Django :: 5.2",
|
|
17
|
+
"Framework :: Django :: 6.0",
|
|
18
|
+
"Framework :: Django :: 6.1",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
# What the generator needs; a production install of an app leaves it out.
|
|
26
|
+
generate = ["seampact-django[generate]==1.0.0"]
|
|
27
|
+
|
|
28
|
+
# The target `use: pypi:seampact-drf` names; `seampact generate` finds it through this group.
|
|
29
|
+
[project.entry-points."seampact.generators"]
|
|
30
|
+
drf = "seampact.drf.generator:DrfGenerator"
|
|
31
|
+
|
|
32
|
+
# PEP 420: `seampact` is a namespace shared by every seampact distribution, so it has no `__init__.py`.
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/seampact"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.sdist]
|
|
37
|
+
include = ["src/seampact", "LICENSE"]
|
|
38
|
+
|
|
39
|
+
[tool.uv.sources]
|
|
40
|
+
seampact-django = { workspace = true }
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"""The Django REST Framework target: the `seampact-drf` generator, found through the `seampact.generators` entry
|
|
2
|
+
point, its emitters (frozen dataclass types, operation types and handler protocols, serializers and `APIView`
|
|
3
|
+
declarations), and the DRF runtime of the code it generates."""
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""The Python names of one api's operation types on the DRF target: per operation its request, input and input
|
|
2
|
+
parts, scopes, responses, page, errors and result; per resource (or surface, for an operation without one) its
|
|
3
|
+
handler protocol and the settings key of its handler factory; per error code its exception and its factory method.
|
|
4
|
+
Every name of the `operations` module is claimed once; a name taken twice is refused, naming both."""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
from seampact.codegen.credentials_classes import CredentialsNames, identity_credentials
|
|
11
|
+
from seampact.codegen.refusals import IrPlace, Refusals
|
|
12
|
+
from seampact.ir.model import ContractIR, CredentialIdentity, Operation, Param, SchemaPrincipal, SignedIdentity
|
|
13
|
+
from seampact.ir.naming import IrName, NameSource, NamingRole, python_identifier, segments_of
|
|
14
|
+
|
|
15
|
+
_BOUNDARY_HEADER_DERIVATIONS: Final = frozenset({"identity.authenticate"})
|
|
16
|
+
_BOUNDARY_HEADER_DERIVATION_PREFIXES: Final = ("csrf.",)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class OperationNames:
|
|
21
|
+
request: str
|
|
22
|
+
input: str
|
|
23
|
+
path: str | None
|
|
24
|
+
query: str | None
|
|
25
|
+
headers: str | None
|
|
26
|
+
scopes: str | None
|
|
27
|
+
result: str
|
|
28
|
+
errors: str
|
|
29
|
+
page: str | None
|
|
30
|
+
method: str
|
|
31
|
+
owner: "HandlerOwner"
|
|
32
|
+
responses: Mapping[int, str]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class HandlerOwner:
|
|
37
|
+
"""A handler protocol (`TasksEndpoints`) and the settings key of its factory (`tasks`)."""
|
|
38
|
+
|
|
39
|
+
protocol: str
|
|
40
|
+
key: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(slots=True)
|
|
44
|
+
class ApiLayout:
|
|
45
|
+
api_class: str
|
|
46
|
+
operations: dict[str, OperationNames] = field(default_factory=dict)
|
|
47
|
+
owners: dict[tuple[str, ...], HandlerOwner] = field(default_factory=dict)
|
|
48
|
+
error_classes: dict[str, str] = field(default_factory=dict)
|
|
49
|
+
error_factories: dict[str, str] = field(default_factory=dict)
|
|
50
|
+
credentials: dict[str, CredentialsNames] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def api_layout(ir: ContractIR, refusals: Refusals) -> ApiLayout:
|
|
54
|
+
layout = ApiLayout(python_identifier(IrName(tuple(ir.api.name.segments), NamingRole.SCHEMA)))
|
|
55
|
+
claim = NameClaims("operations", refusals)
|
|
56
|
+
|
|
57
|
+
claim.name(f"{layout.api_class}ContractError", IrPlace("/api/name"))
|
|
58
|
+
|
|
59
|
+
for code in sorted(ir.errors):
|
|
60
|
+
at = IrPlace(f"/errors/{code}")
|
|
61
|
+
segments = segments_of(code, NameSource.NAME, None, None)
|
|
62
|
+
|
|
63
|
+
if not segments:
|
|
64
|
+
refusals.inconsistent("error.code.no-segment", f"The error code '{code}' has no name segment", at)
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
layout.error_classes[code] = claim.name(python_identifier(IrName(tuple(segments), NamingRole.ERROR)), at)
|
|
68
|
+
layout.error_factories[code] = python_identifier(IrName(tuple(segments), NamingRole.MEMBER))
|
|
69
|
+
|
|
70
|
+
for index, operation in enumerate(ir.operations):
|
|
71
|
+
at = IrPlace(f"/operations/{index}", operation.id)
|
|
72
|
+
owner = _owner(ir, operation)
|
|
73
|
+
|
|
74
|
+
if owner is None:
|
|
75
|
+
refusals.inconsistent(
|
|
76
|
+
"endpoints.owner.no-segment",
|
|
77
|
+
f"The operation '{operation.id}' has no resource or surface that gives a name segment",
|
|
78
|
+
at.below("surface"),
|
|
79
|
+
)
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if owner not in layout.owners:
|
|
83
|
+
protocol = claim.name(python_identifier(IrName(owner, NamingRole.ENDPOINTS)), at.below("name"))
|
|
84
|
+
layout.owners[owner] = HandlerOwner(protocol, python_identifier(IrName(owner, NamingRole.MEMBER)))
|
|
85
|
+
|
|
86
|
+
layout.operations[operation.id] = _operation_names(operation, layout.owners[owner], claim, at.below("name"))
|
|
87
|
+
|
|
88
|
+
layout.credentials = identity_credentials(ir, layout.operations, claim.owned_name, refusals)
|
|
89
|
+
|
|
90
|
+
return layout
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _operation_names(operation: Operation, owner: HandlerOwner, claim: "NameClaims", at: IrPlace) -> OperationNames:
|
|
94
|
+
segments = tuple(operation.name.segments)
|
|
95
|
+
|
|
96
|
+
def type_name(role: NamingRole, *words: str, status: int | None = None) -> str:
|
|
97
|
+
return claim.name(python_identifier(IrName((*segments, *words), role, status)), at)
|
|
98
|
+
|
|
99
|
+
def part_name(present: bool, role: NamingRole, *words: str) -> str | None:
|
|
100
|
+
return type_name(role, *words) if present else None
|
|
101
|
+
|
|
102
|
+
return OperationNames(
|
|
103
|
+
request=type_name(NamingRole.SCHEMA, "request"),
|
|
104
|
+
input=type_name(NamingRole.INPUT),
|
|
105
|
+
path=part_name(bool(operation.params.path), NamingRole.PATH),
|
|
106
|
+
query=part_name(bool(operation.params.query), NamingRole.QUERY),
|
|
107
|
+
headers=part_name(bool(handler_headers(operation)), NamingRole.HEADERS),
|
|
108
|
+
scopes=part_name(bool(operation.scopes), NamingRole.SCHEMA, "scopes"),
|
|
109
|
+
result=type_name(NamingRole.SCHEMA, "result"),
|
|
110
|
+
errors=type_name(NamingRole.ERRORS),
|
|
111
|
+
page=part_name(operation.paging is not None, NamingRole.SCHEMA, "page"),
|
|
112
|
+
method=python_identifier(IrName(segments, NamingRole.OPERATION)),
|
|
113
|
+
owner=owner,
|
|
114
|
+
responses={
|
|
115
|
+
response.status: type_name(NamingRole.RESPONSE, status=response.status) for response in operation.responses
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def handler_headers(operation: Operation) -> list[Param]:
|
|
121
|
+
"""The header parameters the handler reads: the credential and CSRF headers stay the boundary's."""
|
|
122
|
+
return [
|
|
123
|
+
param
|
|
124
|
+
for param in operation.params.header
|
|
125
|
+
if not _boundary_header(param.derived_from.derivation if param.derived_from is not None else "")
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _boundary_header(derivation: str) -> bool:
|
|
130
|
+
return derivation in _BOUNDARY_HEADER_DERIVATIONS or derivation.startswith(_BOUNDARY_HEADER_DERIVATION_PREFIXES)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _owner(ir: ContractIR, operation: Operation) -> tuple[str, ...] | None:
|
|
134
|
+
"""A resource's operations share one protocol; one without a resource joins its surface's."""
|
|
135
|
+
if operation.resource is not None:
|
|
136
|
+
segments = segments_of(operation.resource.type, NameSource.NAME, None, None)
|
|
137
|
+
|
|
138
|
+
if segments:
|
|
139
|
+
return tuple(segments)
|
|
140
|
+
|
|
141
|
+
surface = ir.api.surfaces.get(operation.surface)
|
|
142
|
+
segments = list(surface.name.segments) if surface is not None else segments_of(operation.surface, NameSource.NAME)
|
|
143
|
+
|
|
144
|
+
return tuple(segments) if segments else None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class NameClaims:
|
|
148
|
+
"""The class names of one generated module; a name claimed twice is refused, naming both places."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, module: str, refusals: Refusals) -> None:
|
|
151
|
+
self._module = module
|
|
152
|
+
self._refusals = refusals
|
|
153
|
+
self._claimed: dict[str, IrPlace] = {}
|
|
154
|
+
|
|
155
|
+
def owned_name(self, name: str, _owner: str, at: IrPlace) -> str:
|
|
156
|
+
"""`name`, in the shape the shared emitters claim through: the place already names its owner."""
|
|
157
|
+
return self.name(name, at)
|
|
158
|
+
|
|
159
|
+
def name(self, name: str, at: IrPlace) -> str:
|
|
160
|
+
earlier = self._claimed.setdefault(name, at)
|
|
161
|
+
|
|
162
|
+
if earlier is not at:
|
|
163
|
+
self._refusals.add(
|
|
164
|
+
"name.collision",
|
|
165
|
+
f"The names at {earlier.pointer} and at {at.pointer} both give the Python class '{name}' in the "
|
|
166
|
+
f"{self._module} module.",
|
|
167
|
+
"Rename one of them in the contract, or give one a `codeName`.",
|
|
168
|
+
at,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return name
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def principal_schema(ir: ContractIR, identity: str) -> str | None:
|
|
175
|
+
"""The object schema of an identity's principal; None for a principal of several variants."""
|
|
176
|
+
declared = ir.identities.get(identity)
|
|
177
|
+
principal = declared.principal if isinstance(declared, CredentialIdentity | SignedIdentity) else None
|
|
178
|
+
|
|
179
|
+
return principal.schema if isinstance(principal, SchemaPrincipal) else None
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""The `schemas` module of an api on the DRF target (ADR 0027 "Types and validation"): a `StrEnum` per enum and a
|
|
2
|
+
frozen, slotted, keyword-only dataclass per object schema, which the handler reads. A member both optional and
|
|
3
|
+
nullable is a `Field` of `seampact.hooks`, absent by default; an optional member that is not nullable is `None` when
|
|
4
|
+
absent. Value rules are not part of the types: the serializers check them."""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
from seampact.codegen.names import ApiNames
|
|
9
|
+
from seampact.codegen.printer import INDENT, PythonModule, string_literal
|
|
10
|
+
from seampact.codegen.redacted_members import redacted_dataclass_reprs, redacted_marker_block
|
|
11
|
+
from seampact.codegen.refusals import Delivery, IrPlace, Refusals
|
|
12
|
+
from seampact.drf.python_types import PythonTypes, use_nullable
|
|
13
|
+
from seampact.ir.hydration import OMITTED
|
|
14
|
+
from seampact.ir.model import ContractIR, MapSchema, ObjectSchema, Property, ScalarTypeUse, TypeUse, UnionSchema
|
|
15
|
+
|
|
16
|
+
DATACLASS_DECORATOR = "dataclass(frozen=True, slots=True, kw_only=True)"
|
|
17
|
+
_REDACTED_DATACLASS_DECORATOR = "dataclass(frozen=True, slots=True, kw_only=True, repr=False)"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FrozenDataclasses:
|
|
21
|
+
"""The frozen dataclasses of one generated module. A class with sensitive members gets the generated `__repr__`
|
|
22
|
+
and `__rich_repr__` that print them `[redacted]` (ADR 0026 "Redaction"); the module declares the marker they print
|
|
23
|
+
once, before its first use."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, module: PythonModule) -> None:
|
|
26
|
+
self.module = module
|
|
27
|
+
self._marker_declared = False
|
|
28
|
+
|
|
29
|
+
def block(
|
|
30
|
+
self, class_name: str, body: Sequence[str], attributes: Sequence[str], redacted: Sequence[str]
|
|
31
|
+
) -> list[str]:
|
|
32
|
+
"""The class with its `body` (docstring and member lines); `attributes` names its members in order."""
|
|
33
|
+
dataclasses = self.module.imported("dataclasses", "_dataclasses")
|
|
34
|
+
|
|
35
|
+
if not redacted:
|
|
36
|
+
return [f"@{dataclasses}.{DATACLASS_DECORATOR}", f"class {class_name}:", *body]
|
|
37
|
+
|
|
38
|
+
if not self._marker_declared:
|
|
39
|
+
self._marker_declared = True
|
|
40
|
+
self.module.add_block(redacted_marker_block())
|
|
41
|
+
|
|
42
|
+
abc = self.module.imported("collections.abc", "_abc")
|
|
43
|
+
reprs = redacted_dataclass_reprs(class_name, attributes, redacted, abc)
|
|
44
|
+
|
|
45
|
+
return [f"@{dataclasses}.{_REDACTED_DATACLASS_DECORATOR}", f"class {class_name}:", *body, *reprs]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_sensitive_use(type_use: TypeUse) -> bool:
|
|
49
|
+
"""A use of a declared scalar marked sensitive (ADR 0003 section 6)."""
|
|
50
|
+
return isinstance(type_use, ScalarTypeUse) and type_use.sensitive is True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def dataclass_schemas_module(ir: ContractIR, names: ApiNames, refusals: Refusals) -> str:
|
|
54
|
+
module = PythonModule(f"The schema types of api '{ir.api.id}'.")
|
|
55
|
+
defined: set[str] = set()
|
|
56
|
+
types = PythonTypes(ir, names, refusals, defined=defined)
|
|
57
|
+
dataclasses = FrozenDataclasses(module)
|
|
58
|
+
|
|
59
|
+
for enum_id in sorted(ir.enums):
|
|
60
|
+
enum = module.imported("enum", "_enum")
|
|
61
|
+
module.add_block(
|
|
62
|
+
[
|
|
63
|
+
f"class {names.enum_classes[enum_id]}({enum}.StrEnum):",
|
|
64
|
+
*(
|
|
65
|
+
f"{INDENT}{member} = {string_literal(value)}"
|
|
66
|
+
for value, member in names.enum_members[enum_id].items()
|
|
67
|
+
),
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
for schema_id, schema in sorted(ir.schemas.items()):
|
|
72
|
+
at = IrPlace(f"/schemas/{schema_id}")
|
|
73
|
+
|
|
74
|
+
if isinstance(schema, ObjectSchema):
|
|
75
|
+
module.add_block(_dataclass(dataclasses, types, names, schema_id, schema, at, refusals))
|
|
76
|
+
defined.add(schema_id)
|
|
77
|
+
elif isinstance(schema, MapSchema | UnionSchema):
|
|
78
|
+
refusals.waiting(f"schema.{schema.kind}", f"A {schema.kind} schema", at, Delivery.CONSUMER)
|
|
79
|
+
else:
|
|
80
|
+
refusals.unknown_value(schema.kind, at.below("kind"))
|
|
81
|
+
|
|
82
|
+
return module.text()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _dataclass(
|
|
86
|
+
dataclasses: FrozenDataclasses,
|
|
87
|
+
types: PythonTypes,
|
|
88
|
+
names: ApiNames,
|
|
89
|
+
schema_id: str,
|
|
90
|
+
schema: ObjectSchema,
|
|
91
|
+
at: IrPlace,
|
|
92
|
+
refusals: Refusals,
|
|
93
|
+
) -> list[str]:
|
|
94
|
+
module = dataclasses.module
|
|
95
|
+
members = names.members.get(schema_id, {})
|
|
96
|
+
body = [
|
|
97
|
+
_member(module, types, attribute, member, at.below("properties", index), refusals)
|
|
98
|
+
for index, member in enumerate(schema.properties)
|
|
99
|
+
if (attribute := members.get(member.name)) is not None
|
|
100
|
+
]
|
|
101
|
+
redacted = [
|
|
102
|
+
attribute
|
|
103
|
+
for member in schema.properties
|
|
104
|
+
if (member.sensitive or is_sensitive_use(member.type)) and (attribute := members.get(member.name)) is not None
|
|
105
|
+
]
|
|
106
|
+
attributes = [attribute for member in schema.properties if (attribute := members.get(member.name)) is not None]
|
|
107
|
+
class_name = names.schema_classes[schema_id]
|
|
108
|
+
|
|
109
|
+
if not body:
|
|
110
|
+
return dataclasses.block(
|
|
111
|
+
class_name, [f'{INDENT}"""The schema \'{schema_id}\', which has no member."""'], [], []
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return dataclasses.block(class_name, body, attributes, redacted)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _member(
|
|
118
|
+
module: PythonModule, types: PythonTypes, attribute: str, member: Property, at: IrPlace, refusals: Refusals
|
|
119
|
+
) -> str:
|
|
120
|
+
if member.default is not OMITTED:
|
|
121
|
+
refusals.waiting("member.default", "A member with a default", at.below("default"), Delivery.CONSUMER)
|
|
122
|
+
|
|
123
|
+
type_nullable = use_nullable(member.type)
|
|
124
|
+
suffix = " | None" if (member.optional or member.nullable) and not type_nullable else ""
|
|
125
|
+
annotation = types.annotation(module, member.type, at.below("type"), suffix)
|
|
126
|
+
|
|
127
|
+
if member.optional and (member.nullable or type_nullable):
|
|
128
|
+
hooks = module.imported("seampact.hooks", "_hooks")
|
|
129
|
+
return f"{INDENT}{attribute}: {hooks}.Field[{annotation}] = {hooks}.Absent.ABSENT"
|
|
130
|
+
|
|
131
|
+
return f"{INDENT}{attribute}: {annotation}{' = None' if member.optional else ''}"
|