sera-2 1.1.0__py3-none-any.whl → 1.2.0__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/.DS_Store +0 -0
- sera/libs/base_orm.py +3 -4
- sera/libs/base_service.py +19 -6
- sera/make/.DS_Store +0 -0
- sera/make/make_app.py +9 -3
- sera/make/make_python_api.py +562 -7
- sera/make/make_python_model.py +480 -94
- sera/models/__init__.py +2 -1
- sera/models/_class.py +2 -0
- sera/models/_datatype.py +49 -11
- sera/models/_module.py +12 -3
- sera/models/_parse.py +20 -19
- sera/models/_property.py +6 -2
- {sera_2-1.1.0.dist-info → sera_2-1.2.0.dist-info}/METADATA +1 -1
- sera_2-1.2.0.dist-info/RECORD +31 -0
- sera_2-1.1.0.dist-info/RECORD +0 -29
- {sera_2-1.1.0.dist-info → sera_2-1.2.0.dist-info}/WHEEL +0 -0
sera/make/make_python_model.py
CHANGED
@@ -1,21 +1,162 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from operator import is_
|
3
4
|
from typing import Sequence
|
4
5
|
|
5
|
-
from codegen.models import DeferredVar, PredefinedFn, Program, expr, stmt
|
6
|
-
from sera.misc import
|
7
|
-
|
6
|
+
from codegen.models import AST, DeferredVar, PredefinedFn, Program, expr, stmt
|
7
|
+
from sera.misc import (
|
8
|
+
assert_isinstance,
|
9
|
+
assert_not_null,
|
10
|
+
filter_duplication,
|
11
|
+
to_snake_case,
|
12
|
+
)
|
13
|
+
from sera.models import (
|
14
|
+
Cardinality,
|
15
|
+
Class,
|
16
|
+
DataProperty,
|
17
|
+
ObjectProperty,
|
18
|
+
Package,
|
19
|
+
Schema,
|
20
|
+
)
|
8
21
|
|
9
22
|
|
10
23
|
def make_python_data_model(schema: Schema, target_pkg: Package):
|
11
24
|
"""Generate public classes for the API from the schema."""
|
12
|
-
|
13
|
-
|
25
|
+
app = target_pkg.app
|
26
|
+
|
27
|
+
def from_db_type_conversion(
|
28
|
+
record: expr.ExprIdent, prop: DataProperty | ObjectProperty
|
29
|
+
):
|
30
|
+
value = PredefinedFn.attr_getter(record, expr.ExprIdent(prop.name))
|
31
|
+
if isinstance(prop, ObjectProperty) and prop.target.db is not None:
|
32
|
+
if prop.cardinality.is_star_to_many():
|
33
|
+
return PredefinedFn.map_list(
|
34
|
+
value,
|
35
|
+
lambda item: expr.ExprFuncCall(
|
36
|
+
PredefinedFn.attr_getter(
|
37
|
+
expr.ExprIdent(prop.target.name), expr.ExprIdent("from_db")
|
38
|
+
),
|
39
|
+
[PredefinedFn.attr_getter(item, expr.ExprIdent(prop.name))],
|
40
|
+
),
|
41
|
+
)
|
42
|
+
else:
|
43
|
+
return expr.ExprFuncCall(
|
44
|
+
PredefinedFn.attr_getter(
|
45
|
+
expr.ExprIdent(prop.target.name), expr.ExprIdent("from_db")
|
46
|
+
),
|
47
|
+
[value],
|
48
|
+
)
|
49
|
+
|
50
|
+
return value
|
51
|
+
|
52
|
+
def to_db_type_conversion(
|
53
|
+
program: Program,
|
54
|
+
slf: expr.ExprIdent,
|
55
|
+
cls: Class,
|
56
|
+
prop: DataProperty | ObjectProperty,
|
57
|
+
):
|
58
|
+
value = PredefinedFn.attr_getter(slf, expr.ExprIdent(prop.name))
|
59
|
+
if (
|
60
|
+
isinstance(prop, ObjectProperty)
|
61
|
+
and prop.target.db is not None
|
62
|
+
and prop.cardinality == Cardinality.MANY_TO_MANY
|
63
|
+
):
|
64
|
+
# we have to use the associated object
|
65
|
+
AssociationTable = f"{cls.name}{prop.target.name}"
|
66
|
+
program.import_(
|
67
|
+
app.models.db.path
|
68
|
+
+ f".{to_snake_case(AssociationTable)}.{AssociationTable}",
|
69
|
+
True,
|
70
|
+
)
|
71
|
+
return PredefinedFn.map_list(
|
72
|
+
value,
|
73
|
+
lambda item: expr.ExprFuncCall(
|
74
|
+
expr.ExprIdent(AssociationTable),
|
75
|
+
[PredefinedFn.keyword_assignment(f"{prop.name}_id", item)],
|
76
|
+
),
|
77
|
+
)
|
78
|
+
return value
|
79
|
+
|
80
|
+
def make_create(program: Program, cls: Class):
|
14
81
|
program.import_("__future__.annotations", True)
|
15
82
|
program.import_("msgspec", False)
|
16
|
-
|
83
|
+
program.import_(
|
84
|
+
app.models.db.path + f".{cls.get_pymodule_name()}.{cls.name}",
|
85
|
+
True,
|
86
|
+
alias=f"{cls.name}DB",
|
87
|
+
)
|
88
|
+
cls_ast = program.root.class_(
|
89
|
+
"Upsert" + cls.name, [expr.ExprIdent("msgspec.Struct")]
|
90
|
+
)
|
91
|
+
for prop in cls.properties.values():
|
92
|
+
# this is a create object, so users can create private field
|
93
|
+
# hence, we do not check for prop.is_private
|
94
|
+
# if prop.is_private:
|
95
|
+
# continue
|
96
|
+
|
97
|
+
if isinstance(prop, DataProperty):
|
98
|
+
pytype = prop.datatype.get_python_type()
|
99
|
+
if pytype.dep is not None:
|
100
|
+
program.import_(pytype.dep, True)
|
101
|
+
cls_ast(stmt.DefClassVarStatement(prop.name, pytype.type))
|
102
|
+
elif isinstance(prop, ObjectProperty):
|
103
|
+
if prop.target.db is not None:
|
104
|
+
# if the target class is in the database, we expect the user to pass the foreign key for it.
|
105
|
+
pytype = (
|
106
|
+
assert_not_null(prop.target.get_id_property())
|
107
|
+
.datatype.get_python_type()
|
108
|
+
.type
|
109
|
+
)
|
110
|
+
else:
|
111
|
+
program.import_(
|
112
|
+
f"{target_pkg.module(prop.target.get_pymodule_name()).path}.{prop.target.name}",
|
113
|
+
is_import_attr=True,
|
114
|
+
)
|
115
|
+
pytype = prop.target.name
|
116
|
+
|
117
|
+
if prop.cardinality.is_star_to_many():
|
118
|
+
pytype = f"list[{pytype}]"
|
119
|
+
cls_ast(stmt.DefClassVarStatement(prop.name, pytype))
|
120
|
+
|
121
|
+
# has_to_db = True
|
122
|
+
# if any(prop for prop in cls.properties.values() if isinstance(prop, ObjectProperty) and prop.cardinality == Cardinality.MANY_TO_MANY):
|
123
|
+
# # if the class has many-to-many relationship, we need to
|
124
|
+
cls_ast(
|
125
|
+
stmt.LineBreak(),
|
126
|
+
lambda ast00: ast00.func(
|
127
|
+
"to_db",
|
128
|
+
[
|
129
|
+
DeferredVar.simple("self"),
|
130
|
+
],
|
131
|
+
return_type=expr.ExprIdent(f"{cls.name}DB"),
|
132
|
+
)(
|
133
|
+
lambda ast10: ast10.return_(
|
134
|
+
expr.ExprFuncCall(
|
135
|
+
expr.ExprIdent(f"{cls.name}DB"),
|
136
|
+
[
|
137
|
+
to_db_type_conversion(
|
138
|
+
program, expr.ExprIdent("self"), cls, prop
|
139
|
+
)
|
140
|
+
for prop in cls.properties.values()
|
141
|
+
],
|
142
|
+
)
|
143
|
+
)
|
144
|
+
),
|
145
|
+
)
|
146
|
+
|
147
|
+
def make_update(program: Program, cls: Class):
|
148
|
+
if not cls.is_public:
|
149
|
+
# skip classes that are not public
|
150
|
+
return
|
151
|
+
|
152
|
+
program.import_("__future__.annotations", True)
|
153
|
+
program.import_("msgspec", False)
|
154
|
+
program.import_(
|
155
|
+
app.models.db.path + f".{cls.get_pymodule_name()}.{cls.name}",
|
156
|
+
True,
|
157
|
+
alias=f"{cls.name}DB",
|
158
|
+
)
|
17
159
|
|
18
|
-
# program.root(stmt.PythonStatement("@dataclass"))
|
19
160
|
cls_ast = program.root.class_(cls.name, [expr.ExprIdent("msgspec.Struct")])
|
20
161
|
for prop in cls.properties.values():
|
21
162
|
if prop.is_private:
|
@@ -32,9 +173,42 @@ def make_python_data_model(schema: Schema, target_pkg: Package):
|
|
32
173
|
f"{target_pkg.module(prop.target.get_pymodule_name()).path}.{prop.target.name}",
|
33
174
|
is_import_attr=True,
|
34
175
|
)
|
35
|
-
|
176
|
+
if prop.cardinality.is_star_to_many():
|
177
|
+
pytype = f"list[{prop.target.name}]"
|
178
|
+
else:
|
179
|
+
pytype = prop.target.name
|
36
180
|
cls_ast(stmt.DefClassVarStatement(prop.name, pytype))
|
37
181
|
|
182
|
+
cls_ast(
|
183
|
+
stmt.LineBreak(),
|
184
|
+
stmt.PythonDecoratorStatement(
|
185
|
+
expr.ExprFuncCall(expr.ExprIdent("classmethod"), [])
|
186
|
+
),
|
187
|
+
lambda ast00: ast00.func(
|
188
|
+
"from_db",
|
189
|
+
[
|
190
|
+
DeferredVar.simple("cls"),
|
191
|
+
DeferredVar.simple("record", expr.ExprIdent(f"{cls.name}DB")),
|
192
|
+
],
|
193
|
+
)(
|
194
|
+
lambda ast10: ast10.return_(
|
195
|
+
expr.ExprFuncCall(
|
196
|
+
expr.ExprIdent("cls"),
|
197
|
+
[
|
198
|
+
from_db_type_conversion(expr.ExprIdent("record"), prop)
|
199
|
+
for prop in cls.properties.values()
|
200
|
+
if not prop.is_private
|
201
|
+
],
|
202
|
+
)
|
203
|
+
)
|
204
|
+
),
|
205
|
+
)
|
206
|
+
|
207
|
+
for cls in schema.topological_sort():
|
208
|
+
program = Program()
|
209
|
+
make_create(program, cls)
|
210
|
+
program.root.linebreak()
|
211
|
+
make_update(program, cls)
|
38
212
|
target_pkg.module(cls.get_pymodule_name()).write(program)
|
39
213
|
|
40
214
|
|
@@ -53,7 +227,7 @@ def make_python_relational_model(
|
|
53
227
|
program.import_("__future__.annotations", True)
|
54
228
|
program.import_("sera.libs.base_orm.BaseORM", True)
|
55
229
|
program.import_("sera.libs.base_orm.create_engine", True)
|
56
|
-
program.import_("
|
230
|
+
program.import_("sqlalchemy.orm.DeclarativeBase", True)
|
57
231
|
program.import_("sqlalchemy.orm.Session", True)
|
58
232
|
|
59
233
|
# assume configuration for the app at the top level
|
@@ -91,7 +265,9 @@ def make_python_relational_model(
|
|
91
265
|
|
92
266
|
type_map.append((expr.ExprIdent(type), expr.ExprIdent(maptype)))
|
93
267
|
|
94
|
-
cls_ast = program.root.class_(
|
268
|
+
cls_ast = program.root.class_(
|
269
|
+
"Base", [expr.ExprIdent("DeclarativeBase"), expr.ExprIdent("BaseORM")]
|
270
|
+
)(
|
95
271
|
stmt.DefClassVarStatement(
|
96
272
|
"type_annotation_map", "dict", PredefinedFn.dict(type_map)
|
97
273
|
),
|
@@ -118,8 +294,7 @@ def make_python_relational_model(
|
|
118
294
|
)
|
119
295
|
|
120
296
|
program.root.linebreak()
|
121
|
-
program.root.
|
122
|
-
program.root.func("get_session", [])(
|
297
|
+
program.root.func("get_session", [], is_async=True)(
|
123
298
|
lambda ast00: ast00.python_stmt("with Session(engine) as session:")(
|
124
299
|
lambda ast01: ast01.python_stmt("yield session")
|
125
300
|
)
|
@@ -139,7 +314,6 @@ def make_python_relational_model(
|
|
139
314
|
program.import_("sqlalchemy.orm.MappedAsDataclass", True)
|
140
315
|
program.import_("sqlalchemy.orm.mapped_column", True)
|
141
316
|
program.import_("sqlalchemy.orm.Mapped", True)
|
142
|
-
program.import_("typing.ClassVar", True)
|
143
317
|
program.import_(f"{target_pkg.path}.base.Base", True)
|
144
318
|
|
145
319
|
cls_ast = program.root.class_(
|
@@ -148,7 +322,7 @@ def make_python_relational_model(
|
|
148
322
|
cls_ast(
|
149
323
|
stmt.DefClassVarStatement(
|
150
324
|
"__tablename__",
|
151
|
-
type=
|
325
|
+
type=None,
|
152
326
|
value=expr.ExprConstant(cls.db.table_name),
|
153
327
|
),
|
154
328
|
stmt.LineBreak(),
|
@@ -189,94 +363,306 @@ def make_python_relational_model(
|
|
189
363
|
propval = expr.ExprFuncCall(
|
190
364
|
expr.ExprIdent("mapped_column"), propvalargs
|
191
365
|
)
|
366
|
+
cls_ast(stmt.DefClassVarStatement(propname, proptype, propval))
|
192
367
|
else:
|
193
368
|
assert isinstance(prop, ObjectProperty)
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
369
|
+
make_python_relational_object_property(
|
370
|
+
program=program,
|
371
|
+
target_pkg=target_pkg,
|
372
|
+
cls_ast=cls_ast,
|
373
|
+
cls=cls,
|
374
|
+
prop=prop,
|
375
|
+
custom_types=custom_types,
|
376
|
+
)
|
377
|
+
|
378
|
+
target_pkg.module(cls.get_pymodule_name()).write(program)
|
379
|
+
|
380
|
+
# make a base class that implements the mapping for custom types
|
381
|
+
custom_types = filter_duplication(
|
382
|
+
custom_types, lambda p: (p.target.name, p.cardinality, p.is_optional, p.is_map)
|
383
|
+
)
|
384
|
+
make_base(custom_types)
|
385
|
+
|
386
|
+
|
387
|
+
def make_python_relational_object_property(
|
388
|
+
program: Program,
|
389
|
+
target_pkg: Package,
|
390
|
+
cls_ast: AST,
|
391
|
+
cls: Class,
|
392
|
+
prop: ObjectProperty,
|
393
|
+
custom_types: list[ObjectProperty],
|
394
|
+
):
|
395
|
+
assert prop.db is not None
|
396
|
+
if prop.target.db is not None:
|
397
|
+
# if the target class is in the database, we generate a foreign key for it.
|
398
|
+
program.import_("sqlalchemy.orm.relationship", True)
|
399
|
+
|
400
|
+
if prop.cardinality == Cardinality.MANY_TO_MANY:
|
401
|
+
make_python_relational_object_property_many_to_many(
|
402
|
+
program, cls_ast, target_pkg, cls, prop
|
403
|
+
)
|
404
|
+
return
|
405
|
+
|
406
|
+
if prop.cardinality.is_star_to_many():
|
407
|
+
raise NotImplementedError()
|
408
|
+
|
409
|
+
# we store this class in the database
|
410
|
+
propname = f"{prop.name}_id"
|
411
|
+
idprop = prop.target.get_id_property()
|
412
|
+
assert idprop is not None
|
413
|
+
idprop_pytype = idprop.datatype.get_sqlalchemy_type()
|
414
|
+
if idprop_pytype.dep is not None:
|
415
|
+
program.import_(idprop_pytype.dep, True)
|
416
|
+
|
417
|
+
proptype = f"Mapped[{idprop_pytype.type}]"
|
418
|
+
propval = expr.ExprFuncCall(
|
419
|
+
expr.ExprIdent("mapped_column"),
|
420
|
+
[
|
421
|
+
expr.ExprFuncCall(
|
422
|
+
expr.ExprIdent("ForeignKey"),
|
423
|
+
[
|
424
|
+
expr.ExprConstant(f"{prop.target.db.table_name}.{idprop.name}"),
|
212
425
|
PredefinedFn.keyword_assignment(
|
213
426
|
"ondelete",
|
214
|
-
expr.ExprConstant(prop.db.
|
215
|
-
)
|
216
|
-
)
|
217
|
-
propvalargs.append(
|
427
|
+
expr.ExprConstant(prop.db.on_target_delete.to_sqlalchemy()),
|
428
|
+
),
|
218
429
|
PredefinedFn.keyword_assignment(
|
219
430
|
"onupdate",
|
220
|
-
expr.ExprConstant(prop.db.
|
221
|
-
)
|
222
|
-
|
431
|
+
expr.ExprConstant(prop.db.on_target_update.to_sqlalchemy()),
|
432
|
+
),
|
433
|
+
],
|
434
|
+
),
|
435
|
+
],
|
436
|
+
)
|
223
437
|
|
224
|
-
|
225
|
-
|
226
|
-
[
|
227
|
-
expr.ExprFuncCall(
|
228
|
-
expr.ExprIdent("ForeignKey"),
|
229
|
-
propvalargs,
|
230
|
-
),
|
231
|
-
],
|
232
|
-
)
|
233
|
-
else:
|
234
|
-
# if the target class is not in the database,
|
235
|
-
program.import_(
|
236
|
-
f"{target_pkg.module(prop.target.get_pymodule_name()).path}.{prop.target.name}",
|
237
|
-
is_import_attr=True,
|
238
|
-
)
|
239
|
-
propname = prop.name
|
240
|
-
proptype = f"Mapped[{prop.target.name}]"
|
241
|
-
|
242
|
-
# we have two choices, one is to create a composite class, one is to create a custom field
|
243
|
-
if prop.db.is_embedded == "composite":
|
244
|
-
# for a class to be composite, it must have only data properties
|
245
|
-
program.import_("sqlalchemy.orm.composite", True)
|
246
|
-
propvalargs = [expr.ExprIdent(prop.target.name)]
|
247
|
-
for p in prop.target.properties.values():
|
248
|
-
propvalargs.append(
|
249
|
-
expr.ExprFuncCall(
|
250
|
-
expr.ExprIdent("mapped_column"),
|
251
|
-
[
|
252
|
-
expr.ExprIdent(f"{prop.name}_{p.name}"),
|
253
|
-
expr.ExprIdent(
|
254
|
-
assert_isinstance(p, DataProperty)
|
255
|
-
.datatype.get_sqlalchemy_type()
|
256
|
-
.type
|
257
|
-
),
|
258
|
-
expr.PredefinedFn.keyword_assignment(
|
259
|
-
"nullable",
|
260
|
-
expr.ExprConstant(prop.is_optional),
|
261
|
-
),
|
262
|
-
],
|
263
|
-
)
|
264
|
-
)
|
265
|
-
propval = expr.ExprFuncCall(
|
266
|
-
expr.ExprIdent("composite"),
|
267
|
-
propvalargs,
|
268
|
-
)
|
269
|
-
else:
|
270
|
-
# we create a custom field, the custom field mapping need to be defined in the base
|
271
|
-
propval = expr.ExprFuncCall(expr.ExprIdent("mapped_column"), [])
|
272
|
-
custom_types.append(prop)
|
438
|
+
cls_ast(stmt.DefClassVarStatement(propname, proptype, propval))
|
439
|
+
return
|
273
440
|
|
274
|
-
|
441
|
+
# if the target class is not in the database,
|
442
|
+
program.import_(
|
443
|
+
f"{target_pkg.module(prop.target.get_pymodule_name()).path}.{prop.target.name}",
|
444
|
+
is_import_attr=True,
|
445
|
+
)
|
446
|
+
propname = prop.name
|
447
|
+
proptype = f"Mapped[{prop.target.name}]"
|
448
|
+
|
449
|
+
# we have two choices, one is to create a composite class, one is to create a custom field
|
450
|
+
if prop.db.is_embedded == "composite":
|
451
|
+
# for a class to be composite, it must have only data properties
|
452
|
+
program.import_("sqlalchemy.orm.composite", True)
|
453
|
+
propvalargs: list[expr.Expr] = [expr.ExprIdent(prop.target.name)]
|
454
|
+
for p in prop.target.properties.values():
|
455
|
+
propvalargs.append(
|
456
|
+
expr.ExprFuncCall(
|
457
|
+
expr.ExprIdent("mapped_column"),
|
458
|
+
[
|
459
|
+
expr.ExprIdent(f"{prop.name}_{p.name}"),
|
460
|
+
expr.ExprIdent(
|
461
|
+
assert_isinstance(p, DataProperty)
|
462
|
+
.datatype.get_sqlalchemy_type()
|
463
|
+
.type
|
464
|
+
),
|
465
|
+
expr.PredefinedFn.keyword_assignment(
|
466
|
+
"nullable",
|
467
|
+
expr.ExprConstant(prop.is_optional),
|
468
|
+
),
|
469
|
+
],
|
470
|
+
)
|
471
|
+
)
|
472
|
+
propval = expr.ExprFuncCall(
|
473
|
+
expr.ExprIdent("composite"),
|
474
|
+
propvalargs,
|
475
|
+
)
|
476
|
+
else:
|
477
|
+
# we create a custom field, the custom field mapping need to be defined in the base
|
478
|
+
propval = expr.ExprFuncCall(expr.ExprIdent("mapped_column"), [])
|
479
|
+
custom_types.append(prop)
|
275
480
|
|
276
|
-
|
481
|
+
cls_ast(stmt.DefClassVarStatement(propname, proptype, propval))
|
277
482
|
|
278
|
-
|
279
|
-
|
280
|
-
|
483
|
+
|
484
|
+
def make_python_relational_object_property_many_to_many(
|
485
|
+
program: Program, ast: AST, target_pkg: Package, cls: Class, prop: ObjectProperty
|
486
|
+
):
|
487
|
+
assert cls.db is not None
|
488
|
+
assert prop.db is not None and prop.target.db is not None
|
489
|
+
assert prop.cardinality == Cardinality.MANY_TO_MANY
|
490
|
+
|
491
|
+
# we create a new table to store the many-to-many relationship
|
492
|
+
new_table = f"{cls.name}{prop.target.name}"
|
493
|
+
clsdb = cls.db
|
494
|
+
propdb = prop.db
|
495
|
+
targetdb = prop.target.db
|
496
|
+
|
497
|
+
source_idprop = assert_not_null(cls.get_id_property())
|
498
|
+
source_id_type = source_idprop.datatype.get_python_type().type
|
499
|
+
target_idprop = assert_not_null(prop.target.get_id_property())
|
500
|
+
target_id_type = target_idprop.datatype.get_python_type().type
|
501
|
+
|
502
|
+
newprogram = Program()
|
503
|
+
newprogram.import_("__future__.annotations", True)
|
504
|
+
newprogram.import_("sqlalchemy.ForeignKey", True)
|
505
|
+
newprogram.import_("sqlalchemy.orm.mapped_column", True)
|
506
|
+
newprogram.import_("sqlalchemy.orm.Mapped", True)
|
507
|
+
newprogram.import_("sqlalchemy.orm.relationship", True)
|
508
|
+
newprogram.import_(f"{target_pkg.path}.base.Base", True)
|
509
|
+
newprogram.import_("typing.TYPE_CHECKING", True)
|
510
|
+
newprogram.import_area.if_(expr.ExprIdent("TYPE_CHECKING"))(
|
511
|
+
lambda ast00: ast00.import_(
|
512
|
+
target_pkg.path + f".{cls.get_pymodule_name()}.{cls.name}",
|
513
|
+
is_import_attr=True,
|
514
|
+
),
|
515
|
+
lambda ast10: ast10.import_(
|
516
|
+
target_pkg.path + f".{prop.target.get_pymodule_name()}.{prop.target.name}",
|
517
|
+
is_import_attr=True,
|
518
|
+
),
|
519
|
+
)
|
520
|
+
|
521
|
+
newprogram.root(
|
522
|
+
stmt.LineBreak(),
|
523
|
+
lambda ast00: ast00.class_(new_table, [expr.ExprIdent("Base")])(
|
524
|
+
stmt.DefClassVarStatement(
|
525
|
+
"__tablename__",
|
526
|
+
type=None,
|
527
|
+
value=expr.ExprConstant(f"{clsdb.table_name}_{targetdb.table_name}"),
|
528
|
+
),
|
529
|
+
stmt.LineBreak(),
|
530
|
+
stmt.DefClassVarStatement(
|
531
|
+
"id",
|
532
|
+
"Mapped[int]",
|
533
|
+
expr.ExprFuncCall(
|
534
|
+
expr.ExprIdent("mapped_column"),
|
535
|
+
[
|
536
|
+
PredefinedFn.keyword_assignment(
|
537
|
+
"primary_key", expr.ExprConstant(True)
|
538
|
+
),
|
539
|
+
PredefinedFn.keyword_assignment(
|
540
|
+
"autoincrement", expr.ExprConstant("auto")
|
541
|
+
),
|
542
|
+
],
|
543
|
+
),
|
544
|
+
),
|
545
|
+
stmt.DefClassVarStatement(
|
546
|
+
to_snake_case(cls.name),
|
547
|
+
f"Mapped[{cls.name}]",
|
548
|
+
expr.ExprFuncCall(
|
549
|
+
expr.ExprIdent("relationship"),
|
550
|
+
[
|
551
|
+
PredefinedFn.keyword_assignment(
|
552
|
+
"back_populates",
|
553
|
+
expr.ExprConstant(prop.name),
|
554
|
+
),
|
555
|
+
PredefinedFn.keyword_assignment(
|
556
|
+
"lazy",
|
557
|
+
expr.ExprConstant("raise_on_sql"),
|
558
|
+
),
|
559
|
+
],
|
560
|
+
),
|
561
|
+
),
|
562
|
+
stmt.DefClassVarStatement(
|
563
|
+
to_snake_case(cls.name) + "_id",
|
564
|
+
f"Mapped[{source_id_type}]",
|
565
|
+
expr.ExprFuncCall(
|
566
|
+
expr.ExprIdent("mapped_column"),
|
567
|
+
[
|
568
|
+
expr.ExprFuncCall(
|
569
|
+
expr.ExprIdent("ForeignKey"),
|
570
|
+
[
|
571
|
+
expr.ExprConstant(
|
572
|
+
f"{clsdb.table_name}.{source_idprop.name}"
|
573
|
+
),
|
574
|
+
PredefinedFn.keyword_assignment(
|
575
|
+
"ondelete",
|
576
|
+
expr.ExprConstant(
|
577
|
+
propdb.on_source_delete.to_sqlalchemy()
|
578
|
+
),
|
579
|
+
),
|
580
|
+
PredefinedFn.keyword_assignment(
|
581
|
+
"onupdate",
|
582
|
+
expr.ExprConstant(
|
583
|
+
propdb.on_source_update.to_sqlalchemy()
|
584
|
+
),
|
585
|
+
),
|
586
|
+
],
|
587
|
+
),
|
588
|
+
],
|
589
|
+
),
|
590
|
+
),
|
591
|
+
stmt.DefClassVarStatement(
|
592
|
+
to_snake_case(prop.target.name),
|
593
|
+
f"Mapped[{prop.target.name}]",
|
594
|
+
expr.ExprFuncCall(
|
595
|
+
expr.ExprIdent("relationship"),
|
596
|
+
[
|
597
|
+
PredefinedFn.keyword_assignment(
|
598
|
+
"lazy",
|
599
|
+
expr.ExprConstant("raise_on_sql"),
|
600
|
+
),
|
601
|
+
],
|
602
|
+
),
|
603
|
+
),
|
604
|
+
stmt.DefClassVarStatement(
|
605
|
+
to_snake_case(prop.target.name) + "_id",
|
606
|
+
f"Mapped[{target_id_type}]",
|
607
|
+
expr.ExprFuncCall(
|
608
|
+
expr.ExprIdent("mapped_column"),
|
609
|
+
[
|
610
|
+
expr.ExprFuncCall(
|
611
|
+
expr.ExprIdent("ForeignKey"),
|
612
|
+
[
|
613
|
+
expr.ExprConstant(
|
614
|
+
f"{targetdb.table_name}.{target_idprop.name}"
|
615
|
+
),
|
616
|
+
PredefinedFn.keyword_assignment(
|
617
|
+
"ondelete",
|
618
|
+
expr.ExprConstant(
|
619
|
+
propdb.on_target_delete.to_sqlalchemy()
|
620
|
+
),
|
621
|
+
),
|
622
|
+
PredefinedFn.keyword_assignment(
|
623
|
+
"onupdate",
|
624
|
+
expr.ExprConstant(
|
625
|
+
propdb.on_target_update.to_sqlalchemy()
|
626
|
+
),
|
627
|
+
),
|
628
|
+
],
|
629
|
+
),
|
630
|
+
],
|
631
|
+
),
|
632
|
+
),
|
633
|
+
),
|
634
|
+
)
|
635
|
+
|
636
|
+
new_table_module = target_pkg.module(to_snake_case(new_table))
|
637
|
+
new_table_module.write(newprogram)
|
638
|
+
|
639
|
+
# now we add the relationship to the source.
|
640
|
+
# we can configure it to be list, set, or dict depends on what we want.
|
641
|
+
program.import_(new_table_module.path + f".{new_table}", True)
|
642
|
+
# program.import_("typing.TYPE_CHECKING", True)
|
643
|
+
# program.import_area.if_(expr.ExprIdent("TYPE_CHECKING"))(
|
644
|
+
# lambda ast00: ast00.import_(
|
645
|
+
# target_pkg.path + f".{prop.target.get_pymodule_name()}.{prop.target.name}",
|
646
|
+
# is_import_attr=True,
|
647
|
+
# )
|
648
|
+
# )
|
649
|
+
|
650
|
+
ast(
|
651
|
+
stmt.DefClassVarStatement(
|
652
|
+
prop.name,
|
653
|
+
f"Mapped[list[{new_table}]]",
|
654
|
+
expr.ExprFuncCall(
|
655
|
+
expr.ExprIdent("relationship"),
|
656
|
+
[
|
657
|
+
PredefinedFn.keyword_assignment(
|
658
|
+
"back_populates",
|
659
|
+
expr.ExprConstant(to_snake_case(cls.name)),
|
660
|
+
),
|
661
|
+
PredefinedFn.keyword_assignment(
|
662
|
+
"lazy",
|
663
|
+
expr.ExprConstant("raise_on_sql"),
|
664
|
+
),
|
665
|
+
],
|
666
|
+
),
|
667
|
+
),
|
281
668
|
)
|
282
|
-
make_base(custom_types)
|
sera/models/__init__.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
from sera.models._class import Class
|
2
2
|
from sera.models._collection import DataCollection
|
3
3
|
from sera.models._datatype import DataType
|
4
|
-
from sera.models._module import App, Module, Package
|
4
|
+
from sera.models._module import App, Language, Module, Package
|
5
5
|
from sera.models._multi_lingual_string import MultiLingualString
|
6
6
|
from sera.models._parse import parse_schema
|
7
7
|
from sera.models._property import Cardinality, DataProperty, ObjectProperty, Property
|
@@ -21,4 +21,5 @@ __all__ = [
|
|
21
21
|
"DataCollection",
|
22
22
|
"Module",
|
23
23
|
"App",
|
24
|
+
"Language",
|
24
25
|
]
|
sera/models/_class.py
CHANGED
@@ -35,6 +35,8 @@ class Class:
|
|
35
35
|
|
36
36
|
# whether to store this class in a table in the database
|
37
37
|
db: Optional[ClassDBMapInfo]
|
38
|
+
# whether this class is public and we generate a data model for it.
|
39
|
+
is_public: bool = True
|
38
40
|
|
39
41
|
def get_id_property(self) -> Optional[DataProperty]:
|
40
42
|
"""
|