sera-2 1.12.3__py3-none-any.whl → 1.12.6__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/base_orm.py +21 -3
- sera/libs/base_service.py +29 -20
- sera/libs/middlewares/auth.py +3 -6
- sera/make/__main__.py +2 -1
- sera/make/make_python_api.py +100 -79
- sera/make/make_python_model.py +185 -51
- sera/make/make_python_services.py +3 -2
- sera/models/__init__.py +8 -1
- sera/models/_constraints.py +15 -1
- sera/models/_datatype.py +10 -0
- sera/models/_parse.py +9 -4
- sera/models/_property.py +14 -1
- {sera_2-1.12.3.dist-info → sera_2-1.12.6.dist-info}/METADATA +3 -3
- {sera_2-1.12.3.dist-info → sera_2-1.12.6.dist-info}/RECORD +15 -15
- {sera_2-1.12.3.dist-info → sera_2-1.12.6.dist-info}/WHEEL +0 -0
sera/libs/base_orm.py
CHANGED
@@ -4,10 +4,12 @@ from typing import Optional
|
|
4
4
|
|
5
5
|
import orjson
|
6
6
|
from msgspec.json import decode, encode
|
7
|
-
from sera.typing import UNSET
|
8
7
|
from sqlalchemy import LargeBinary, TypeDecorator
|
9
8
|
from sqlalchemy import create_engine as sqlalchemy_create_engine
|
10
9
|
from sqlalchemy import update
|
10
|
+
from sqlalchemy.ext.asyncio import create_async_engine as sqlalchemy_create_async_engine
|
11
|
+
|
12
|
+
from sera.typing import UNSET
|
11
13
|
|
12
14
|
|
13
15
|
class BaseORM:
|
@@ -104,11 +106,27 @@ class DictDataclassType(TypeDecorator):
|
|
104
106
|
def create_engine(
|
105
107
|
dbconn: str,
|
106
108
|
connect_args: Optional[dict] = None,
|
107
|
-
|
109
|
+
echo: bool = False,
|
110
|
+
):
|
111
|
+
if dbconn.startswith("sqlite"):
|
112
|
+
connect_args = {"check_same_thread": False}
|
113
|
+
else:
|
114
|
+
connect_args = {}
|
115
|
+
engine = sqlalchemy_create_engine(dbconn, connect_args=connect_args, echo=echo)
|
116
|
+
return engine
|
117
|
+
|
118
|
+
|
119
|
+
def create_async_engine(
|
120
|
+
dbconn: str,
|
121
|
+
connect_args: Optional[dict] = None,
|
122
|
+
echo: bool = False,
|
108
123
|
):
|
109
124
|
if dbconn.startswith("sqlite"):
|
110
125
|
connect_args = {"check_same_thread": False}
|
111
126
|
else:
|
112
127
|
connect_args = {}
|
113
|
-
|
128
|
+
|
129
|
+
engine = sqlalchemy_create_async_engine(
|
130
|
+
dbconn, connect_args=connect_args, echo=echo
|
131
|
+
)
|
114
132
|
return engine
|
sera/libs/base_service.py
CHANGED
@@ -4,8 +4,11 @@ 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
|
8
|
-
from sqlalchemy
|
7
|
+
from litestar.exceptions import HTTPException
|
8
|
+
from sqlalchemy import Result, Select, delete, exists, func, select
|
9
|
+
from sqlalchemy.exc import IntegrityError
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
11
|
+
from sqlalchemy.orm import load_only
|
9
12
|
|
10
13
|
from sera.libs.base_orm import BaseORM
|
11
14
|
from sera.misc import assert_not_null
|
@@ -41,7 +44,7 @@ class QueryResult(NamedTuple, Generic[R]):
|
|
41
44
|
total: int
|
42
45
|
|
43
46
|
|
44
|
-
class
|
47
|
+
class BaseAsyncService(Generic[ID, R]):
|
45
48
|
|
46
49
|
instance = None
|
47
50
|
|
@@ -51,7 +54,7 @@ class BaseService(Generic[ID, R]):
|
|
51
54
|
self.id_prop = assert_not_null(cls.get_id_property())
|
52
55
|
|
53
56
|
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
|
57
|
+
self.is_id_auto_increment = assert_not_null(self.id_prop.db).is_auto_increment
|
55
58
|
|
56
59
|
@classmethod
|
57
60
|
def get_instance(cls):
|
@@ -59,10 +62,10 @@ class BaseService(Generic[ID, R]):
|
|
59
62
|
if cls.instance is None:
|
60
63
|
# assume that the subclass overrides the __init__ method
|
61
64
|
# so that we don't need to pass the class and orm_cls
|
62
|
-
cls.instance = cls()
|
65
|
+
cls.instance = cls() # type: ignore[call-arg]
|
63
66
|
return cls.instance
|
64
67
|
|
65
|
-
def get(
|
68
|
+
async def get(
|
66
69
|
self,
|
67
70
|
query: Query,
|
68
71
|
limit: int,
|
@@ -71,7 +74,7 @@ class BaseService(Generic[ID, R]):
|
|
71
74
|
sorted_by: list[str],
|
72
75
|
group_by: list[str],
|
73
76
|
fields: list[str],
|
74
|
-
session:
|
77
|
+
session: AsyncSession,
|
75
78
|
) -> QueryResult[R]:
|
76
79
|
"""Retrieving records matched a query.
|
77
80
|
|
@@ -103,35 +106,37 @@ class BaseService(Generic[ID, R]):
|
|
103
106
|
|
104
107
|
cq = select(func.count()).select_from(q.subquery())
|
105
108
|
rq = q.limit(limit).offset(offset)
|
106
|
-
records = self._process_result(session.execute(rq)).scalars().all()
|
107
|
-
total = session.execute(cq).scalar_one()
|
109
|
+
records = self._process_result(await session.execute(rq)).scalars().all()
|
110
|
+
total = (await session.execute(cq)).scalar_one()
|
108
111
|
return QueryResult(records, total)
|
109
112
|
|
110
|
-
def get_by_id(self, id: ID, session:
|
113
|
+
async def get_by_id(self, id: ID, session: AsyncSession) -> Optional[R]:
|
111
114
|
"""Retrieving a record by ID."""
|
112
115
|
q = self._select().where(self._cls_id_prop == id)
|
113
|
-
result = self._process_result(session.execute(q)).scalar_one_or_none()
|
116
|
+
result = self._process_result(await session.execute(q)).scalar_one_or_none()
|
114
117
|
return result
|
115
118
|
|
116
|
-
def has_id(self, id: ID, session:
|
119
|
+
async def has_id(self, id: ID, session: AsyncSession) -> bool:
|
117
120
|
"""Check whether we have a record with the given ID."""
|
118
|
-
q = exists().where(self._cls_id_prop == id)
|
119
|
-
result = session.
|
121
|
+
q = exists().where(self._cls_id_prop == id).select()
|
122
|
+
result = (await session.execute(q)).scalar()
|
120
123
|
return bool(result)
|
121
124
|
|
122
|
-
def create(self, record: R, session:
|
125
|
+
async def create(self, record: R, session: AsyncSession) -> R:
|
123
126
|
"""Create a new record."""
|
124
127
|
if self.is_id_auto_increment:
|
125
128
|
setattr(record, self.id_prop.name, None)
|
126
129
|
|
127
|
-
|
128
|
-
|
130
|
+
try:
|
131
|
+
session.add(record)
|
132
|
+
await session.flush()
|
133
|
+
except IntegrityError:
|
134
|
+
raise HTTPException(detail="Invalid request", status_code=409)
|
129
135
|
return record
|
130
136
|
|
131
|
-
def update(self, record: R, session:
|
137
|
+
async def update(self, record: R, session: AsyncSession) -> R:
|
132
138
|
"""Update an existing record."""
|
133
|
-
session.execute(record.get_update_query())
|
134
|
-
session.commit()
|
139
|
+
await session.execute(record.get_update_query())
|
135
140
|
return record
|
136
141
|
|
137
142
|
def _select(self) -> Select:
|
@@ -141,3 +146,7 @@ class BaseService(Generic[ID, R]):
|
|
141
146
|
def _process_result(self, result: SqlResult) -> SqlResult:
|
142
147
|
"""Process the result of a query."""
|
143
148
|
return result
|
149
|
+
|
150
|
+
async def truncate(self, session: AsyncSession) -> None:
|
151
|
+
"""Truncate the table."""
|
152
|
+
await session.execute(delete(self.orm_cls))
|
sera/libs/middlewares/auth.py
CHANGED
@@ -1,16 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from datetime import datetime, timezone
|
4
|
-
from typing import
|
4
|
+
from typing import Awaitable, Callable, Generic, Sequence
|
5
5
|
|
6
6
|
from litestar import Request
|
7
7
|
from litestar.connection import ASGIConnection
|
8
8
|
from litestar.exceptions import NotAuthorizedException
|
9
9
|
from litestar.middleware import AbstractAuthenticationMiddleware, AuthenticationResult
|
10
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
11
|
|
15
12
|
from sera.typing import T
|
16
13
|
|
@@ -26,7 +23,7 @@ class AuthMiddleware(AbstractAuthenticationMiddleware, Generic[T]):
|
|
26
23
|
def __init__(
|
27
24
|
self,
|
28
25
|
app: ASGIApp,
|
29
|
-
user_handler: Callable[[str], T],
|
26
|
+
user_handler: Callable[[str], Awaitable[T]],
|
30
27
|
exclude: str | list[str] | None = None,
|
31
28
|
exclude_from_auth_key: str = "exclude_from_auth",
|
32
29
|
exclude_http_methods: Sequence[Method] | None = None,
|
@@ -59,7 +56,7 @@ class AuthMiddleware(AbstractAuthenticationMiddleware, Generic[T]):
|
|
59
56
|
detail="Credentials expired",
|
60
57
|
)
|
61
58
|
|
62
|
-
user = self.user_handler(userid)
|
59
|
+
user = await self.user_handler(userid)
|
63
60
|
if user is None:
|
64
61
|
raise NotAuthorizedException(
|
65
62
|
detail="User not found",
|
sera/make/__main__.py
CHANGED
sera/make/make_python_api.py
CHANGED
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from typing import Sequence
|
4
4
|
|
5
|
-
from codegen.models import DeferredVar, PredefinedFn, Program, expr, stmt
|
5
|
+
from codegen.models import DeferredVar, ImportHelper, PredefinedFn, Program, expr, stmt
|
6
6
|
from loguru import logger
|
7
7
|
|
8
8
|
from sera.misc import assert_not_null, to_snake_case
|
9
|
-
from sera.models import App, DataCollection, Module, Package
|
9
|
+
from sera.models import App, DataCollection, Module, Package, SystemControlledMode
|
10
|
+
|
11
|
+
GLOBAL_IDENTS = {"AsyncSession": "sqlalchemy.ext.asyncio.AsyncSession"}
|
10
12
|
|
11
13
|
|
12
14
|
def make_python_api(app: App, collections: Sequence[DataCollection]):
|
@@ -112,12 +114,13 @@ def make_python_get_api(
|
|
112
114
|
app = target_pkg.app
|
113
115
|
|
114
116
|
program = Program()
|
117
|
+
import_helper = ImportHelper(program, GLOBAL_IDENTS)
|
118
|
+
|
115
119
|
program.import_("__future__.annotations", True)
|
116
120
|
program.import_("typing.Annotated", True)
|
117
121
|
program.import_("litestar.get", True)
|
118
122
|
program.import_("litestar.Request", True)
|
119
123
|
program.import_("litestar.params.Parameter", True)
|
120
|
-
program.import_("sqlalchemy.orm.Session", True)
|
121
124
|
program.import_(app.config.path + ".API_DEBUG", True)
|
122
125
|
program.import_(
|
123
126
|
app.services.path
|
@@ -191,7 +194,7 @@ def make_python_get_api(
|
|
191
194
|
),
|
192
195
|
DeferredVar.simple(
|
193
196
|
"session",
|
194
|
-
|
197
|
+
import_helper.use("AsyncSession"),
|
195
198
|
),
|
196
199
|
],
|
197
200
|
return_type=expr.ExprIdent(f"dict"),
|
@@ -226,35 +229,37 @@ def make_python_get_api(
|
|
226
229
|
),
|
227
230
|
lambda ast102: ast102.assign(
|
228
231
|
DeferredVar.simple("result"),
|
229
|
-
expr.
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
[
|
235
|
-
expr.ExprIdent("query"),
|
236
|
-
PredefinedFn.keyword_assignment(
|
237
|
-
"limit", expr.ExprIdent("limit")
|
238
|
-
),
|
239
|
-
PredefinedFn.keyword_assignment(
|
240
|
-
"offset", expr.ExprIdent("offset")
|
241
|
-
),
|
242
|
-
PredefinedFn.keyword_assignment(
|
243
|
-
"unique", expr.ExprIdent("unique")
|
244
|
-
),
|
245
|
-
PredefinedFn.keyword_assignment(
|
246
|
-
"sorted_by", expr.ExprIdent("sorted_by")
|
247
|
-
),
|
248
|
-
PredefinedFn.keyword_assignment(
|
249
|
-
"group_by", expr.ExprIdent("group_by")
|
250
|
-
),
|
251
|
-
PredefinedFn.keyword_assignment(
|
252
|
-
"fields", expr.ExprIdent("fields")
|
253
|
-
),
|
254
|
-
PredefinedFn.keyword_assignment(
|
255
|
-
"session", expr.ExprIdent("session")
|
232
|
+
expr.ExprAwait(
|
233
|
+
expr.ExprFuncCall(
|
234
|
+
PredefinedFn.attr_getter(
|
235
|
+
expr.ExprIdent("service"),
|
236
|
+
expr.ExprIdent("get"),
|
256
237
|
),
|
257
|
-
|
238
|
+
[
|
239
|
+
expr.ExprIdent("query"),
|
240
|
+
PredefinedFn.keyword_assignment(
|
241
|
+
"limit", expr.ExprIdent("limit")
|
242
|
+
),
|
243
|
+
PredefinedFn.keyword_assignment(
|
244
|
+
"offset", expr.ExprIdent("offset")
|
245
|
+
),
|
246
|
+
PredefinedFn.keyword_assignment(
|
247
|
+
"unique", expr.ExprIdent("unique")
|
248
|
+
),
|
249
|
+
PredefinedFn.keyword_assignment(
|
250
|
+
"sorted_by", expr.ExprIdent("sorted_by")
|
251
|
+
),
|
252
|
+
PredefinedFn.keyword_assignment(
|
253
|
+
"group_by", expr.ExprIdent("group_by")
|
254
|
+
),
|
255
|
+
PredefinedFn.keyword_assignment(
|
256
|
+
"fields", expr.ExprIdent("fields")
|
257
|
+
),
|
258
|
+
PredefinedFn.keyword_assignment(
|
259
|
+
"session", expr.ExprIdent("session")
|
260
|
+
),
|
261
|
+
],
|
262
|
+
)
|
258
263
|
),
|
259
264
|
),
|
260
265
|
lambda ast103: ast103.return_(
|
@@ -300,14 +305,13 @@ def make_python_get_by_id_api(
|
|
300
305
|
"""Make an endpoint for querying resource by id"""
|
301
306
|
app = target_pkg.app
|
302
307
|
|
303
|
-
ServiceNameDep = to_snake_case(f"{collection.name}ServiceDependency")
|
304
|
-
|
305
308
|
program = Program()
|
309
|
+
import_helper = ImportHelper(program, GLOBAL_IDENTS)
|
310
|
+
|
306
311
|
program.import_("__future__.annotations", True)
|
307
312
|
program.import_("litestar.get", True)
|
308
313
|
program.import_("litestar.status_codes", True)
|
309
314
|
program.import_("litestar.exceptions.HTTPException", True)
|
310
|
-
program.import_("sqlalchemy.orm.Session", True)
|
311
315
|
program.import_(
|
312
316
|
app.services.path
|
313
317
|
+ f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
|
@@ -342,7 +346,7 @@ def make_python_get_by_id_api(
|
|
342
346
|
),
|
343
347
|
DeferredVar.simple(
|
344
348
|
"session",
|
345
|
-
|
349
|
+
import_helper.use("AsyncSession"),
|
346
350
|
),
|
347
351
|
],
|
348
352
|
return_type=expr.ExprIdent("dict"),
|
@@ -361,12 +365,14 @@ def make_python_get_by_id_api(
|
|
361
365
|
),
|
362
366
|
lambda ast11: ast11.assign(
|
363
367
|
DeferredVar.simple("record"),
|
364
|
-
expr.
|
365
|
-
expr.
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
368
|
+
expr.ExprAwait(
|
369
|
+
expr.ExprFuncCall(
|
370
|
+
expr.ExprIdent("service.get_by_id"),
|
371
|
+
[
|
372
|
+
expr.ExprIdent("id"),
|
373
|
+
expr.ExprIdent("session"),
|
374
|
+
],
|
375
|
+
)
|
370
376
|
),
|
371
377
|
),
|
372
378
|
lambda ast12: ast12.if_(PredefinedFn.is_null(expr.ExprIdent("record")))(
|
@@ -423,14 +429,13 @@ def make_python_has_api(
|
|
423
429
|
"""Make an endpoint for querying resource by id"""
|
424
430
|
app = target_pkg.app
|
425
431
|
|
426
|
-
ServiceNameDep = to_snake_case(f"{collection.name}ServiceDependency")
|
427
|
-
|
428
432
|
program = Program()
|
433
|
+
import_helper = ImportHelper(program, GLOBAL_IDENTS)
|
434
|
+
|
429
435
|
program.import_("__future__.annotations", True)
|
430
436
|
program.import_("litestar.head", True)
|
431
437
|
program.import_("litestar.status_codes", True)
|
432
438
|
program.import_("litestar.exceptions.HTTPException", True)
|
433
|
-
program.import_("sqlalchemy.orm.Session", True)
|
434
439
|
program.import_(
|
435
440
|
app.services.path
|
436
441
|
+ f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
|
@@ -465,13 +470,15 @@ def make_python_has_api(
|
|
465
470
|
),
|
466
471
|
DeferredVar.simple(
|
467
472
|
"session",
|
468
|
-
|
473
|
+
import_helper.use("AsyncSession"),
|
469
474
|
),
|
470
475
|
],
|
471
476
|
return_type=expr.ExprConstant(None),
|
472
477
|
is_async=True,
|
473
478
|
)(
|
474
|
-
stmt.SingleExprStatement(
|
479
|
+
stmt.SingleExprStatement(
|
480
|
+
expr.ExprConstant("Checking if record exists by id")
|
481
|
+
),
|
475
482
|
lambda ast100: ast100.assign(
|
476
483
|
DeferredVar.simple("service"),
|
477
484
|
expr.ExprFuncCall(
|
@@ -484,12 +491,14 @@ def make_python_has_api(
|
|
484
491
|
),
|
485
492
|
lambda ast11: ast11.assign(
|
486
493
|
DeferredVar.simple("record_exist"),
|
487
|
-
expr.
|
488
|
-
expr.
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
494
|
+
expr.ExprAwait(
|
495
|
+
expr.ExprFuncCall(
|
496
|
+
expr.ExprIdent("service.has_id"),
|
497
|
+
[
|
498
|
+
expr.ExprIdent("id"),
|
499
|
+
expr.ExprIdent("session"),
|
500
|
+
],
|
501
|
+
)
|
493
502
|
),
|
494
503
|
),
|
495
504
|
lambda ast12: ast12.if_(expr.ExprNegation(expr.ExprIdent("record_exist")))(
|
@@ -524,9 +533,10 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
|
|
524
533
|
app = target_pkg.app
|
525
534
|
|
526
535
|
program = Program()
|
536
|
+
import_helper = ImportHelper(program, GLOBAL_IDENTS)
|
537
|
+
|
527
538
|
program.import_("__future__.annotations", True)
|
528
539
|
program.import_("litestar.post", True)
|
529
|
-
program.import_("sqlalchemy.orm.Session", True)
|
530
540
|
program.import_(
|
531
541
|
app.services.path
|
532
542
|
+ f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
|
@@ -540,12 +550,13 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
|
|
540
550
|
|
541
551
|
# assuming the collection has only one class
|
542
552
|
cls = collection.cls
|
543
|
-
|
544
|
-
prop.data.is_system_controlled
|
553
|
+
has_restricted_system_controlled_prop = any(
|
554
|
+
prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
|
555
|
+
for prop in cls.properties.values()
|
545
556
|
)
|
546
557
|
idprop = assert_not_null(cls.get_id_property())
|
547
558
|
|
548
|
-
if
|
559
|
+
if has_restricted_system_controlled_prop:
|
549
560
|
program.import_("sera.libs.api_helper.SingleAutoUSCP", True)
|
550
561
|
|
551
562
|
func_name = "create"
|
@@ -568,7 +579,7 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
|
|
568
579
|
),
|
569
580
|
)
|
570
581
|
]
|
571
|
-
if
|
582
|
+
if has_restricted_system_controlled_prop
|
572
583
|
else []
|
573
584
|
),
|
574
585
|
)
|
@@ -582,7 +593,7 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
|
|
582
593
|
),
|
583
594
|
DeferredVar.simple(
|
584
595
|
"session",
|
585
|
-
|
596
|
+
import_helper.use("AsyncSession"),
|
586
597
|
),
|
587
598
|
],
|
588
599
|
return_type=expr.ExprIdent(idprop.datatype.get_python_type().type),
|
@@ -601,13 +612,17 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
|
|
601
612
|
),
|
602
613
|
lambda ast13: ast13.return_(
|
603
614
|
PredefinedFn.attr_getter(
|
604
|
-
expr.
|
605
|
-
expr.
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
615
|
+
expr.ExprAwait(
|
616
|
+
expr.ExprMethodCall(
|
617
|
+
expr.ExprIdent("service"),
|
618
|
+
"create",
|
619
|
+
[
|
620
|
+
expr.ExprMethodCall(
|
621
|
+
expr.ExprIdent("data"), "to_db", []
|
622
|
+
),
|
623
|
+
expr.ExprIdent("session"),
|
624
|
+
],
|
625
|
+
)
|
611
626
|
),
|
612
627
|
expr.ExprIdent(idprop.name),
|
613
628
|
)
|
@@ -626,9 +641,10 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
|
|
626
641
|
app = target_pkg.app
|
627
642
|
|
628
643
|
program = Program()
|
644
|
+
import_helper = ImportHelper(program, GLOBAL_IDENTS)
|
645
|
+
|
629
646
|
program.import_("__future__.annotations", True)
|
630
647
|
program.import_("litestar.put", True)
|
631
|
-
program.import_("sqlalchemy.orm.Session", True)
|
632
648
|
program.import_(
|
633
649
|
app.services.path
|
634
650
|
+ f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
|
@@ -645,10 +661,11 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
|
|
645
661
|
id_prop = assert_not_null(cls.get_id_property())
|
646
662
|
id_type = id_prop.datatype.get_python_type().type
|
647
663
|
|
648
|
-
|
649
|
-
prop.data.is_system_controlled
|
664
|
+
has_restricted_system_controlled_prop = any(
|
665
|
+
prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
|
666
|
+
for prop in cls.properties.values()
|
650
667
|
)
|
651
|
-
if
|
668
|
+
if has_restricted_system_controlled_prop:
|
652
669
|
program.import_("sera.libs.api_helper.SingleAutoUSCP", True)
|
653
670
|
|
654
671
|
func_name = "update"
|
@@ -671,7 +688,7 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
|
|
671
688
|
),
|
672
689
|
)
|
673
690
|
]
|
674
|
-
if
|
691
|
+
if has_restricted_system_controlled_prop
|
675
692
|
else []
|
676
693
|
),
|
677
694
|
)
|
@@ -689,7 +706,7 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
|
|
689
706
|
),
|
690
707
|
DeferredVar.simple(
|
691
708
|
"session",
|
692
|
-
|
709
|
+
import_helper.use("AsyncSession"),
|
693
710
|
),
|
694
711
|
],
|
695
712
|
return_type=expr.ExprIdent(id_prop.datatype.get_python_type().type),
|
@@ -715,13 +732,17 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
|
|
715
732
|
),
|
716
733
|
lambda ast13: ast13.return_(
|
717
734
|
PredefinedFn.attr_getter(
|
718
|
-
expr.
|
719
|
-
expr.
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
735
|
+
expr.ExprAwait(
|
736
|
+
expr.ExprMethodCall(
|
737
|
+
expr.ExprIdent("service"),
|
738
|
+
"update",
|
739
|
+
[
|
740
|
+
expr.ExprMethodCall(
|
741
|
+
expr.ExprIdent("data"), "to_db", []
|
742
|
+
),
|
743
|
+
expr.ExprIdent("session"),
|
744
|
+
],
|
745
|
+
)
|
725
746
|
),
|
726
747
|
expr.ExprIdent(id_prop.name),
|
727
748
|
)
|
sera/make/make_python_model.py
CHANGED
@@ -1,9 +1,16 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from operator import is_
|
4
3
|
from typing import Callable, Optional, Sequence
|
5
4
|
|
6
|
-
from codegen.models import
|
5
|
+
from codegen.models import (
|
6
|
+
AST,
|
7
|
+
DeferredVar,
|
8
|
+
ImportHelper,
|
9
|
+
PredefinedFn,
|
10
|
+
Program,
|
11
|
+
expr,
|
12
|
+
stmt,
|
13
|
+
)
|
7
14
|
|
8
15
|
from sera.misc import (
|
9
16
|
assert_isinstance,
|
@@ -20,6 +27,7 @@ from sera.models import (
|
|
20
27
|
PyTypeWithDep,
|
21
28
|
Schema,
|
22
29
|
)
|
30
|
+
from sera.models._property import SystemControlledMode
|
23
31
|
from sera.typing import ObjectPath
|
24
32
|
|
25
33
|
|
@@ -211,10 +219,19 @@ def make_python_data_model(
|
|
211
219
|
alias=f"{cls.name}DB",
|
212
220
|
)
|
213
221
|
|
214
|
-
|
215
|
-
|
222
|
+
ident_manager = ImportHelper(
|
223
|
+
program,
|
224
|
+
{
|
225
|
+
"UNSET": "sera.typing.UNSET",
|
226
|
+
},
|
227
|
+
)
|
228
|
+
|
229
|
+
# property that normal users cannot set, but super users can
|
230
|
+
has_restricted_system_controlled = any(
|
231
|
+
prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
|
232
|
+
for prop in cls.properties.values()
|
216
233
|
)
|
217
|
-
if
|
234
|
+
if has_restricted_system_controlled:
|
218
235
|
program.import_("typing.TypedDict", True)
|
219
236
|
program.root(
|
220
237
|
stmt.LineBreak(),
|
@@ -236,6 +253,7 @@ def make_python_data_model(
|
|
236
253
|
)
|
237
254
|
for prop in cls.properties.values()
|
238
255
|
if prop.data.is_system_controlled
|
256
|
+
== SystemControlledMode.RESTRICTED
|
239
257
|
],
|
240
258
|
),
|
241
259
|
)
|
@@ -246,11 +264,10 @@ def make_python_data_model(
|
|
246
264
|
[expr.ExprIdent("msgspec.Struct"), expr.ExprIdent("kw_only=True")],
|
247
265
|
)
|
248
266
|
for prop in cls.properties.values():
|
249
|
-
#
|
250
|
-
#
|
251
|
-
|
252
|
-
|
253
|
-
# continue
|
267
|
+
# a field that is fully controlled by the system (e.g., cached or derived fields)
|
268
|
+
# aren't allowed to be set by the users, so we skip them
|
269
|
+
if prop.data.is_system_controlled == SystemControlledMode.AUTO:
|
270
|
+
continue
|
254
271
|
|
255
272
|
if isinstance(prop, DataProperty):
|
256
273
|
pytype = prop.get_data_model_datatype().get_python_type()
|
@@ -272,14 +289,20 @@ def make_python_data_model(
|
|
272
289
|
else:
|
273
290
|
raise NotImplementedError(prop.data.constraints)
|
274
291
|
|
275
|
-
if
|
292
|
+
if (
|
293
|
+
prop.data.is_private
|
294
|
+
or prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
|
295
|
+
):
|
276
296
|
program.import_("typing.Union", True)
|
277
297
|
program.import_("sera.typing.UnsetType", True)
|
278
298
|
program.import_("sera.typing.UNSET", True)
|
279
299
|
pytype_type = f"Union[{pytype_type}, UnsetType]"
|
280
300
|
|
281
301
|
prop_default_value = None
|
282
|
-
if
|
302
|
+
if (
|
303
|
+
prop.data.is_private
|
304
|
+
or prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
|
305
|
+
):
|
283
306
|
prop_default_value = expr.ExprIdent("UNSET")
|
284
307
|
elif prop.default_value is not None:
|
285
308
|
prop_default_value = expr.ExprConstant(prop.default_value)
|
@@ -321,25 +344,28 @@ def make_python_data_model(
|
|
321
344
|
elif prop.is_optional:
|
322
345
|
pytype = pytype.as_optional_type()
|
323
346
|
|
347
|
+
pytype_type = pytype.type
|
348
|
+
prop_default_value = None
|
349
|
+
if prop.data.is_system_controlled == SystemControlledMode.RESTRICTED:
|
350
|
+
program.import_("typing.Union", True)
|
351
|
+
program.import_("sera.typing.UnsetType", True)
|
352
|
+
program.import_("sera.typing.UNSET", True)
|
353
|
+
pytype_type = f"Union[{pytype_type}, UnsetType]"
|
354
|
+
prop_default_value = expr.ExprIdent("UNSET")
|
355
|
+
|
324
356
|
for dep in pytype.deps:
|
325
357
|
program.import_(dep, True)
|
326
358
|
|
327
|
-
cls_ast(
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
359
|
+
cls_ast(
|
360
|
+
stmt.DefClassVarStatement(
|
361
|
+
prop.name, pytype_type, prop_default_value
|
362
|
+
)
|
363
|
+
)
|
332
364
|
|
333
|
-
if
|
365
|
+
if has_restricted_system_controlled:
|
334
366
|
program.import_("typing.Optional", True)
|
367
|
+
program.import_("sera.typing.is_set", True)
|
335
368
|
cls_ast(
|
336
|
-
stmt.LineBreak(),
|
337
|
-
stmt.Comment(
|
338
|
-
"_verified is a special marker to indicate whether the data is updated by the system."
|
339
|
-
),
|
340
|
-
stmt.DefClassVarStatement(
|
341
|
-
"_verified", "bool", expr.ExprConstant(False)
|
342
|
-
),
|
343
369
|
stmt.LineBreak(),
|
344
370
|
lambda ast: ast.func(
|
345
371
|
"__post_init__",
|
@@ -347,12 +373,17 @@ def make_python_data_model(
|
|
347
373
|
DeferredVar.simple("self"),
|
348
374
|
],
|
349
375
|
)(
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
376
|
+
*[
|
377
|
+
stmt.AssignStatement(
|
378
|
+
PredefinedFn.attr_getter(
|
379
|
+
expr.ExprIdent("self"), expr.ExprIdent(prop.name)
|
380
|
+
),
|
381
|
+
expr.ExprIdent("UNSET"),
|
382
|
+
)
|
383
|
+
for prop in cls.properties.values()
|
384
|
+
if prop.data.is_system_controlled
|
385
|
+
== SystemControlledMode.RESTRICTED
|
386
|
+
]
|
356
387
|
),
|
357
388
|
stmt.LineBreak(),
|
358
389
|
lambda ast: ast.func(
|
@@ -381,14 +412,9 @@ def make_python_data_model(
|
|
381
412
|
)
|
382
413
|
for prop in cls.properties.values()
|
383
414
|
if prop.data.is_system_controlled
|
415
|
+
== SystemControlledMode.RESTRICTED
|
384
416
|
]
|
385
417
|
),
|
386
|
-
stmt.AssignStatement(
|
387
|
-
PredefinedFn.attr_getter(
|
388
|
-
expr.ExprIdent("self"), expr.ExprIdent("_verified")
|
389
|
-
),
|
390
|
-
expr.ExprConstant(True),
|
391
|
-
),
|
392
418
|
),
|
393
419
|
)
|
394
420
|
|
@@ -405,14 +431,27 @@ def make_python_data_model(
|
|
405
431
|
)(
|
406
432
|
(
|
407
433
|
stmt.AssertionStatement(
|
408
|
-
|
409
|
-
|
434
|
+
expr.ExprLogicalAnd(
|
435
|
+
[
|
436
|
+
expr.ExprFuncCall(
|
437
|
+
expr.ExprIdent("is_set"),
|
438
|
+
[
|
439
|
+
PredefinedFn.attr_getter(
|
440
|
+
expr.ExprIdent("self"),
|
441
|
+
expr.ExprIdent(prop.name),
|
442
|
+
)
|
443
|
+
],
|
444
|
+
)
|
445
|
+
for prop in cls.properties.values()
|
446
|
+
if prop.data.is_system_controlled
|
447
|
+
== SystemControlledMode.RESTRICTED
|
448
|
+
]
|
410
449
|
),
|
411
450
|
expr.ExprConstant(
|
412
451
|
"The model data must be verified before converting to db model"
|
413
452
|
),
|
414
453
|
)
|
415
|
-
if
|
454
|
+
if has_restricted_system_controlled
|
416
455
|
else None
|
417
456
|
),
|
418
457
|
lambda ast10: ast10.return_(
|
@@ -421,8 +460,13 @@ def make_python_data_model(
|
|
421
460
|
f"{cls.name}DB" if cls.db is not None else cls.name
|
422
461
|
),
|
423
462
|
[
|
424
|
-
|
425
|
-
|
463
|
+
(
|
464
|
+
to_db_type_conversion(
|
465
|
+
program, expr.ExprIdent("self"), cls, prop
|
466
|
+
)
|
467
|
+
if prop.data.is_system_controlled
|
468
|
+
!= SystemControlledMode.AUTO
|
469
|
+
else ident_manager.use("UNSET")
|
426
470
|
)
|
427
471
|
for prop in cls.properties.values()
|
428
472
|
],
|
@@ -623,8 +667,11 @@ def make_python_relational_model(
|
|
623
667
|
program.import_("__future__.annotations", True)
|
624
668
|
program.import_("sera.libs.base_orm.BaseORM", True)
|
625
669
|
program.import_("sera.libs.base_orm.create_engine", True)
|
670
|
+
program.import_("sera.libs.base_orm.create_async_engine", True)
|
626
671
|
program.import_("sqlalchemy.orm.DeclarativeBase", True)
|
627
672
|
program.import_("sqlalchemy.orm.Session", True)
|
673
|
+
program.import_("sqlalchemy.ext.asyncio.AsyncSession", True)
|
674
|
+
program.import_("sqlalchemy.text", True)
|
628
675
|
|
629
676
|
# assume configuration for the app at the top level
|
630
677
|
program.import_(f"{app.config.path}.DB_CONNECTION", True)
|
@@ -673,14 +720,22 @@ def make_python_relational_model(
|
|
673
720
|
|
674
721
|
program.root.linebreak()
|
675
722
|
program.root.assign(
|
676
|
-
DeferredVar("engine"
|
723
|
+
DeferredVar.simple("engine"),
|
677
724
|
expr.ExprFuncCall(
|
678
725
|
expr.ExprIdent("create_engine"),
|
679
726
|
[
|
680
727
|
expr.ExprIdent("DB_CONNECTION"),
|
681
|
-
PredefinedFn.keyword_assignment(
|
682
|
-
|
683
|
-
|
728
|
+
PredefinedFn.keyword_assignment("echo", expr.ExprIdent("DB_DEBUG")),
|
729
|
+
],
|
730
|
+
),
|
731
|
+
)
|
732
|
+
program.root.assign(
|
733
|
+
DeferredVar.simple("async_engine"),
|
734
|
+
expr.ExprFuncCall(
|
735
|
+
expr.ExprIdent("create_async_engine"),
|
736
|
+
[
|
737
|
+
expr.ExprIdent("DB_CONNECTION"),
|
738
|
+
PredefinedFn.keyword_assignment("echo", expr.ExprIdent("DB_DEBUG")),
|
684
739
|
],
|
685
740
|
),
|
686
741
|
)
|
@@ -691,17 +746,96 @@ def make_python_relational_model(
|
|
691
746
|
)
|
692
747
|
|
693
748
|
program.root.linebreak()
|
694
|
-
program.root.func("
|
695
|
-
lambda
|
696
|
-
|
749
|
+
program.root.func("get_async_session", [], is_async=True)(
|
750
|
+
lambda ast: ast.python_stmt(
|
751
|
+
"async with AsyncSession(async_engine, expire_on_commit=False) as session:"
|
752
|
+
)(
|
753
|
+
lambda ast_l1: ast_l1.try_()(stmt.PythonStatement("yield session")),
|
754
|
+
lambda ast_l1: ast_l1.catch()(
|
755
|
+
stmt.SingleExprStatement(
|
756
|
+
expr.ExprAwait(
|
757
|
+
expr.ExprFuncCall(
|
758
|
+
PredefinedFn.attr_getter(
|
759
|
+
expr.ExprIdent("session"),
|
760
|
+
expr.ExprIdent("rollback"),
|
761
|
+
),
|
762
|
+
[],
|
763
|
+
)
|
764
|
+
)
|
765
|
+
),
|
766
|
+
stmt.PythonStatement("raise"),
|
767
|
+
),
|
768
|
+
lambda ast_l1: ast_l1.else_()(
|
769
|
+
stmt.SingleExprStatement(
|
770
|
+
expr.ExprAwait(
|
771
|
+
expr.ExprFuncCall(
|
772
|
+
PredefinedFn.attr_getter(
|
773
|
+
expr.ExprIdent("session"), expr.ExprIdent("execute")
|
774
|
+
),
|
775
|
+
[
|
776
|
+
expr.ExprFuncCall(
|
777
|
+
expr.ExprIdent("text"),
|
778
|
+
[expr.ExprConstant("RESET ROLE;")],
|
779
|
+
)
|
780
|
+
],
|
781
|
+
)
|
782
|
+
)
|
783
|
+
),
|
784
|
+
stmt.SingleExprStatement(
|
785
|
+
expr.ExprAwait(
|
786
|
+
expr.ExprFuncCall(
|
787
|
+
PredefinedFn.attr_getter(
|
788
|
+
expr.ExprIdent("session"), expr.ExprIdent("commit")
|
789
|
+
),
|
790
|
+
[],
|
791
|
+
)
|
792
|
+
)
|
793
|
+
),
|
794
|
+
),
|
697
795
|
)
|
698
796
|
)
|
699
797
|
|
700
798
|
program.root.linebreak()
|
701
799
|
program.root.python_stmt("@contextmanager")
|
702
800
|
program.root.func("get_session", [])(
|
703
|
-
lambda
|
704
|
-
|
801
|
+
lambda ast: ast.python_stmt(
|
802
|
+
"with Session(engine, expire_on_commit=False) as session:"
|
803
|
+
)(
|
804
|
+
lambda ast_l1: ast_l1.try_()(stmt.PythonStatement("yield session")),
|
805
|
+
lambda ast_l1: ast_l1.catch()(
|
806
|
+
stmt.SingleExprStatement(
|
807
|
+
expr.ExprFuncCall(
|
808
|
+
PredefinedFn.attr_getter(
|
809
|
+
expr.ExprIdent("session"), expr.ExprIdent("rollback")
|
810
|
+
),
|
811
|
+
[],
|
812
|
+
)
|
813
|
+
),
|
814
|
+
stmt.PythonStatement("raise"),
|
815
|
+
),
|
816
|
+
lambda ast_l1: ast_l1.else_()(
|
817
|
+
stmt.SingleExprStatement(
|
818
|
+
expr.ExprFuncCall(
|
819
|
+
PredefinedFn.attr_getter(
|
820
|
+
expr.ExprIdent("session"), expr.ExprIdent("execute")
|
821
|
+
),
|
822
|
+
[
|
823
|
+
expr.ExprFuncCall(
|
824
|
+
expr.ExprIdent("text"),
|
825
|
+
[expr.ExprConstant("RESET ROLE;")],
|
826
|
+
)
|
827
|
+
],
|
828
|
+
)
|
829
|
+
),
|
830
|
+
stmt.SingleExprStatement(
|
831
|
+
expr.ExprFuncCall(
|
832
|
+
PredefinedFn.attr_getter(
|
833
|
+
expr.ExprIdent("session"), expr.ExprIdent("commit")
|
834
|
+
),
|
835
|
+
[],
|
836
|
+
)
|
837
|
+
),
|
838
|
+
),
|
705
839
|
)
|
706
840
|
)
|
707
841
|
|
@@ -4,6 +4,7 @@ from typing import Sequence
|
|
4
4
|
|
5
5
|
from codegen.models import DeferredVar, Program, expr, stmt
|
6
6
|
from loguru import logger
|
7
|
+
|
7
8
|
from sera.misc import assert_not_null
|
8
9
|
from sera.models import App, DataCollection, Package
|
9
10
|
|
@@ -35,13 +36,13 @@ def make_python_service(collection: DataCollection, target_pkg: Package):
|
|
35
36
|
True,
|
36
37
|
)
|
37
38
|
program.import_(app.config.path + f".schema", True)
|
38
|
-
program.import_("sera.libs.base_service.
|
39
|
+
program.import_("sera.libs.base_service.BaseAsyncService", True)
|
39
40
|
|
40
41
|
program.root(
|
41
42
|
stmt.LineBreak(),
|
42
43
|
lambda ast00: ast00.class_(
|
43
44
|
collection.get_service_name(),
|
44
|
-
[expr.ExprIdent(f"
|
45
|
+
[expr.ExprIdent(f"BaseAsyncService[{id_type}, {cls.name}]")],
|
45
46
|
)(
|
46
47
|
lambda ast01: ast01.func(
|
47
48
|
"__init__",
|
sera/models/__init__.py
CHANGED
@@ -5,7 +5,13 @@ from sera.models._enum import Enum
|
|
5
5
|
from sera.models._module import App, Module, Package
|
6
6
|
from sera.models._multi_lingual_string import MultiLingualString
|
7
7
|
from sera.models._parse import parse_schema
|
8
|
-
from sera.models._property import
|
8
|
+
from sera.models._property import (
|
9
|
+
Cardinality,
|
10
|
+
DataProperty,
|
11
|
+
ObjectProperty,
|
12
|
+
Property,
|
13
|
+
SystemControlledMode,
|
14
|
+
)
|
9
15
|
from sera.models._schema import Schema
|
10
16
|
|
11
17
|
__all__ = [
|
@@ -25,4 +31,5 @@ __all__ = [
|
|
25
31
|
"PyTypeWithDep",
|
26
32
|
"TsTypeWithDep",
|
27
33
|
"Enum",
|
34
|
+
"SystemControlledMode",
|
28
35
|
]
|
sera/models/_constraints.py
CHANGED
@@ -3,7 +3,15 @@ from __future__ import annotations
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
from typing import Literal
|
5
5
|
|
6
|
-
ConstraintName = Literal[
|
6
|
+
ConstraintName = Literal[
|
7
|
+
"phone_number",
|
8
|
+
"email",
|
9
|
+
"not_empty",
|
10
|
+
"username",
|
11
|
+
"password",
|
12
|
+
"whole_number",
|
13
|
+
"positive_number",
|
14
|
+
]
|
7
15
|
|
8
16
|
|
9
17
|
@dataclass
|
@@ -25,6 +33,10 @@ class Constraint:
|
|
25
33
|
)
|
26
34
|
elif self.name == "password":
|
27
35
|
return "msgspec.Meta(min_length=8, max_length=40)"
|
36
|
+
elif self.name == "whole_number":
|
37
|
+
return "msgspec.Meta(ge=0)"
|
38
|
+
elif self.name == "positive_number":
|
39
|
+
return "msgspec.Meta(gt=0)"
|
28
40
|
|
29
41
|
raise NotImplementedError()
|
30
42
|
|
@@ -43,4 +55,6 @@ predefined_constraints: dict[ConstraintName, Constraint] = {
|
|
43
55
|
"not_empty": Constraint("not_empty", ()),
|
44
56
|
"username": Constraint("username", ()),
|
45
57
|
"password": Constraint("password", ()),
|
58
|
+
"whole_number": Constraint("whole_number", ()),
|
59
|
+
"positive_number": Constraint("positive_number", ()),
|
46
60
|
}
|
sera/models/_datatype.py
CHANGED
@@ -164,6 +164,16 @@ predefined_datatypes = {
|
|
164
164
|
tstype=TsTypeWithDep(type="number"),
|
165
165
|
is_list=False,
|
166
166
|
),
|
167
|
+
"date": DataType(
|
168
|
+
pytype=PyTypeWithDep(type="date", deps=["datetime.date"]),
|
169
|
+
sqltype=SQLTypeWithDep(
|
170
|
+
type="Date",
|
171
|
+
mapped_pytype="date",
|
172
|
+
deps=["sqlalchemy.Date", "datetime.date"],
|
173
|
+
),
|
174
|
+
tstype=TsTypeWithDep(type="string"),
|
175
|
+
is_list=False,
|
176
|
+
),
|
167
177
|
"datetime": DataType(
|
168
178
|
pytype=PyTypeWithDep(type="datetime", deps=["datetime.datetime"]),
|
169
179
|
sqltype=SQLTypeWithDep(
|
sera/models/_parse.py
CHANGED
@@ -31,6 +31,7 @@ from sera.models._property import (
|
|
31
31
|
ObjectPropDBInfo,
|
32
32
|
ObjectProperty,
|
33
33
|
PropDataAttrs,
|
34
|
+
SystemControlledMode,
|
34
35
|
)
|
35
36
|
from sera.models._schema import Schema
|
36
37
|
|
@@ -76,7 +77,7 @@ def _parse_class_without_prop(schema: Schema, clsname: str, cls: dict) -> Class:
|
|
76
77
|
return Class(
|
77
78
|
name=clsname,
|
78
79
|
label=_parse_multi_lingual_string(cls["label"]),
|
79
|
-
description=_parse_multi_lingual_string(cls
|
80
|
+
description=_parse_multi_lingual_string(cls.get("desc", "")),
|
80
81
|
properties={},
|
81
82
|
db=db,
|
82
83
|
)
|
@@ -131,7 +132,9 @@ def _parse_property(
|
|
131
132
|
constraints=[
|
132
133
|
_parse_constraint(constraint) for constraint in _data.get("constraints", [])
|
133
134
|
],
|
134
|
-
is_system_controlled=
|
135
|
+
is_system_controlled=SystemControlledMode(
|
136
|
+
_data.get("is_system_controlled", SystemControlledMode.NO.value)
|
137
|
+
),
|
135
138
|
)
|
136
139
|
|
137
140
|
assert isinstance(prop, dict), prop
|
@@ -222,12 +225,14 @@ def _parse_datatype(schema: Schema, datatype: dict | str) -> DataType:
|
|
222
225
|
# the correct package yet.
|
223
226
|
pytype=PyTypeWithDep(
|
224
227
|
type=enum.name,
|
225
|
-
|
228
|
+
deps=[
|
229
|
+
f"{schema.name}.models.enums.{enum.get_pymodule_name()}.{enum.name}"
|
230
|
+
],
|
226
231
|
),
|
227
232
|
sqltype=SQLTypeWithDep(
|
228
233
|
type="String", mapped_pytype="str", deps=["sqlalchemy.String"]
|
229
234
|
),
|
230
|
-
tstype=TsTypeWithDep(type=enum.name,
|
235
|
+
tstype=TsTypeWithDep(type=enum.name, deps=["@/models/enums"]),
|
231
236
|
is_list=is_list,
|
232
237
|
)
|
233
238
|
|
sera/models/_property.py
CHANGED
@@ -56,6 +56,19 @@ class Cardinality(str, Enum):
|
|
56
56
|
]
|
57
57
|
|
58
58
|
|
59
|
+
class SystemControlledMode(str, Enum):
|
60
|
+
"""Indicates if this property is controlled by the system.
|
61
|
+
|
62
|
+
There are two modes:
|
63
|
+
1. The system automatically sets the value, and users cannot modify it.
|
64
|
+
2. Users with special roles are allowed to set the value and other users cannot modify it
|
65
|
+
"""
|
66
|
+
|
67
|
+
AUTO = "auto"
|
68
|
+
RESTRICTED = "restricted"
|
69
|
+
NO = "no"
|
70
|
+
|
71
|
+
|
59
72
|
@dataclass(kw_only=True)
|
60
73
|
class PropDataAttrs:
|
61
74
|
"""Storing other attributes for generating data model (upsert & public) -- this is different from a db model"""
|
@@ -72,7 +85,7 @@ class PropDataAttrs:
|
|
72
85
|
constraints: list[Constraint] = field(default_factory=list)
|
73
86
|
|
74
87
|
# whether this property is controlled by the system or not
|
75
|
-
is_system_controlled:
|
88
|
+
is_system_controlled: SystemControlledMode = SystemControlledMode.NO
|
76
89
|
|
77
90
|
|
78
91
|
@dataclass(kw_only=True)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: sera-2
|
3
|
-
Version: 1.12.
|
3
|
+
Version: 1.12.6
|
4
4
|
Summary:
|
5
5
|
Author: Binh Vu
|
6
6
|
Author-email: bvu687@gmail.com
|
@@ -9,13 +9,13 @@ Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.12
|
10
10
|
Classifier: Programming Language :: Python :: 3.13
|
11
11
|
Requires-Dist: black (>=25.1.0,<26.0.0)
|
12
|
-
Requires-Dist: codegen-2 (>=2.
|
12
|
+
Requires-Dist: codegen-2 (>=2.11.0,<3.0.0)
|
13
13
|
Requires-Dist: isort (>=6.0.1,<7.0.0)
|
14
14
|
Requires-Dist: litestar (>=2.15.1,<3.0.0)
|
15
15
|
Requires-Dist: loguru (>=0.7.0,<0.8.0)
|
16
16
|
Requires-Dist: msgspec (>=0.19.0,<0.20.0)
|
17
17
|
Requires-Dist: serde2 (>=1.9.0,<2.0.0)
|
18
|
-
Requires-Dist: sqlalchemy (>=2.0.
|
18
|
+
Requires-Dist: sqlalchemy[asyncio] (>=2.0.41,<3.0.0)
|
19
19
|
Requires-Dist: typer (>=0.12.3,<0.13.0)
|
20
20
|
Project-URL: Repository, https://github.com/binh-vu/sera
|
21
21
|
Description-Content-Type: text/markdown
|
@@ -2,36 +2,36 @@ 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
4
|
sera/libs/api_helper.py,sha256=47y1kcwk3Xd2ZEMnUj_0OwCuUmgwOs5kYrE95BDVUn4,5411
|
5
|
-
sera/libs/base_orm.py,sha256=
|
6
|
-
sera/libs/base_service.py,sha256=
|
5
|
+
sera/libs/base_orm.py,sha256=5hOH_diUeaABm3cpE2-9u50VRqG1QW2osPQnvVHIhIA,3365
|
6
|
+
sera/libs/base_service.py,sha256=AX1WoTHte6Z_birkkfagkNE6BrCLTlTjQE4jEsKEaAY,5152
|
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
9
|
sera/libs/middlewares/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
sera/libs/middlewares/auth.py,sha256=
|
10
|
+
sera/libs/middlewares/auth.py,sha256=r6aix1ZBwxMd1Jv5hMCTB8a_gFOJQ6egvxIrf3DWEOs,2323
|
11
11
|
sera/libs/middlewares/uscp.py,sha256=H5umW8iEQSCdb_MJ5Im49kxg1E7TpxSg1p2_2A5zI1U,2600
|
12
12
|
sera/make/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
-
sera/make/__main__.py,sha256=
|
13
|
+
sera/make/__main__.py,sha256=bt-gDF8E026OWc2zqr9_a3paMOiDkFd3ybWn8ltL2g0,1448
|
14
14
|
sera/make/make_app.py,sha256=n9NtW73O3s_5Q31VHIRmnd-jEIcpDO7ksAsOdovde2s,5999
|
15
|
-
sera/make/make_python_api.py,sha256=
|
16
|
-
sera/make/make_python_model.py,sha256=
|
17
|
-
sera/make/make_python_services.py,sha256=
|
15
|
+
sera/make/make_python_api.py,sha256=sf-J5Pt1LTyM_H-SgXSAvKjEMDrRA6WnDpgDPbJG360,26896
|
16
|
+
sera/make/make_python_model.py,sha256=XWoNmfVNfdzF3lwEaItPZjo27ibqL9yjHA3a0vrHsnA,51708
|
17
|
+
sera/make/make_python_services.py,sha256=0ZpWLwQ7Nwfn8BXAikAB4JRpNknpSJyJgY5b1cjtxV4,2073
|
18
18
|
sera/make/make_typescript_model.py,sha256=ugDdSTw_1ayHLuL--92RQ8hf_D-dpJtnvmUZNxcwcDs,63687
|
19
19
|
sera/misc/__init__.py,sha256=Dh4uDq0D4N53h3zhvmwfa5a0TPVRSUvLzb0hkFuPirk,411
|
20
20
|
sera/misc/_formatter.py,sha256=aCGYL08l8f3aLODHxSocxBBwkRYEo3K1QzCDEn3suj0,1685
|
21
21
|
sera/misc/_utils.py,sha256=V5g4oLGHOhUCR75Kkcn1w01pAvGvaepK-T8Z3pIgHjI,1450
|
22
|
-
sera/models/__init__.py,sha256=
|
22
|
+
sera/models/__init__.py,sha256=VcC7HvqXuYrkgXwzs2vOH6LJPpzFBkeDvYVNrd3P-6E,855
|
23
23
|
sera/models/_class.py,sha256=Wf0e8x6-szG9TzoFkAlqj7_dG0SCICMBw_333n3paxk,2514
|
24
24
|
sera/models/_collection.py,sha256=ZnQEriKC4X88Zz48Kn1AVZKH-1_l8OgWa-zf2kcQOOE,1414
|
25
|
-
sera/models/_constraints.py,sha256=
|
26
|
-
sera/models/_datatype.py,sha256=
|
25
|
+
sera/models/_constraints.py,sha256=PsSOX94b9-73wZTcUif9KTzV9uTfoF0WN87g4GZXQmU,1827
|
26
|
+
sera/models/_datatype.py,sha256=y5kfim0G3gLhnGjiokFBr8leU1Y6as_Vw7oK-caOo68,7140
|
27
27
|
sera/models/_default.py,sha256=ABggW6qdPR4ZDqIPJdJ0GCGQ-7kfsfZmQ_DchgZEa-I,137
|
28
28
|
sera/models/_enum.py,sha256=sy0q7E646F-APsqrVQ52r1fAQ_DCAeaNq5YM5QN3zIk,2070
|
29
29
|
sera/models/_module.py,sha256=8QRSCubZmdDP9rL58rGAS6X5VCrkc1ZHvuMu1I1KrWk,5043
|
30
30
|
sera/models/_multi_lingual_string.py,sha256=JETN6k00VH4wrA4w5vAHMEJV8fp3SY9bJebskFTjQLA,1186
|
31
|
-
sera/models/_parse.py,sha256=
|
32
|
-
sera/models/_property.py,sha256=
|
31
|
+
sera/models/_parse.py,sha256=q_YZ7PrHWIN85_WW-fPP7-2gLXlGWM2-EIdbYXuG7Xg,10052
|
32
|
+
sera/models/_property.py,sha256=4y9F58D6DoX25-6aWPBRiE72nCPQy0KWlGNDTZXSV-8,6038
|
33
33
|
sera/models/_schema.py,sha256=r-Gqg9Lb_wR3UrbNvfXXgt_qs5bts0t2Ve7aquuF_OI,1155
|
34
34
|
sera/typing.py,sha256=Q4QMfbtfrCjC9tFfsZPhsAnbNX4lm4NHQ9lmjNXYdV0,772
|
35
|
-
sera_2-1.12.
|
36
|
-
sera_2-1.12.
|
37
|
-
sera_2-1.12.
|
35
|
+
sera_2-1.12.6.dist-info/METADATA,sha256=MlOqmC3MJASvXLuS8k0KO5UE8325j9Ff53RArvIn5Wo,867
|
36
|
+
sera_2-1.12.6.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
37
|
+
sera_2-1.12.6.dist-info/RECORD,,
|
File without changes
|