sera-2 1.9.0__py3-none-any.whl → 1.11.1__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.
- sera/libs/api_helper.py +81 -0
- sera/libs/base_service.py +18 -2
- sera/libs/middlewares/__init__.py +0 -0
- sera/libs/middlewares/auth.py +73 -0
- sera/libs/middlewares/uscp.py +68 -0
- sera/make/make_python_api.py +121 -221
- sera/make/make_python_model.py +124 -8
- sera/models/_parse.py +20 -16
- sera/models/_property.py +3 -0
- {sera_2-1.9.0.dist-info → sera_2-1.11.1.dist-info}/METADATA +1 -1
- {sera_2-1.9.0.dist-info → sera_2-1.11.1.dist-info}/RECORD +12 -9
- {sera_2-1.9.0.dist-info → sera_2-1.11.1.dist-info}/WHEEL +0 -0
sera/libs/api_helper.py
CHANGED
@@ -1,10 +1,22 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import re
|
4
|
+
from typing import Collection, Generic, cast
|
4
5
|
|
5
6
|
from litestar import Request, status_codes
|
7
|
+
from litestar.connection import ASGIConnection
|
8
|
+
from litestar.dto import MsgspecDTO
|
9
|
+
from litestar.dto._backend import DTOBackend
|
10
|
+
from litestar.dto._codegen_backend import DTOCodegenBackend
|
11
|
+
from litestar.enums import RequestEncodingType
|
6
12
|
from litestar.exceptions import HTTPException
|
13
|
+
from litestar.serialization import decode_json, decode_msgpack
|
14
|
+
from litestar.typing import FieldDefinition
|
15
|
+
from msgspec import Struct
|
16
|
+
|
7
17
|
from sera.libs.base_service import Query, QueryOp
|
18
|
+
from sera.libs.middlewares.uscp import STATE_SYSTEM_CONTROLLED_PROP_KEY
|
19
|
+
from sera.typing import T
|
8
20
|
|
9
21
|
# for parsing field names and operations from query string
|
10
22
|
FIELD_REG = re.compile(r"(?P<name>[a-zA-Z_0-9]+)(?:\[(?P<op>[a-zA-Z0-9]+)\])?")
|
@@ -64,3 +76,72 @@ def parse_query(request: Request, fields: set[str], debug: bool) -> Query:
|
|
64
76
|
continue
|
65
77
|
|
66
78
|
return query
|
79
|
+
|
80
|
+
|
81
|
+
class SingleAutoUSCP(MsgspecDTO[T], Generic[T]):
|
82
|
+
"""Auto Update System Controlled Property DTO"""
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def create_for_field_definition(
|
86
|
+
cls,
|
87
|
+
field_definition: FieldDefinition,
|
88
|
+
handler_id: str,
|
89
|
+
backend_cls: type[DTOBackend] | None = None,
|
90
|
+
) -> None:
|
91
|
+
assert backend_cls is None, "Custom backend not supported"
|
92
|
+
super().create_for_field_definition(
|
93
|
+
field_definition, handler_id, FixedDTOBackend
|
94
|
+
)
|
95
|
+
|
96
|
+
def decode_bytes(self, value: bytes):
|
97
|
+
"""Decode a byte string into an object"""
|
98
|
+
backend = self._dto_backends[self.asgi_connection.route_handler.handler_id][
|
99
|
+
"data_backend"
|
100
|
+
] # pyright: ignore
|
101
|
+
obj = backend.populate_data_from_raw(value, self.asgi_connection)
|
102
|
+
obj.update_system_controlled_props(
|
103
|
+
self.asgi_connection.scope["state"][STATE_SYSTEM_CONTROLLED_PROP_KEY]
|
104
|
+
)
|
105
|
+
return obj
|
106
|
+
|
107
|
+
|
108
|
+
class FixedDTOBackend(DTOCodegenBackend):
|
109
|
+
def parse_raw(
|
110
|
+
self, raw: bytes, asgi_connection: ASGIConnection
|
111
|
+
) -> Struct | Collection[Struct]:
|
112
|
+
"""Parse raw bytes into transfer model type.
|
113
|
+
|
114
|
+
Note: instead of decoding into self.annotation, which I encounter this error: https://github.com/litestar-org/litestar/issues/4181; we have to use self.model_type, which is the original type.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
raw: bytes
|
118
|
+
asgi_connection: The current ASGI Connection
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
The raw bytes parsed into transfer model type.
|
122
|
+
"""
|
123
|
+
request_encoding = RequestEncodingType.JSON
|
124
|
+
|
125
|
+
if (content_type := getattr(asgi_connection, "content_type", None)) and (
|
126
|
+
media_type := content_type[0]
|
127
|
+
):
|
128
|
+
request_encoding = media_type
|
129
|
+
|
130
|
+
type_decoders = asgi_connection.route_handler.resolve_type_decoders()
|
131
|
+
|
132
|
+
if request_encoding == RequestEncodingType.MESSAGEPACK:
|
133
|
+
result = decode_msgpack(
|
134
|
+
value=raw,
|
135
|
+
target_type=self.model_type,
|
136
|
+
type_decoders=type_decoders,
|
137
|
+
strict=False,
|
138
|
+
)
|
139
|
+
else:
|
140
|
+
result = decode_json(
|
141
|
+
value=raw,
|
142
|
+
target_type=self.model_type,
|
143
|
+
type_decoders=type_decoders,
|
144
|
+
strict=False,
|
145
|
+
)
|
146
|
+
|
147
|
+
return cast("Struct | Collection[Struct]", result)
|
sera/libs/base_service.py
CHANGED
@@ -4,12 +4,13 @@ from enum import Enum
|
|
4
4
|
from math import dist
|
5
5
|
from typing import Annotated, Any, Generic, NamedTuple, Optional, Sequence, TypeVar
|
6
6
|
|
7
|
+
from sqlalchemy import Result, Select, exists, func, select
|
8
|
+
from sqlalchemy.orm import Session, load_only
|
9
|
+
|
7
10
|
from sera.libs.base_orm import BaseORM
|
8
11
|
from sera.misc import assert_not_null
|
9
12
|
from sera.models import Class
|
10
13
|
from sera.typing import FieldName, T, doc
|
11
|
-
from sqlalchemy import Result, Select, exists, func, select
|
12
|
-
from sqlalchemy.orm import Session, load_only
|
13
14
|
|
14
15
|
|
15
16
|
class QueryOp(str, Enum):
|
@@ -42,12 +43,24 @@ class QueryResult(NamedTuple, Generic[R]):
|
|
42
43
|
|
43
44
|
class BaseService(Generic[ID, R]):
|
44
45
|
|
46
|
+
instance = None
|
47
|
+
|
45
48
|
def __init__(self, cls: Class, orm_cls: type[R]):
|
46
49
|
self.cls = cls
|
47
50
|
self.orm_cls = orm_cls
|
48
51
|
self.id_prop = assert_not_null(cls.get_id_property())
|
49
52
|
|
50
53
|
self._cls_id_prop = getattr(self.orm_cls, self.id_prop.name)
|
54
|
+
self.is_id_auto_increment = self.id_prop.db.is_auto_increment
|
55
|
+
|
56
|
+
@classmethod
|
57
|
+
def get_instance(cls):
|
58
|
+
"""Get the singleton instance of the service."""
|
59
|
+
if cls.instance is None:
|
60
|
+
# assume that the subclass overrides the __init__ method
|
61
|
+
# so that we don't need to pass the class and orm_cls
|
62
|
+
cls.instance = cls()
|
63
|
+
return cls.instance
|
51
64
|
|
52
65
|
def get(
|
53
66
|
self,
|
@@ -108,6 +121,9 @@ class BaseService(Generic[ID, R]):
|
|
108
121
|
|
109
122
|
def create(self, record: R, session: Session) -> R:
|
110
123
|
"""Create a new record."""
|
124
|
+
if self.is_id_auto_increment:
|
125
|
+
setattr(record, self.id_prop.name, None)
|
126
|
+
|
111
127
|
session.add(record)
|
112
128
|
session.commit()
|
113
129
|
return record
|
File without changes
|
@@ -0,0 +1,73 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from datetime import datetime, timezone
|
4
|
+
from typing import Callable, Generator, Generic, Optional, Sequence, Type
|
5
|
+
|
6
|
+
from litestar import Request
|
7
|
+
from litestar.connection import ASGIConnection
|
8
|
+
from litestar.exceptions import NotAuthorizedException
|
9
|
+
from litestar.middleware import AbstractAuthenticationMiddleware, AuthenticationResult
|
10
|
+
from litestar.types import ASGIApp, Method, Scopes
|
11
|
+
from litestar.types.composite_types import Dependencies
|
12
|
+
from sqlalchemy import select
|
13
|
+
from sqlalchemy.orm import Session
|
14
|
+
|
15
|
+
from sera.typing import T
|
16
|
+
|
17
|
+
|
18
|
+
class AuthMiddleware(AbstractAuthenticationMiddleware, Generic[T]):
|
19
|
+
"""
|
20
|
+
Middleware to handle authentication for the API.
|
21
|
+
|
22
|
+
This middleware checks if the user is authenticated to access
|
23
|
+
the requested resource. If not, it raises an HTTPException with a 401 status code.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(
|
27
|
+
self,
|
28
|
+
app: ASGIApp,
|
29
|
+
user_handler: Callable[[str], T],
|
30
|
+
exclude: str | list[str] | None = None,
|
31
|
+
exclude_from_auth_key: str = "exclude_from_auth",
|
32
|
+
exclude_http_methods: Sequence[Method] | None = None,
|
33
|
+
scopes: Scopes | None = None,
|
34
|
+
) -> None:
|
35
|
+
super().__init__(
|
36
|
+
app=app,
|
37
|
+
exclude=exclude,
|
38
|
+
exclude_from_auth_key=exclude_from_auth_key,
|
39
|
+
exclude_http_methods=exclude_http_methods,
|
40
|
+
scopes=scopes,
|
41
|
+
)
|
42
|
+
self.user_handler = user_handler
|
43
|
+
|
44
|
+
async def authenticate_request(
|
45
|
+
self, connection: ASGIConnection
|
46
|
+
) -> AuthenticationResult:
|
47
|
+
# do something here.
|
48
|
+
if "userid" not in connection.session:
|
49
|
+
raise NotAuthorizedException(
|
50
|
+
detail="Invalid credentials",
|
51
|
+
)
|
52
|
+
|
53
|
+
userid: str = connection.session["userid"]
|
54
|
+
expired_at = datetime.fromtimestamp(
|
55
|
+
connection.session.get("exp", 0), timezone.utc
|
56
|
+
)
|
57
|
+
if expired_at < datetime.now(timezone.utc):
|
58
|
+
raise NotAuthorizedException(
|
59
|
+
detail="Credentials expired",
|
60
|
+
)
|
61
|
+
|
62
|
+
user = self.user_handler(userid)
|
63
|
+
if user is None:
|
64
|
+
raise NotAuthorizedException(
|
65
|
+
detail="User not found",
|
66
|
+
)
|
67
|
+
|
68
|
+
return AuthenticationResult(user, None)
|
69
|
+
|
70
|
+
@staticmethod
|
71
|
+
def save(req: Request, user: T, expired_at: datetime) -> None:
|
72
|
+
req.session["userid"] = user.id
|
73
|
+
req.session["exp"] = expired_at.timestamp()
|
@@ -0,0 +1,68 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Callable
|
4
|
+
|
5
|
+
from litestar.connection.base import UserT
|
6
|
+
from litestar.middleware import AbstractMiddleware
|
7
|
+
from litestar.types import ASGIApp, Message, Receive, Scope, Scopes, Send
|
8
|
+
|
9
|
+
STATE_SYSTEM_CONTROLLED_PROP_KEY = "system_controlled_properties"
|
10
|
+
|
11
|
+
|
12
|
+
class USCPMiddleware(AbstractMiddleware):
|
13
|
+
"""
|
14
|
+
Middleware to update system-controlled properties in the request.
|
15
|
+
|
16
|
+
This middleware updates the `created_at`, `updated_at`, and `deleted_at` properties
|
17
|
+
of the request with the current timestamp. It is intended to be used in a Litestar
|
18
|
+
application.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
app: ASGIApp,
|
24
|
+
get_system_controlled_props: Callable[[UserT], dict],
|
25
|
+
skip_update_system_controlled_props: Callable[[UserT], bool],
|
26
|
+
exclude: str | list[str] | None = None,
|
27
|
+
exclude_opt_key: str | None = None,
|
28
|
+
scopes: Scopes | None = None,
|
29
|
+
) -> None:
|
30
|
+
"""Initialize the middleware.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
app: The ``next`` ASGI app to call.
|
34
|
+
exclude: A pattern or list of patterns to match against a request's path.
|
35
|
+
If a match is found, the middleware will be skipped.
|
36
|
+
exclude_opt_key: An identifier that is set in the route handler
|
37
|
+
``opt`` key which allows skipping the middleware.
|
38
|
+
scopes: ASGI scope types, should be a set including
|
39
|
+
either or both 'ScopeType.HTTP' and 'ScopeType.WEBSOCKET'.
|
40
|
+
"""
|
41
|
+
super().__init__(app, exclude, exclude_opt_key, scopes)
|
42
|
+
self.get_system_controlled_props = get_system_controlled_props
|
43
|
+
self.skip_update_system_controlled_props = skip_update_system_controlled_props
|
44
|
+
|
45
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
46
|
+
user = scope["user"]
|
47
|
+
if self.skip_update_system_controlled_props(user):
|
48
|
+
scope["state"][STATE_SYSTEM_CONTROLLED_PROP_KEY] = None
|
49
|
+
else:
|
50
|
+
scope["state"][STATE_SYSTEM_CONTROLLED_PROP_KEY] = (
|
51
|
+
self.get_system_controlled_props(user)
|
52
|
+
)
|
53
|
+
await self.app(scope, receive, send)
|
54
|
+
|
55
|
+
|
56
|
+
def get_scp_from_user(user: UserT, fields: dict[str, str]) -> dict:
|
57
|
+
"""Get system-controlled properties from the user.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
user: The user object.
|
61
|
+
fields: A list of fields to include in the system-controlled properties.
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
A dictionary containing the system-controlled properties.
|
65
|
+
"""
|
66
|
+
return {
|
67
|
+
data_field: getattr(user, db_field) for data_field, db_field in fields.items()
|
68
|
+
}
|
sera/make/make_python_api.py
CHANGED
@@ -14,11 +14,9 @@ def make_python_api(app: App, collections: Sequence[DataCollection]):
|
|
14
14
|
app.api.ensure_exists()
|
15
15
|
app.api.pkg("routes").ensure_exists()
|
16
16
|
|
17
|
-
# make routes
|
18
|
-
dep_pkg = app.api.pkg("dependencies")
|
17
|
+
# make routes
|
19
18
|
routes: list[Module] = []
|
20
19
|
for collection in collections:
|
21
|
-
make_dependency(collection, dep_pkg)
|
22
20
|
route = app.api.pkg("routes").pkg(collection.get_pymodule_name())
|
23
21
|
|
24
22
|
controllers = []
|
@@ -107,64 +105,20 @@ def make_main(target_pkg: Package, routes: Sequence[Module]):
|
|
107
105
|
outmod.write(program)
|
108
106
|
|
109
107
|
|
110
|
-
def make_dependency(collection: DataCollection, target_pkg: Package):
|
111
|
-
"""Generate dependency injection for the service."""
|
112
|
-
app = target_pkg.app
|
113
|
-
|
114
|
-
outmod = target_pkg.module(collection.get_pymodule_name())
|
115
|
-
if outmod.exists():
|
116
|
-
logger.info("`{}` already exists. Skip generation.", outmod.path)
|
117
|
-
return
|
118
|
-
|
119
|
-
ServiceNameDep = to_snake_case(f"{collection.name}ServiceDependency")
|
120
|
-
|
121
|
-
program = Program()
|
122
|
-
program.import_("__future__.annotations", True)
|
123
|
-
program.import_(
|
124
|
-
app.services.path
|
125
|
-
+ f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
|
126
|
-
True,
|
127
|
-
)
|
128
|
-
|
129
|
-
program.root(
|
130
|
-
stmt.LineBreak(),
|
131
|
-
lambda ast: ast.func(
|
132
|
-
ServiceNameDep,
|
133
|
-
[],
|
134
|
-
expr.ExprIdent(collection.get_service_name()),
|
135
|
-
is_async=True,
|
136
|
-
)(
|
137
|
-
lambda ast01: ast01.return_(
|
138
|
-
expr.ExprFuncCall(expr.ExprIdent(collection.get_service_name()), [])
|
139
|
-
)
|
140
|
-
),
|
141
|
-
)
|
142
|
-
outmod.write(program)
|
143
|
-
|
144
|
-
|
145
108
|
def make_python_get_api(
|
146
109
|
collection: DataCollection, target_pkg: Package
|
147
110
|
) -> tuple[Module, str]:
|
148
111
|
"""Make an endpoint for querying resources"""
|
149
112
|
app = target_pkg.app
|
150
113
|
|
151
|
-
ServiceNameDep = to_snake_case(f"{collection.name}ServiceDependency")
|
152
|
-
|
153
114
|
program = Program()
|
154
115
|
program.import_("__future__.annotations", True)
|
155
116
|
program.import_("typing.Annotated", True)
|
156
|
-
program.import_("typing.Sequence", True)
|
157
117
|
program.import_("litestar.get", True)
|
158
118
|
program.import_("litestar.Request", True)
|
159
119
|
program.import_("litestar.params.Parameter", True)
|
160
|
-
program.import_(app.models.db.path + ".base.get_session", True)
|
161
|
-
program.import_("litestar.di.Provide", True)
|
162
120
|
program.import_("sqlalchemy.orm.Session", True)
|
163
121
|
program.import_(app.config.path + ".API_DEBUG", True)
|
164
|
-
program.import_(
|
165
|
-
f"{app.api.path}.dependencies.{collection.get_pymodule_name()}.{ServiceNameDep}",
|
166
|
-
True,
|
167
|
-
)
|
168
122
|
program.import_(
|
169
123
|
app.services.path
|
170
124
|
+ f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
|
@@ -189,21 +143,6 @@ def make_python_get_api(
|
|
189
143
|
expr.ExprIdent("get"),
|
190
144
|
[
|
191
145
|
expr.ExprConstant("/"),
|
192
|
-
PredefinedFn.keyword_assignment(
|
193
|
-
"dependencies",
|
194
|
-
PredefinedFn.dict(
|
195
|
-
[
|
196
|
-
(
|
197
|
-
expr.ExprConstant("service"),
|
198
|
-
expr.ExprIdent(f"Provide({ServiceNameDep})"),
|
199
|
-
),
|
200
|
-
(
|
201
|
-
expr.ExprConstant("session"),
|
202
|
-
expr.ExprIdent(f"Provide(get_session)"),
|
203
|
-
),
|
204
|
-
]
|
205
|
-
),
|
206
|
-
),
|
207
146
|
],
|
208
147
|
)
|
209
148
|
),
|
@@ -250,10 +189,6 @@ def make_python_get_api(
|
|
250
189
|
"request",
|
251
190
|
expr.ExprIdent("Request"),
|
252
191
|
),
|
253
|
-
DeferredVar.simple(
|
254
|
-
"service",
|
255
|
-
expr.ExprIdent(collection.get_service_name()),
|
256
|
-
),
|
257
192
|
DeferredVar.simple(
|
258
193
|
"session",
|
259
194
|
expr.ExprIdent("Session"),
|
@@ -265,7 +200,17 @@ def make_python_get_api(
|
|
265
200
|
stmt.SingleExprStatement(
|
266
201
|
expr.ExprConstant("Retrieving records matched a query")
|
267
202
|
),
|
268
|
-
lambda
|
203
|
+
lambda ast100: ast100.assign(
|
204
|
+
DeferredVar.simple("service"),
|
205
|
+
expr.ExprFuncCall(
|
206
|
+
PredefinedFn.attr_getter(
|
207
|
+
expr.ExprIdent(collection.get_service_name()),
|
208
|
+
expr.ExprIdent("get_instance"),
|
209
|
+
),
|
210
|
+
[],
|
211
|
+
),
|
212
|
+
),
|
213
|
+
lambda ast101: ast101.assign(
|
269
214
|
DeferredVar.simple("query", expr.ExprIdent("ServiceQuery")),
|
270
215
|
expr.ExprFuncCall(
|
271
216
|
expr.ExprIdent("parse_query"),
|
@@ -279,10 +224,13 @@ def make_python_get_api(
|
|
279
224
|
],
|
280
225
|
),
|
281
226
|
),
|
282
|
-
lambda
|
227
|
+
lambda ast102: ast102.assign(
|
283
228
|
DeferredVar.simple("result"),
|
284
229
|
expr.ExprFuncCall(
|
285
|
-
|
230
|
+
PredefinedFn.attr_getter(
|
231
|
+
expr.ExprIdent("service"),
|
232
|
+
expr.ExprIdent("get"),
|
233
|
+
),
|
286
234
|
[
|
287
235
|
expr.ExprIdent("query"),
|
288
236
|
PredefinedFn.keyword_assignment(
|
@@ -309,7 +257,7 @@ def make_python_get_api(
|
|
309
257
|
],
|
310
258
|
),
|
311
259
|
),
|
312
|
-
lambda
|
260
|
+
lambda ast103: ast103.return_(
|
313
261
|
PredefinedFn.dict(
|
314
262
|
[
|
315
263
|
(
|
@@ -359,13 +307,7 @@ def make_python_get_by_id_api(
|
|
359
307
|
program.import_("litestar.get", True)
|
360
308
|
program.import_("litestar.status_codes", True)
|
361
309
|
program.import_("litestar.exceptions.HTTPException", True)
|
362
|
-
program.import_("litestar.di.Provide", True)
|
363
310
|
program.import_("sqlalchemy.orm.Session", True)
|
364
|
-
program.import_(app.models.db.path + ".base.get_session", True)
|
365
|
-
program.import_(
|
366
|
-
f"{app.api.path}.dependencies.{collection.get_pymodule_name()}.{ServiceNameDep}",
|
367
|
-
True,
|
368
|
-
)
|
369
311
|
program.import_(
|
370
312
|
app.services.path
|
371
313
|
+ f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
|
@@ -388,21 +330,6 @@ def make_python_get_by_id_api(
|
|
388
330
|
expr.ExprIdent("get"),
|
389
331
|
[
|
390
332
|
expr.ExprConstant("/{id:%s}" % id_type),
|
391
|
-
PredefinedFn.keyword_assignment(
|
392
|
-
"dependencies",
|
393
|
-
PredefinedFn.dict(
|
394
|
-
[
|
395
|
-
(
|
396
|
-
expr.ExprConstant("service"),
|
397
|
-
expr.ExprIdent(f"Provide({ServiceNameDep})"),
|
398
|
-
),
|
399
|
-
(
|
400
|
-
expr.ExprConstant("session"),
|
401
|
-
expr.ExprIdent(f"Provide(get_session)"),
|
402
|
-
),
|
403
|
-
]
|
404
|
-
),
|
405
|
-
),
|
406
333
|
],
|
407
334
|
)
|
408
335
|
),
|
@@ -413,10 +340,6 @@ def make_python_get_by_id_api(
|
|
413
340
|
"id",
|
414
341
|
expr.ExprIdent(id_type),
|
415
342
|
),
|
416
|
-
DeferredVar.simple(
|
417
|
-
"service",
|
418
|
-
expr.ExprIdent(collection.get_service_name()),
|
419
|
-
),
|
420
343
|
DeferredVar.simple(
|
421
344
|
"session",
|
422
345
|
expr.ExprIdent("Session"),
|
@@ -426,6 +349,16 @@ def make_python_get_by_id_api(
|
|
426
349
|
is_async=True,
|
427
350
|
)(
|
428
351
|
stmt.SingleExprStatement(expr.ExprConstant("Retrieving record by id")),
|
352
|
+
lambda ast100: ast100.assign(
|
353
|
+
DeferredVar.simple("service"),
|
354
|
+
expr.ExprFuncCall(
|
355
|
+
PredefinedFn.attr_getter(
|
356
|
+
expr.ExprIdent(collection.get_service_name()),
|
357
|
+
expr.ExprIdent("get_instance"),
|
358
|
+
),
|
359
|
+
[],
|
360
|
+
),
|
361
|
+
),
|
429
362
|
lambda ast11: ast11.assign(
|
430
363
|
DeferredVar.simple("record"),
|
431
364
|
expr.ExprFuncCall(
|
@@ -483,22 +416,12 @@ def make_python_has_api(
|
|
483
416
|
program.import_("litestar.head", True)
|
484
417
|
program.import_("litestar.status_codes", True)
|
485
418
|
program.import_("litestar.exceptions.HTTPException", True)
|
486
|
-
program.import_("litestar.di.Provide", True)
|
487
419
|
program.import_("sqlalchemy.orm.Session", True)
|
488
|
-
program.import_(app.models.db.path + ".base.get_session", True)
|
489
|
-
program.import_(
|
490
|
-
f"{app.api.path}.dependencies.{collection.get_pymodule_name()}.{ServiceNameDep}",
|
491
|
-
True,
|
492
|
-
)
|
493
420
|
program.import_(
|
494
421
|
app.services.path
|
495
422
|
+ f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
|
496
423
|
True,
|
497
424
|
)
|
498
|
-
program.import_(
|
499
|
-
app.models.data.path + f".{collection.get_pymodule_name()}.{collection.name}",
|
500
|
-
True,
|
501
|
-
)
|
502
425
|
|
503
426
|
# assuming the collection has only one class
|
504
427
|
cls = collection.cls
|
@@ -516,21 +439,6 @@ def make_python_has_api(
|
|
516
439
|
"status_code",
|
517
440
|
expr.ExprIdent("status_codes.HTTP_204_NO_CONTENT"),
|
518
441
|
),
|
519
|
-
PredefinedFn.keyword_assignment(
|
520
|
-
"dependencies",
|
521
|
-
PredefinedFn.dict(
|
522
|
-
[
|
523
|
-
(
|
524
|
-
expr.ExprConstant("service"),
|
525
|
-
expr.ExprIdent(f"Provide({ServiceNameDep})"),
|
526
|
-
),
|
527
|
-
(
|
528
|
-
expr.ExprConstant("session"),
|
529
|
-
expr.ExprIdent(f"Provide(get_session)"),
|
530
|
-
),
|
531
|
-
]
|
532
|
-
),
|
533
|
-
),
|
534
442
|
],
|
535
443
|
)
|
536
444
|
),
|
@@ -541,10 +449,6 @@ def make_python_has_api(
|
|
541
449
|
"id",
|
542
450
|
expr.ExprIdent(id_type),
|
543
451
|
),
|
544
|
-
DeferredVar.simple(
|
545
|
-
"service",
|
546
|
-
expr.ExprIdent(collection.get_service_name()),
|
547
|
-
),
|
548
452
|
DeferredVar.simple(
|
549
453
|
"session",
|
550
454
|
expr.ExprIdent("Session"),
|
@@ -554,6 +458,16 @@ def make_python_has_api(
|
|
554
458
|
is_async=True,
|
555
459
|
)(
|
556
460
|
stmt.SingleExprStatement(expr.ExprConstant("Retrieving record by id")),
|
461
|
+
lambda ast100: ast100.assign(
|
462
|
+
DeferredVar.simple("service"),
|
463
|
+
expr.ExprFuncCall(
|
464
|
+
PredefinedFn.attr_getter(
|
465
|
+
expr.ExprIdent(collection.get_service_name()),
|
466
|
+
expr.ExprIdent("get_instance"),
|
467
|
+
),
|
468
|
+
[],
|
469
|
+
),
|
470
|
+
),
|
557
471
|
lambda ast11: ast11.assign(
|
558
472
|
DeferredVar.simple("record_exist"),
|
559
473
|
expr.ExprFuncCall(
|
@@ -595,27 +509,15 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
|
|
595
509
|
"""Make an endpoint for creating a resource"""
|
596
510
|
app = target_pkg.app
|
597
511
|
|
598
|
-
ServiceNameDep = to_snake_case(f"{collection.name}ServiceDependency")
|
599
|
-
|
600
512
|
program = Program()
|
601
513
|
program.import_("__future__.annotations", True)
|
602
514
|
program.import_("litestar.post", True)
|
603
|
-
program.import_("litestar.di.Provide", True)
|
604
515
|
program.import_("sqlalchemy.orm.Session", True)
|
605
|
-
program.import_(app.models.db.path + ".base.get_session", True)
|
606
|
-
program.import_(
|
607
|
-
f"{app.api.path}.dependencies.{collection.get_pymodule_name()}.{ServiceNameDep}",
|
608
|
-
True,
|
609
|
-
)
|
610
516
|
program.import_(
|
611
517
|
app.services.path
|
612
518
|
+ f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
|
613
519
|
True,
|
614
520
|
)
|
615
|
-
program.import_(
|
616
|
-
app.models.data.path + f".{collection.get_pymodule_name()}.{collection.name}",
|
617
|
-
True,
|
618
|
-
)
|
619
521
|
program.import_(
|
620
522
|
app.models.data.path
|
621
523
|
+ f".{collection.get_pymodule_name()}.Upsert{collection.name}",
|
@@ -624,7 +526,13 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
|
|
624
526
|
|
625
527
|
# assuming the collection has only one class
|
626
528
|
cls = collection.cls
|
627
|
-
|
529
|
+
has_system_controlled_prop = any(
|
530
|
+
prop.data.is_system_controlled for prop in cls.properties.values()
|
531
|
+
)
|
532
|
+
idprop = assert_not_null(cls.get_id_property())
|
533
|
+
|
534
|
+
if has_system_controlled_prop:
|
535
|
+
program.import_("sera.libs.api_helper.SingleAutoUSCP", True)
|
628
536
|
|
629
537
|
func_name = "create"
|
630
538
|
|
@@ -635,22 +543,20 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
|
|
635
543
|
expr.ExprIdent("post"),
|
636
544
|
[
|
637
545
|
expr.ExprConstant("/"),
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
),
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
),
|
653
|
-
],
|
546
|
+
]
|
547
|
+
+ (
|
548
|
+
[
|
549
|
+
PredefinedFn.keyword_assignment(
|
550
|
+
"dto",
|
551
|
+
PredefinedFn.item_getter(
|
552
|
+
expr.ExprIdent("SingleAutoUSCP"),
|
553
|
+
expr.ExprIdent(f"Upsert{cls.name}"),
|
554
|
+
),
|
555
|
+
)
|
556
|
+
]
|
557
|
+
if has_system_controlled_prop
|
558
|
+
else []
|
559
|
+
),
|
654
560
|
)
|
655
561
|
),
|
656
562
|
lambda ast10: ast10.func(
|
@@ -660,35 +566,36 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
|
|
660
566
|
"data",
|
661
567
|
expr.ExprIdent(f"Upsert{cls.name}"),
|
662
568
|
),
|
663
|
-
DeferredVar.simple(
|
664
|
-
"service",
|
665
|
-
expr.ExprIdent(collection.get_service_name()),
|
666
|
-
),
|
667
569
|
DeferredVar.simple(
|
668
570
|
"session",
|
669
571
|
expr.ExprIdent("Session"),
|
670
572
|
),
|
671
573
|
],
|
672
|
-
return_type=expr.ExprIdent(
|
574
|
+
return_type=expr.ExprIdent(idprop.datatype.get_python_type().type),
|
673
575
|
is_async=True,
|
674
576
|
)(
|
675
577
|
stmt.SingleExprStatement(expr.ExprConstant("Creating new record")),
|
578
|
+
lambda ast100: ast100.assign(
|
579
|
+
DeferredVar.simple("service"),
|
580
|
+
expr.ExprFuncCall(
|
581
|
+
PredefinedFn.attr_getter(
|
582
|
+
expr.ExprIdent(collection.get_service_name()),
|
583
|
+
expr.ExprIdent("get_instance"),
|
584
|
+
),
|
585
|
+
[],
|
586
|
+
),
|
587
|
+
),
|
676
588
|
lambda ast13: ast13.return_(
|
677
|
-
|
678
|
-
expr.
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
expr.ExprIdent("
|
683
|
-
"
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
),
|
688
|
-
expr.ExprIdent("session"),
|
689
|
-
],
|
690
|
-
)
|
691
|
-
],
|
589
|
+
PredefinedFn.attr_getter(
|
590
|
+
expr.ExprMethodCall(
|
591
|
+
expr.ExprIdent("service"),
|
592
|
+
"create",
|
593
|
+
[
|
594
|
+
expr.ExprMethodCall(expr.ExprIdent("data"), "to_db", []),
|
595
|
+
expr.ExprIdent("session"),
|
596
|
+
],
|
597
|
+
),
|
598
|
+
expr.ExprIdent(idprop.name),
|
692
599
|
)
|
693
600
|
),
|
694
601
|
),
|
@@ -704,27 +611,15 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
|
|
704
611
|
"""Make an endpoint for updating resource"""
|
705
612
|
app = target_pkg.app
|
706
613
|
|
707
|
-
ServiceNameDep = to_snake_case(f"{collection.name}ServiceDependency")
|
708
|
-
|
709
614
|
program = Program()
|
710
615
|
program.import_("__future__.annotations", True)
|
711
616
|
program.import_("litestar.put", True)
|
712
|
-
program.import_("litestar.di.Provide", True)
|
713
617
|
program.import_("sqlalchemy.orm.Session", True)
|
714
|
-
program.import_(app.models.db.path + ".base.get_session", True)
|
715
|
-
program.import_(
|
716
|
-
f"{app.api.path}.dependencies.{collection.get_pymodule_name()}.{ServiceNameDep}",
|
717
|
-
True,
|
718
|
-
)
|
719
618
|
program.import_(
|
720
619
|
app.services.path
|
721
620
|
+ f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
|
722
621
|
True,
|
723
622
|
)
|
724
|
-
program.import_(
|
725
|
-
app.models.data.path + f".{collection.get_pymodule_name()}.{collection.name}",
|
726
|
-
True,
|
727
|
-
)
|
728
623
|
program.import_(
|
729
624
|
app.models.data.path
|
730
625
|
+ f".{collection.get_pymodule_name()}.Upsert{collection.name}",
|
@@ -736,6 +631,12 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
|
|
736
631
|
id_prop = assert_not_null(cls.get_id_property())
|
737
632
|
id_type = id_prop.datatype.get_python_type().type
|
738
633
|
|
634
|
+
has_system_controlled_prop = any(
|
635
|
+
prop.data.is_system_controlled for prop in cls.properties.values()
|
636
|
+
)
|
637
|
+
if has_system_controlled_prop:
|
638
|
+
program.import_("sera.libs.api_helper.SingleAutoUSCP", True)
|
639
|
+
|
739
640
|
func_name = "update"
|
740
641
|
|
741
642
|
program.root(
|
@@ -745,22 +646,20 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
|
|
745
646
|
expr.ExprIdent("put"),
|
746
647
|
[
|
747
648
|
expr.ExprConstant("/{id:%s}" % id_type),
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
),
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
),
|
763
|
-
],
|
649
|
+
]
|
650
|
+
+ (
|
651
|
+
[
|
652
|
+
PredefinedFn.keyword_assignment(
|
653
|
+
"dto",
|
654
|
+
PredefinedFn.item_getter(
|
655
|
+
expr.ExprIdent("SingleAutoUSCP"),
|
656
|
+
expr.ExprIdent(f"Upsert{cls.name}"),
|
657
|
+
),
|
658
|
+
)
|
659
|
+
]
|
660
|
+
if has_system_controlled_prop
|
661
|
+
else []
|
662
|
+
),
|
764
663
|
)
|
765
664
|
),
|
766
665
|
lambda ast10: ast10.func(
|
@@ -774,19 +673,25 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
|
|
774
673
|
"data",
|
775
674
|
expr.ExprIdent(f"Upsert{cls.name}"),
|
776
675
|
),
|
777
|
-
DeferredVar.simple(
|
778
|
-
"service",
|
779
|
-
expr.ExprIdent(collection.get_service_name()),
|
780
|
-
),
|
781
676
|
DeferredVar.simple(
|
782
677
|
"session",
|
783
678
|
expr.ExprIdent("Session"),
|
784
679
|
),
|
785
680
|
],
|
786
|
-
return_type=expr.ExprIdent(
|
681
|
+
return_type=expr.ExprIdent(id_prop.datatype.get_python_type().type),
|
787
682
|
is_async=True,
|
788
683
|
)(
|
789
684
|
stmt.SingleExprStatement(expr.ExprConstant("Update an existing record")),
|
685
|
+
lambda ast100: ast100.assign(
|
686
|
+
DeferredVar.simple("service"),
|
687
|
+
expr.ExprFuncCall(
|
688
|
+
PredefinedFn.attr_getter(
|
689
|
+
expr.ExprIdent(collection.get_service_name()),
|
690
|
+
expr.ExprIdent("get_instance"),
|
691
|
+
),
|
692
|
+
[],
|
693
|
+
),
|
694
|
+
),
|
790
695
|
stmt.SingleExprStatement(
|
791
696
|
PredefinedFn.attr_setter(
|
792
697
|
expr.ExprIdent("data"),
|
@@ -795,21 +700,16 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
|
|
795
700
|
)
|
796
701
|
),
|
797
702
|
lambda ast13: ast13.return_(
|
798
|
-
|
799
|
-
expr.
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
expr.ExprIdent("
|
804
|
-
"
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
),
|
809
|
-
expr.ExprIdent("session"),
|
810
|
-
],
|
811
|
-
)
|
812
|
-
],
|
703
|
+
PredefinedFn.attr_getter(
|
704
|
+
expr.ExprMethodCall(
|
705
|
+
expr.ExprIdent("service"),
|
706
|
+
"update",
|
707
|
+
[
|
708
|
+
expr.ExprMethodCall(expr.ExprIdent("data"), "to_db", []),
|
709
|
+
expr.ExprIdent("session"),
|
710
|
+
],
|
711
|
+
),
|
712
|
+
expr.ExprIdent(id_prop.name),
|
813
713
|
)
|
814
714
|
),
|
815
715
|
),
|
sera/make/make_python_model.py
CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
3
3
|
from typing import Callable, Sequence
|
4
4
|
|
5
5
|
from codegen.models import AST, DeferredVar, PredefinedFn, Program, expr, stmt
|
6
|
+
|
6
7
|
from sera.misc import (
|
7
8
|
assert_isinstance,
|
8
9
|
assert_not_null,
|
@@ -189,6 +190,37 @@ def make_python_data_model(
|
|
189
190
|
True,
|
190
191
|
alias=f"{cls.name}DB",
|
191
192
|
)
|
193
|
+
|
194
|
+
has_system_controlled = any(
|
195
|
+
prop.data.is_system_controlled for prop in cls.properties.values()
|
196
|
+
)
|
197
|
+
if has_system_controlled:
|
198
|
+
program.import_("typing.TypedDict", True)
|
199
|
+
program.root(
|
200
|
+
stmt.LineBreak(),
|
201
|
+
lambda ast: ast.class_(
|
202
|
+
"SystemControlledProps",
|
203
|
+
[expr.ExprIdent("TypedDict")],
|
204
|
+
)(
|
205
|
+
*[
|
206
|
+
stmt.DefClassVarStatement(
|
207
|
+
prop.name,
|
208
|
+
(
|
209
|
+
prop.get_data_model_datatype().get_python_type().type
|
210
|
+
if isinstance(prop, DataProperty)
|
211
|
+
else assert_not_null(prop.target.get_id_property())
|
212
|
+
.get_data_model_datatype()
|
213
|
+
.get_python_type()
|
214
|
+
.type
|
215
|
+
),
|
216
|
+
)
|
217
|
+
for prop in cls.properties.values()
|
218
|
+
if prop.data.is_system_controlled
|
219
|
+
],
|
220
|
+
),
|
221
|
+
)
|
222
|
+
|
223
|
+
program.root.linebreak()
|
192
224
|
cls_ast = program.root.class_(
|
193
225
|
"Upsert" + cls.name,
|
194
226
|
[expr.ExprIdent("msgspec.Struct"), expr.ExprIdent("kw_only=True")],
|
@@ -206,12 +238,6 @@ def make_python_data_model(
|
|
206
238
|
program.import_(pytype.dep, True)
|
207
239
|
|
208
240
|
pytype_type = pytype.type
|
209
|
-
if prop.data.is_private:
|
210
|
-
program.import_("typing.Union", True)
|
211
|
-
program.import_("sera.typing.UnsetType", True)
|
212
|
-
program.import_("sera.typing.UNSET", True)
|
213
|
-
pytype_type = f"Union[{pytype_type}, UnsetType]"
|
214
|
-
|
215
241
|
if len(prop.data.constraints) > 0:
|
216
242
|
# if the property has constraints, we need to figure out
|
217
243
|
program.import_("typing.Annotated", True)
|
@@ -223,6 +249,12 @@ def make_python_data_model(
|
|
223
249
|
else:
|
224
250
|
raise NotImplementedError(prop.data.constraints)
|
225
251
|
|
252
|
+
if prop.data.is_private:
|
253
|
+
program.import_("typing.Union", True)
|
254
|
+
program.import_("sera.typing.UnsetType", True)
|
255
|
+
program.import_("sera.typing.UNSET", True)
|
256
|
+
pytype_type = f"Union[{pytype_type}, UnsetType]"
|
257
|
+
|
226
258
|
prop_default_value = None
|
227
259
|
if prop.data.is_private:
|
228
260
|
prop_default_value = expr.ExprIdent("UNSET")
|
@@ -269,6 +301,69 @@ def make_python_data_model(
|
|
269
301
|
# has_to_db = True
|
270
302
|
# if any(prop for prop in cls.properties.values() if isinstance(prop, ObjectProperty) and prop.cardinality == Cardinality.MANY_TO_MANY):
|
271
303
|
# # if the class has many-to-many relationship, we need to
|
304
|
+
|
305
|
+
if has_system_controlled:
|
306
|
+
program.import_("typing.Optional", True)
|
307
|
+
cls_ast(
|
308
|
+
stmt.LineBreak(),
|
309
|
+
stmt.Comment(
|
310
|
+
"_verified is a special marker to indicate whether the data is updated by the system."
|
311
|
+
),
|
312
|
+
stmt.DefClassVarStatement(
|
313
|
+
"_verified", "bool", expr.ExprConstant(False)
|
314
|
+
),
|
315
|
+
stmt.LineBreak(),
|
316
|
+
lambda ast: ast.func(
|
317
|
+
"__post_init__",
|
318
|
+
[
|
319
|
+
DeferredVar.simple("self"),
|
320
|
+
],
|
321
|
+
)(
|
322
|
+
stmt.AssignStatement(
|
323
|
+
PredefinedFn.attr_getter(
|
324
|
+
expr.ExprIdent("self"), expr.ExprIdent("_verified")
|
325
|
+
),
|
326
|
+
expr.ExprConstant(False),
|
327
|
+
)
|
328
|
+
),
|
329
|
+
stmt.LineBreak(),
|
330
|
+
lambda ast: ast.func(
|
331
|
+
"update_system_controlled_props",
|
332
|
+
[
|
333
|
+
DeferredVar.simple("self"),
|
334
|
+
DeferredVar.simple(
|
335
|
+
"data",
|
336
|
+
expr.ExprIdent("Optional[SystemControlledProps]"),
|
337
|
+
),
|
338
|
+
],
|
339
|
+
)(
|
340
|
+
lambda ast00: ast00.if_(
|
341
|
+
expr.ExprNegation(
|
342
|
+
expr.ExprIs(expr.ExprIdent("data"), expr.ExprConstant(None))
|
343
|
+
),
|
344
|
+
)(
|
345
|
+
*[
|
346
|
+
stmt.AssignStatement(
|
347
|
+
PredefinedFn.attr_getter(
|
348
|
+
expr.ExprIdent("self"), expr.ExprIdent(prop.name)
|
349
|
+
),
|
350
|
+
PredefinedFn.item_getter(
|
351
|
+
expr.ExprIdent("data"), expr.ExprConstant(prop.name)
|
352
|
+
),
|
353
|
+
)
|
354
|
+
for prop in cls.properties.values()
|
355
|
+
if prop.data.is_system_controlled
|
356
|
+
]
|
357
|
+
),
|
358
|
+
stmt.AssignStatement(
|
359
|
+
PredefinedFn.attr_getter(
|
360
|
+
expr.ExprIdent("self"), expr.ExprIdent("_verified")
|
361
|
+
),
|
362
|
+
expr.ExprConstant(True),
|
363
|
+
),
|
364
|
+
),
|
365
|
+
)
|
366
|
+
|
272
367
|
cls_ast(
|
273
368
|
stmt.LineBreak(),
|
274
369
|
lambda ast00: ast00.func(
|
@@ -280,6 +375,18 @@ def make_python_data_model(
|
|
280
375
|
f"{cls.name}DB" if cls.db is not None else cls.name
|
281
376
|
),
|
282
377
|
)(
|
378
|
+
(
|
379
|
+
stmt.AssertionStatement(
|
380
|
+
PredefinedFn.attr_getter(
|
381
|
+
expr.ExprIdent("self"), expr.ExprIdent("_verified")
|
382
|
+
),
|
383
|
+
expr.ExprConstant(
|
384
|
+
"The model data must be verified before converting to db model"
|
385
|
+
),
|
386
|
+
)
|
387
|
+
if has_system_controlled
|
388
|
+
else None
|
389
|
+
),
|
283
390
|
lambda ast10: ast10.return_(
|
284
391
|
expr.ExprFuncCall(
|
285
392
|
expr.ExprIdent(
|
@@ -292,7 +399,7 @@ def make_python_data_model(
|
|
292
399
|
for prop in cls.properties.values()
|
293
400
|
],
|
294
401
|
)
|
295
|
-
)
|
402
|
+
),
|
296
403
|
),
|
297
404
|
)
|
298
405
|
|
@@ -422,6 +529,7 @@ def make_python_relational_model(
|
|
422
529
|
# assume configuration for the app at the top level
|
423
530
|
program.import_(f"{app.config.path}.DB_CONNECTION", True)
|
424
531
|
program.import_(f"{app.config.path}.DB_DEBUG", True)
|
532
|
+
program.import_(f"contextlib.contextmanager", True)
|
425
533
|
|
426
534
|
program.root.linebreak()
|
427
535
|
|
@@ -483,7 +591,15 @@ def make_python_relational_model(
|
|
483
591
|
)
|
484
592
|
|
485
593
|
program.root.linebreak()
|
486
|
-
program.root.func("
|
594
|
+
program.root.func("async_get_session", [], is_async=True)(
|
595
|
+
lambda ast00: ast00.python_stmt("with Session(engine) as session:")(
|
596
|
+
lambda ast01: ast01.python_stmt("yield session")
|
597
|
+
)
|
598
|
+
)
|
599
|
+
|
600
|
+
program.root.linebreak()
|
601
|
+
program.root.python_stmt("@contextmanager")
|
602
|
+
program.root.func("get_session", [])(
|
487
603
|
lambda ast00: ast00.python_stmt("with Session(engine) as session:")(
|
488
604
|
lambda ast01: ast01.python_stmt("yield session")
|
489
605
|
)
|
sera/models/_parse.py
CHANGED
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
6
6
|
from typing import Sequence
|
7
7
|
|
8
8
|
import serde.yaml
|
9
|
+
|
9
10
|
from sera.models._class import Class, ClassDBMapInfo, Index
|
10
11
|
from sera.models._constraints import Constraint, predefined_constraints
|
11
12
|
from sera.models._datatype import (
|
@@ -101,22 +102,24 @@ def _parse_property(
|
|
101
102
|
schema: Schema, prop_name: str, prop: dict
|
102
103
|
) -> DataProperty | ObjectProperty:
|
103
104
|
if isinstance(prop, str):
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
105
|
+
# deprecated
|
106
|
+
assert False, prop
|
107
|
+
# datatype = prop
|
108
|
+
# if datatype in schema.classes:
|
109
|
+
# return ObjectProperty(
|
110
|
+
# name=prop_name,
|
111
|
+
# label=_parse_multi_lingual_string(prop_name),
|
112
|
+
# description=_parse_multi_lingual_string(""),
|
113
|
+
# target=schema.classes[datatype],
|
114
|
+
# cardinality=Cardinality.ONE_TO_ONE,
|
115
|
+
# )
|
116
|
+
# else:
|
117
|
+
# return DataProperty(
|
118
|
+
# name=prop_name,
|
119
|
+
# label=_parse_multi_lingual_string(prop_name),
|
120
|
+
# description=_parse_multi_lingual_string(""),
|
121
|
+
# datatype=_parse_datatype(schema, datatype),
|
122
|
+
# )
|
120
123
|
|
121
124
|
db = prop.get("db", {})
|
122
125
|
_data = prop.get("data", {})
|
@@ -128,6 +131,7 @@ def _parse_property(
|
|
128
131
|
constraints=[
|
129
132
|
_parse_constraint(constraint) for constraint in _data.get("constraints", [])
|
130
133
|
],
|
134
|
+
is_system_controlled=prop.get("is_system_controlled", False),
|
131
135
|
)
|
132
136
|
|
133
137
|
assert isinstance(prop, dict), prop
|
sera/models/_property.py
CHANGED
@@ -71,6 +71,9 @@ class PropDataAttrs:
|
|
71
71
|
# list of constraints applied to the data model's field
|
72
72
|
constraints: list[Constraint] = field(default_factory=list)
|
73
73
|
|
74
|
+
# whether this property is controlled by the system or not
|
75
|
+
is_system_controlled: bool = False
|
76
|
+
|
74
77
|
|
75
78
|
@dataclass(kw_only=True)
|
76
79
|
class Property:
|
@@ -1,16 +1,19 @@
|
|
1
1
|
sera/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
sera/constants.py,sha256=mzAaMyIx8TJK0-RYYJ5I24C4s0Uvj26OLMJmBo0pxHI,123
|
3
3
|
sera/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
sera/libs/api_helper.py,sha256=
|
4
|
+
sera/libs/api_helper.py,sha256=47y1kcwk3Xd2ZEMnUj_0OwCuUmgwOs5kYrE95BDVUn4,5411
|
5
5
|
sera/libs/base_orm.py,sha256=dyh0OT2sbHku5qPJXvRzYAHRTSXvvbQaS-Qwg65Bw04,2918
|
6
|
-
sera/libs/base_service.py,sha256=
|
6
|
+
sera/libs/base_service.py,sha256=tKvYyCNIBLUbcZUp0EbrU9T2vHSRu_oISbkPECjZEfo,4613
|
7
7
|
sera/libs/dag/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
8
|
sera/libs/dag/_dag.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
sera/libs/middlewares/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
sera/libs/middlewares/auth.py,sha256=aH2fJshQpJT_dw3lUr67doZ8zdf90XyakdY35qZ1iZs,2443
|
11
|
+
sera/libs/middlewares/uscp.py,sha256=H5umW8iEQSCdb_MJ5Im49kxg1E7TpxSg1p2_2A5zI1U,2600
|
9
12
|
sera/make/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
13
|
sera/make/__main__.py,sha256=G5O7s6135-708honwqMFn2yPTs06WbGQTHpupID0eZ4,1417
|
11
14
|
sera/make/make_app.py,sha256=n9NtW73O3s_5Q31VHIRmnd-jEIcpDO7ksAsOdovde2s,5999
|
12
|
-
sera/make/make_python_api.py,sha256=
|
13
|
-
sera/make/make_python_model.py,sha256=
|
15
|
+
sera/make/make_python_api.py,sha256=kq5DClmEeeNgg-a3Bb_8GN9jxvjnhjmW3RfBHNzynO8,25407
|
16
|
+
sera/make/make_python_model.py,sha256=UWI-HbpFjt8n5yLAv2oGQK68LgrF-m0BUI334qtB9Mk,41228
|
14
17
|
sera/make/make_python_services.py,sha256=RsinYZdfkrTlTn9CT50VgqGs9w6IZawsJx-KEmqfnEY,2062
|
15
18
|
sera/make/make_typescript_model.py,sha256=fMjz4YxdGHijb2pHI4xj7rKJf1PxyJxVLYW0ivsu70c,50128
|
16
19
|
sera/misc/__init__.py,sha256=Dh4uDq0D4N53h3zhvmwfa5a0TPVRSUvLzb0hkFuPirk,411
|
@@ -25,10 +28,10 @@ sera/models/_default.py,sha256=ABggW6qdPR4ZDqIPJdJ0GCGQ-7kfsfZmQ_DchgZEa-I,137
|
|
25
28
|
sera/models/_enum.py,sha256=sy0q7E646F-APsqrVQ52r1fAQ_DCAeaNq5YM5QN3zIk,2070
|
26
29
|
sera/models/_module.py,sha256=8QRSCubZmdDP9rL58rGAS6X5VCrkc1ZHvuMu1I1KrWk,5043
|
27
30
|
sera/models/_multi_lingual_string.py,sha256=JETN6k00VH4wrA4w5vAHMEJV8fp3SY9bJebskFTjQLA,1186
|
28
|
-
sera/models/_parse.py,sha256=
|
29
|
-
sera/models/_property.py,sha256=
|
31
|
+
sera/models/_parse.py,sha256=uw6fvvh1ucGqE2jFTCCr-e6_qMfZfSVpaPolNxmrHww,9897
|
32
|
+
sera/models/_property.py,sha256=SJSm5fZJimd2rQuL4UH_aZuNyp9v7x64xMbEVbtYx8Q,5633
|
30
33
|
sera/models/_schema.py,sha256=r-Gqg9Lb_wR3UrbNvfXXgt_qs5bts0t2Ve7aquuF_OI,1155
|
31
34
|
sera/typing.py,sha256=Q4QMfbtfrCjC9tFfsZPhsAnbNX4lm4NHQ9lmjNXYdV0,772
|
32
|
-
sera_2-1.
|
33
|
-
sera_2-1.
|
34
|
-
sera_2-1.
|
35
|
+
sera_2-1.11.1.dist-info/METADATA,sha256=vymmUgXvGuXjkCDXFhPc0Y8ZsM-Kwb7cBBLLy4I10ss,857
|
36
|
+
sera_2-1.11.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
37
|
+
sera_2-1.11.1.dist-info/RECORD,,
|
File without changes
|