sera-2 1.2.1__py3-none-any.whl → 1.4.3__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/constants.py +4 -0
- sera/libs/base_orm.py +1 -1
- sera/libs/base_service.py +44 -8
- sera/make/__main__.py +14 -1
- sera/make/make_app.py +46 -16
- sera/make/make_python_api.py +63 -44
- sera/make/make_python_model.py +262 -120
- sera/make/make_typescript_model.py +846 -0
- sera/misc/__init__.py +4 -3
- sera/misc/_utils.py +12 -0
- sera/models/__init__.py +3 -1
- sera/models/_class.py +18 -1
- sera/models/_collection.py +10 -2
- sera/models/_datatype.py +150 -45
- sera/models/_module.py +37 -6
- sera/models/_parse.py +69 -18
- sera/models/_property.py +27 -3
- sera/typing.py +3 -0
- {sera_2-1.2.1.dist-info → sera_2-1.4.3.dist-info}/METADATA +4 -4
- sera_2-1.4.3.dist-info/RECORD +28 -0
- {sera_2-1.2.1.dist-info → sera_2-1.4.3.dist-info}/WHEEL +1 -1
- sera/misc/_rdf.py +0 -60
- sera/namespace.py +0 -5
- sera_2-1.2.1.dist-info/RECORD +0 -29
sera/constants.py
ADDED
sera/libs/base_orm.py
CHANGED
@@ -73,7 +73,7 @@ class ListDataclassType(TypeDecorator):
|
|
73
73
|
return [self.cls.from_dict(x) for x in result]
|
74
74
|
|
75
75
|
|
76
|
-
class
|
76
|
+
class DictDataclassType(TypeDecorator):
|
77
77
|
"""SqlAlchemy Type decorator to serialize mapping of dataclasses"""
|
78
78
|
|
79
79
|
impl = LargeBinary
|
sera/libs/base_service.py
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from enum import Enum
|
4
|
-
from
|
4
|
+
from math import dist
|
5
|
+
from typing import Annotated, Any, Generic, NamedTuple, Optional, Sequence, TypeVar
|
5
6
|
|
6
7
|
from sera.libs.base_orm import BaseORM
|
7
8
|
from sera.misc import assert_not_null
|
8
9
|
from sera.models import Class
|
9
10
|
from sera.typing import FieldName, T, doc
|
10
|
-
from sqlalchemy import exists, select
|
11
|
-
from sqlalchemy.orm import Session
|
11
|
+
from sqlalchemy import Result, Select, exists, func, select
|
12
|
+
from sqlalchemy.orm import Session, load_only
|
12
13
|
|
13
14
|
|
14
15
|
class QueryOp(str, Enum):
|
@@ -31,6 +32,12 @@ Query = Annotated[
|
|
31
32
|
]
|
32
33
|
R = TypeVar("R", bound=BaseORM)
|
33
34
|
ID = TypeVar("ID") # ID of a class
|
35
|
+
SqlResult = TypeVar("SqlResult", bound=Result)
|
36
|
+
|
37
|
+
|
38
|
+
class QueryResult(NamedTuple, Generic[R]):
|
39
|
+
records: Sequence[R]
|
40
|
+
total: int
|
34
41
|
|
35
42
|
|
36
43
|
class BaseService(Generic[ID, R]):
|
@@ -52,7 +59,7 @@ class BaseService(Generic[ID, R]):
|
|
52
59
|
group_by: list[str],
|
53
60
|
fields: list[str],
|
54
61
|
session: Session,
|
55
|
-
) ->
|
62
|
+
) -> QueryResult[R]:
|
56
63
|
"""Retrieving records matched a query.
|
57
64
|
|
58
65
|
Args:
|
@@ -62,14 +69,35 @@ class BaseService(Generic[ID, R]):
|
|
62
69
|
unique: Whether to return unique results only
|
63
70
|
sorted_by: list of field names to sort by, prefix a field with '-' to sort that field in descending order
|
64
71
|
group_by: list of field names to group by
|
65
|
-
fields: list of field names to include in the results
|
72
|
+
fields: list of field names to include in the results -- empty means all fields
|
66
73
|
"""
|
67
|
-
|
74
|
+
q = self._select()
|
75
|
+
if fields:
|
76
|
+
q = q.options(
|
77
|
+
load_only(*[getattr(self.orm_cls, field) for field in fields])
|
78
|
+
)
|
79
|
+
if unique:
|
80
|
+
q = q.distinct()
|
81
|
+
if sorted_by:
|
82
|
+
for field in sorted_by:
|
83
|
+
if field.startswith("-"):
|
84
|
+
q = q.order_by(getattr(self.orm_cls, field[1:]).desc())
|
85
|
+
else:
|
86
|
+
q = q.order_by(getattr(self.orm_cls, field))
|
87
|
+
if group_by:
|
88
|
+
for field in group_by:
|
89
|
+
q = q.group_by(getattr(self.orm_cls, field))
|
90
|
+
|
91
|
+
cq = select(func.count()).select_from(q.subquery())
|
92
|
+
rq = q.limit(limit).offset(offset)
|
93
|
+
records = self._process_result(session.execute(q)).scalars().all()
|
94
|
+
total = session.execute(cq).scalar_one()
|
95
|
+
return QueryResult(records, total)
|
68
96
|
|
69
97
|
def get_by_id(self, id: ID, session: Session) -> Optional[R]:
|
70
98
|
"""Retrieving a record by ID."""
|
71
|
-
q =
|
72
|
-
result = session.execute(q).scalar_one_or_none()
|
99
|
+
q = self._select().where(self._cls_id_prop == id)
|
100
|
+
result = self._process_result(session.execute(q)).scalar_one_or_none()
|
73
101
|
return result
|
74
102
|
|
75
103
|
def has_id(self, id: ID, session: Session) -> bool:
|
@@ -89,3 +117,11 @@ class BaseService(Generic[ID, R]):
|
|
89
117
|
session.add(record)
|
90
118
|
session.commit()
|
91
119
|
return record
|
120
|
+
|
121
|
+
def _select(self) -> Select:
|
122
|
+
"""Get the select statement for the class."""
|
123
|
+
return select(self.orm_cls)
|
124
|
+
|
125
|
+
def _process_result(self, result: SqlResult) -> SqlResult:
|
126
|
+
"""Process the result of a query."""
|
127
|
+
return result
|
sera/make/__main__.py
CHANGED
@@ -5,6 +5,7 @@ from typing import Annotated
|
|
5
5
|
|
6
6
|
import typer
|
7
7
|
from sera.make.make_app import make_app
|
8
|
+
from sera.models import Language
|
8
9
|
|
9
10
|
app = typer.Typer(pretty_exceptions_short=True, pretty_exceptions_enable=False)
|
10
11
|
|
@@ -29,10 +30,22 @@ def cli(
|
|
29
30
|
help="API collections to generate.",
|
30
31
|
),
|
31
32
|
],
|
33
|
+
language: Annotated[
|
34
|
+
Language,
|
35
|
+
typer.Option("-l", "--language", help="Language of the generated application"),
|
36
|
+
] = Language.Python,
|
37
|
+
referenced_schema: Annotated[
|
38
|
+
list[str],
|
39
|
+
typer.Option(
|
40
|
+
"-rs",
|
41
|
+
"--referenced-schema",
|
42
|
+
help="Classes in the schema that are defined in different modules, used as references and thus should not be generated.",
|
43
|
+
),
|
44
|
+
] = [],
|
32
45
|
):
|
33
46
|
"""Generate Python model classes from a schema file."""
|
34
47
|
typer.echo(f"Generating application in {app_dir}")
|
35
|
-
make_app(app_dir, schema_files, api_collections)
|
48
|
+
make_app(app_dir, schema_files, api_collections, language, referenced_schema)
|
36
49
|
|
37
50
|
|
38
51
|
app()
|
sera/make/make_app.py
CHANGED
@@ -13,6 +13,7 @@ from sera.make.make_python_model import (
|
|
13
13
|
make_python_relational_model,
|
14
14
|
)
|
15
15
|
from sera.make.make_python_services import make_python_service_structure
|
16
|
+
from sera.make.make_typescript_model import make_typescript_data_model
|
16
17
|
from sera.models import App, DataCollection, Language, parse_schema
|
17
18
|
|
18
19
|
|
@@ -62,8 +63,8 @@ def make_config(app: App):
|
|
62
63
|
expr.ExprIdent('os.environ.get("DB_DEBUG", "0") == "1"'),
|
63
64
|
),
|
64
65
|
lambda ast: ast.assign(
|
65
|
-
DeferredVar.simple("
|
66
|
-
expr.ExprIdent('os.environ.get("
|
66
|
+
DeferredVar.simple("API_DEBUG"),
|
67
|
+
expr.ExprIdent('os.environ.get("API_DEBUG", "0") == "1"'),
|
67
68
|
),
|
68
69
|
stmt.LineBreak(),
|
69
70
|
lambda ast: ast.assign(
|
@@ -113,25 +114,54 @@ def make_app(
|
|
113
114
|
"Language of the generated application. Currently only Python is supported."
|
114
115
|
),
|
115
116
|
] = Language.Python,
|
117
|
+
referenced_schema: Annotated[
|
118
|
+
list[str],
|
119
|
+
doc(
|
120
|
+
"Classes in the schema that are defined in different modules, used as references and thus should not be generated."
|
121
|
+
),
|
122
|
+
] = [],
|
116
123
|
):
|
117
124
|
schema = parse_schema(schema_files)
|
118
125
|
|
119
126
|
app = App(app_dir.name, app_dir, schema_files, language)
|
120
127
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
128
|
+
if language == Language.Python:
|
129
|
+
# generate application configuration
|
130
|
+
make_config(app)
|
131
|
+
|
132
|
+
# generate models from schema
|
133
|
+
# TODO: fix me, this is a hack to make the code work for referenced classes
|
134
|
+
referenced_classes = {
|
135
|
+
path.rsplit(".", 1)[1]: (parts := path.rsplit(".", 1))[0]
|
136
|
+
+ ".data."
|
137
|
+
+ parts[1]
|
138
|
+
for path in referenced_schema
|
139
|
+
}
|
140
|
+
make_python_data_model(schema, app.models.pkg("data"), referenced_classes)
|
141
|
+
referenced_classes = {
|
142
|
+
path.rsplit(".", 1)[1]: (parts := path.rsplit(".", 1))[0]
|
143
|
+
+ ".db."
|
144
|
+
+ parts[1]
|
145
|
+
for path in referenced_schema
|
146
|
+
}
|
147
|
+
make_python_relational_model(
|
148
|
+
schema,
|
149
|
+
app.models.pkg("db"),
|
150
|
+
app.models.pkg("data"),
|
151
|
+
referenced_classes,
|
152
|
+
)
|
153
|
+
|
154
|
+
collections = [
|
155
|
+
DataCollection(schema.classes[cname]) for cname in api_collections
|
156
|
+
]
|
157
|
+
|
158
|
+
# generate API
|
159
|
+
make_python_api(app, collections)
|
160
|
+
|
161
|
+
# generate services
|
162
|
+
make_python_service_structure(app, collections)
|
163
|
+
elif language == Language.Typescript:
|
164
|
+
make_typescript_data_model(schema, app.models)
|
135
165
|
|
136
166
|
return app
|
137
167
|
|
sera/make/make_python_api.py
CHANGED
@@ -27,41 +27,43 @@ def make_python_api(app: App, collections: Sequence[DataCollection]):
|
|
27
27
|
controllers.append(make_python_create_api(collection, route))
|
28
28
|
controllers.append(make_python_update_api(collection, route))
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
program.import_(
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
30
|
+
routemod = route.module("route")
|
31
|
+
if not routemod.exists():
|
32
|
+
program = Program()
|
33
|
+
program.import_("__future__.annotations", True)
|
34
|
+
program.import_("litestar.Router", True)
|
35
|
+
for get_route, get_route_fn in controllers:
|
36
|
+
program.import_(get_route.path + "." + get_route_fn, True)
|
37
|
+
|
38
|
+
program.root(
|
39
|
+
stmt.LineBreak(),
|
40
|
+
lambda ast: ast.assign(
|
41
|
+
DeferredVar.simple("router"),
|
42
|
+
expr.ExprFuncCall(
|
43
|
+
expr.ExprIdent("Router"),
|
44
|
+
[
|
45
|
+
PredefinedFn.keyword_assignment(
|
46
|
+
"path",
|
47
|
+
expr.ExprConstant(
|
48
|
+
f"/api/{to_snake_case(collection.name).replace('_', '-')}"
|
49
|
+
),
|
47
50
|
),
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
51
|
+
PredefinedFn.keyword_assignment(
|
52
|
+
"route_handlers",
|
53
|
+
PredefinedFn.list(
|
54
|
+
[
|
55
|
+
expr.ExprIdent(get_route_fn)
|
56
|
+
for get_route, get_route_fn in controllers
|
57
|
+
]
|
58
|
+
),
|
56
59
|
),
|
57
|
-
|
58
|
-
|
60
|
+
],
|
61
|
+
),
|
59
62
|
),
|
60
|
-
)
|
61
|
-
)
|
63
|
+
)
|
62
64
|
|
63
|
-
|
64
|
-
routes.append(
|
65
|
+
routemod.write(program)
|
66
|
+
routes.append(routemod)
|
65
67
|
|
66
68
|
# make the main entry point
|
67
69
|
make_main(app.api, routes)
|
@@ -153,7 +155,7 @@ def make_python_get_api(
|
|
153
155
|
program.import_(app.models.db.path + ".base.get_session", True)
|
154
156
|
program.import_("litestar.di.Provide", True)
|
155
157
|
program.import_("sqlalchemy.orm.Session", True)
|
156
|
-
program.import_(app.config.path + ".
|
158
|
+
program.import_(app.config.path + ".API_DEBUG", True)
|
157
159
|
program.import_(
|
158
160
|
f"{app.api.path}.dependencies.{collection.get_pymodule_name()}.{ServiceNameDep}",
|
159
161
|
True,
|
@@ -224,19 +226,19 @@ def make_python_get_api(
|
|
224
226
|
DeferredVar.simple(
|
225
227
|
"sorted_by",
|
226
228
|
expr.ExprIdent(
|
227
|
-
"Annotated[list[str], Parameter(description=\"list of field names to sort by, prefix a field with '-' to sort that field in descending order\")]"
|
229
|
+
"Annotated[list[str], Parameter(default=tuple(), description=\"list of field names to sort by, prefix a field with '-' to sort that field in descending order\")]"
|
228
230
|
),
|
229
231
|
),
|
230
232
|
DeferredVar.simple(
|
231
233
|
"group_by",
|
232
234
|
expr.ExprIdent(
|
233
|
-
'Annotated[list[str], Parameter(description="list of field names to group by")]'
|
235
|
+
'Annotated[list[str], Parameter(default=tuple(), description="list of field names to group by")]'
|
234
236
|
),
|
235
237
|
),
|
236
238
|
DeferredVar.simple(
|
237
239
|
"fields",
|
238
240
|
expr.ExprIdent(
|
239
|
-
'Annotated[list[str], Parameter(description="list of field names to include in the results")]'
|
241
|
+
'Annotated[list[str], Parameter(default=tuple(), description="list of field names to include in the results")]'
|
240
242
|
),
|
241
243
|
),
|
242
244
|
DeferredVar.simple(
|
@@ -252,7 +254,7 @@ def make_python_get_api(
|
|
252
254
|
expr.ExprIdent("Session"),
|
253
255
|
),
|
254
256
|
],
|
255
|
-
return_type=expr.ExprIdent(f"
|
257
|
+
return_type=expr.ExprIdent(f"dict"),
|
256
258
|
is_async=True,
|
257
259
|
)(
|
258
260
|
stmt.SingleExprStatement(
|
@@ -267,7 +269,7 @@ def make_python_get_api(
|
|
267
269
|
expr.ExprIdent("QUERYABLE_FIELDS"),
|
268
270
|
PredefinedFn.keyword_assignment(
|
269
271
|
"debug",
|
270
|
-
expr.ExprIdent("
|
272
|
+
expr.ExprIdent("API_DEBUG"),
|
271
273
|
),
|
272
274
|
],
|
273
275
|
),
|
@@ -303,14 +305,31 @@ def make_python_get_api(
|
|
303
305
|
),
|
304
306
|
),
|
305
307
|
lambda ast13: ast13.return_(
|
306
|
-
PredefinedFn.
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
308
|
+
PredefinedFn.dict(
|
309
|
+
[
|
310
|
+
(
|
311
|
+
PredefinedFn.attr_getter(
|
312
|
+
expr.ExprIdent(collection.name),
|
313
|
+
expr.ExprIdent("__name__"),
|
314
|
+
),
|
315
|
+
PredefinedFn.map_list(
|
316
|
+
PredefinedFn.attr_getter(
|
317
|
+
expr.ExprIdent("result"), expr.ExprIdent("records")
|
318
|
+
),
|
319
|
+
lambda item: expr.ExprMethodCall(
|
320
|
+
expr.ExprIdent(collection.name),
|
321
|
+
"from_db",
|
322
|
+
[item],
|
323
|
+
),
|
324
|
+
),
|
311
325
|
),
|
312
|
-
|
313
|
-
|
326
|
+
(
|
327
|
+
expr.ExprConstant("total"),
|
328
|
+
PredefinedFn.attr_getter(
|
329
|
+
expr.ExprIdent("result"), expr.ExprIdent("total")
|
330
|
+
),
|
331
|
+
),
|
332
|
+
]
|
314
333
|
)
|
315
334
|
),
|
316
335
|
),
|