sera-2 1.15.0__py3-none-any.whl → 1.17.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/exports/schema.py +14 -0
- sera/make/make_python_model.py +38 -4
- sera/make/make_typescript_model.py +80 -10
- sera/misc/__init__.py +2 -0
- sera/misc/_utils.py +5 -0
- sera/models/_parse.py +1 -0
- sera/models/_property.py +6 -0
- sera/typing.py +1 -0
- {sera_2-1.15.0.dist-info → sera_2-1.17.0.dist-info}/METADATA +1 -1
- {sera_2-1.15.0.dist-info → sera_2-1.17.0.dist-info}/RECORD +11 -11
- {sera_2-1.15.0.dist-info → sera_2-1.17.0.dist-info}/WHEEL +0 -0
sera/exports/schema.py
CHANGED
@@ -54,6 +54,20 @@ def export_tbls(schema: Schema, outfile: Path):
|
|
54
54
|
"columns": [prop.name],
|
55
55
|
}
|
56
56
|
)
|
57
|
+
|
58
|
+
if prop.db.foreign_key is not None:
|
59
|
+
idprop = assert_not_null(prop.db.foreign_key.get_id_property())
|
60
|
+
out["relations"].append(
|
61
|
+
{
|
62
|
+
"table": cls.name,
|
63
|
+
"columns": [prop.name],
|
64
|
+
"cardinality": "zero_or_one",
|
65
|
+
"parent_table": prop.db.foreign_key.name,
|
66
|
+
"parent_columns": [idprop_name],
|
67
|
+
"parent_cardinality": "zero_or_one",
|
68
|
+
"def": f"FOREIGN KEY ({prop.name}) REFERENCES {prop.db.foreign_key.name}({idprop_name})",
|
69
|
+
}
|
70
|
+
)
|
57
71
|
else:
|
58
72
|
if prop.cardinality == Cardinality.MANY_TO_MANY:
|
59
73
|
# For many-to-many relationships, we need to create a join table
|
sera/make/make_python_model.py
CHANGED
@@ -1079,12 +1079,10 @@ def make_python_relational_model(
|
|
1079
1079
|
|
1080
1080
|
target_pkg.module("base").write(program)
|
1081
1081
|
|
1082
|
-
|
1083
|
-
|
1084
|
-
for cls in schema.topological_sort():
|
1082
|
+
def make_orm(cls: Class):
|
1085
1083
|
if cls.db is None or cls.name in reference_classes:
|
1086
1084
|
# skip classes that are not stored in the database
|
1087
|
-
|
1085
|
+
return
|
1088
1086
|
|
1089
1087
|
program = Program()
|
1090
1088
|
program.import_("__future__.annotations", True)
|
@@ -1093,6 +1091,11 @@ def make_python_relational_model(
|
|
1093
1091
|
program.import_("sqlalchemy.orm.Mapped", True)
|
1094
1092
|
program.import_(f"{target_pkg.path}.base.Base", True)
|
1095
1093
|
|
1094
|
+
ident_manager = ImportHelper(
|
1095
|
+
program,
|
1096
|
+
GLOBAL_IDENTS,
|
1097
|
+
)
|
1098
|
+
|
1096
1099
|
index_stmts = []
|
1097
1100
|
if len(cls.db.indices) > 0:
|
1098
1101
|
program.import_("sqlalchemy.Index", True)
|
@@ -1159,6 +1162,32 @@ def make_python_relational_model(
|
|
1159
1162
|
proptype = f"Mapped[{sqltype.mapped_pytype}]"
|
1160
1163
|
|
1161
1164
|
propvalargs: list[expr.Expr] = [expr.ExprIdent(sqltype.type)]
|
1165
|
+
if prop.db.foreign_key is not None:
|
1166
|
+
assert (
|
1167
|
+
prop.db.foreign_key.db is not None
|
1168
|
+
), f"Foreign key {prop.db.foreign_key.name} must have a database mapping"
|
1169
|
+
foreign_key_idprop = prop.db.foreign_key.get_id_property()
|
1170
|
+
assert (
|
1171
|
+
foreign_key_idprop is not None
|
1172
|
+
), f"Foreign key {prop.db.foreign_key.name} must have an id property"
|
1173
|
+
propvalargs.append(
|
1174
|
+
expr.ExprFuncCall(
|
1175
|
+
ident_manager.use("ForeignKey"),
|
1176
|
+
[
|
1177
|
+
expr.ExprConstant(
|
1178
|
+
f"{prop.db.foreign_key.db.table_name}.{foreign_key_idprop.name}"
|
1179
|
+
),
|
1180
|
+
PredefinedFn.keyword_assignment(
|
1181
|
+
"ondelete",
|
1182
|
+
expr.ExprConstant("CASCADE"),
|
1183
|
+
),
|
1184
|
+
PredefinedFn.keyword_assignment(
|
1185
|
+
"onupdate",
|
1186
|
+
expr.ExprConstant("CASCADE"),
|
1187
|
+
),
|
1188
|
+
],
|
1189
|
+
)
|
1190
|
+
)
|
1162
1191
|
if prop.db.is_primary_key:
|
1163
1192
|
propvalargs.append(
|
1164
1193
|
PredefinedFn.keyword_assignment(
|
@@ -1208,6 +1237,11 @@ def make_python_relational_model(
|
|
1208
1237
|
|
1209
1238
|
target_pkg.module(cls.get_pymodule_name()).write(program)
|
1210
1239
|
|
1240
|
+
custom_types: list[ObjectProperty] = []
|
1241
|
+
|
1242
|
+
for cls in schema.topological_sort():
|
1243
|
+
make_orm(cls)
|
1244
|
+
|
1211
1245
|
# make a base class that implements the mapping for custom types
|
1212
1246
|
custom_types = filter_duplication(
|
1213
1247
|
custom_types, lambda p: (p.target.name, p.cardinality, p.is_optional, p.is_map)
|
@@ -1,15 +1,16 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import re
|
4
|
-
from typing import Any, Callable
|
4
|
+
from typing import Any, Callable, Optional
|
5
5
|
|
6
|
-
from codegen.models import AST, PredefinedFn, Program, expr, stmt
|
6
|
+
from codegen.models import AST, ImportHelper, PredefinedFn, Program, expr, stmt
|
7
7
|
from codegen.models.var import DeferredVar
|
8
8
|
from loguru import logger
|
9
9
|
|
10
10
|
from sera.misc import (
|
11
11
|
assert_isinstance,
|
12
12
|
assert_not_null,
|
13
|
+
identity,
|
13
14
|
to_camel_case,
|
14
15
|
to_pascal_case,
|
15
16
|
to_snake_case,
|
@@ -25,6 +26,11 @@ from sera.models import (
|
|
25
26
|
)
|
26
27
|
from sera.typing import is_set
|
27
28
|
|
29
|
+
TS_GLOBAL_IDENTS = {
|
30
|
+
"normalizers.normalizeNumber": "sera-db.normalizers",
|
31
|
+
"normalizers.normalizeOptionalNumber": "sera-db.normalizers",
|
32
|
+
}
|
33
|
+
|
28
34
|
|
29
35
|
def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
30
36
|
"""Generate TypeScript data model from the schema. The data model aligns with the public data model in Python, not the database model."""
|
@@ -259,6 +265,8 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
259
265
|
program.import_("mobx.action", True)
|
260
266
|
program.import_("sera-db.validators", True)
|
261
267
|
|
268
|
+
import_helper = ImportHelper(program, TS_GLOBAL_IDENTS)
|
269
|
+
|
262
270
|
program.root(
|
263
271
|
stmt.LineBreak(),
|
264
272
|
stmt.TypescriptStatement(
|
@@ -331,12 +339,15 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
331
339
|
|
332
340
|
if isinstance(prop, DataProperty):
|
333
341
|
tstype = prop.get_data_model_datatype().get_typescript_type()
|
342
|
+
original_tstype = tstype
|
343
|
+
|
334
344
|
if idprop is not None and prop.name == idprop.name:
|
335
345
|
# use id type alias
|
336
346
|
tstype = TsTypeWithDep(
|
337
347
|
f"{cls.name}Id",
|
338
348
|
deps=[f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}Id"],
|
339
349
|
)
|
350
|
+
original_tstype = tstype
|
340
351
|
elif tstype.type not in schema.enums:
|
341
352
|
# for none id & none enum properties, we need to include a type for "invalid" value
|
342
353
|
tstype = _inject_type_for_invalid_value(tstype)
|
@@ -344,6 +355,7 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
344
355
|
if prop.is_optional:
|
345
356
|
# convert type to optional
|
346
357
|
tstype = tstype.as_optional_type()
|
358
|
+
original_tstype = original_tstype.as_optional_type()
|
347
359
|
|
348
360
|
for dep in tstype.deps:
|
349
361
|
program.import_(dep, True)
|
@@ -400,6 +412,11 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
400
412
|
expr.ExprIdent("record"), expr.ExprIdent(propname)
|
401
413
|
)
|
402
414
|
|
415
|
+
if original_tstype.type != tstype.type:
|
416
|
+
norm_func = get_norm_func(original_tstype, import_helper)
|
417
|
+
else:
|
418
|
+
norm_func = identity
|
419
|
+
|
403
420
|
ser_args.append(
|
404
421
|
(
|
405
422
|
expr.ExprIdent(prop.name),
|
@@ -420,14 +437,18 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
420
437
|
),
|
421
438
|
expr.ExprIdent("isValid"),
|
422
439
|
),
|
423
|
-
|
424
|
-
|
440
|
+
norm_func(
|
441
|
+
PredefinedFn.attr_getter(
|
442
|
+
expr.ExprIdent("this"), expr.ExprIdent(propname)
|
443
|
+
)
|
425
444
|
),
|
426
445
|
expr.ExprIdent("undefined"),
|
427
446
|
)
|
428
447
|
if prop.is_optional
|
429
|
-
else
|
430
|
-
|
448
|
+
else norm_func(
|
449
|
+
PredefinedFn.attr_getter(
|
450
|
+
expr.ExprIdent("this"), expr.ExprIdent(propname)
|
451
|
+
)
|
431
452
|
)
|
432
453
|
),
|
433
454
|
)
|
@@ -455,14 +476,19 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
455
476
|
),
|
456
477
|
expr.ExprIdent("isValid"),
|
457
478
|
),
|
458
|
-
|
459
|
-
|
479
|
+
norm_func(
|
480
|
+
PredefinedFn.attr_getter(
|
481
|
+
expr.ExprIdent("this"),
|
482
|
+
expr.ExprIdent(propname),
|
483
|
+
)
|
460
484
|
),
|
461
485
|
expr.ExprIdent("undefined"),
|
462
486
|
)
|
463
487
|
if prop.is_optional
|
464
|
-
else
|
465
|
-
|
488
|
+
else norm_func(
|
489
|
+
PredefinedFn.attr_getter(
|
490
|
+
expr.ExprIdent("this"), expr.ExprIdent(propname)
|
491
|
+
)
|
466
492
|
)
|
467
493
|
),
|
468
494
|
)
|
@@ -1086,6 +1112,9 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
1086
1112
|
|
1087
1113
|
program = Program()
|
1088
1114
|
prop_defs: list[tuple[DataProperty | ObjectProperty, expr.Expr, expr.Expr]] = []
|
1115
|
+
prop_normalizers: list[tuple[expr.Expr, expr.Expr]] = []
|
1116
|
+
|
1117
|
+
import_helper = ImportHelper(program, TS_GLOBAL_IDENTS)
|
1089
1118
|
|
1090
1119
|
for prop in cls.properties.values():
|
1091
1120
|
# we must include private properties that are needed during upsert for our forms.
|
@@ -1126,6 +1155,11 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
1126
1155
|
),
|
1127
1156
|
),
|
1128
1157
|
]
|
1158
|
+
|
1159
|
+
norm_func = get_normalizer(tstype, import_helper)
|
1160
|
+
if norm_func is not None:
|
1161
|
+
# we have a normalizer for this type
|
1162
|
+
prop_normalizers.append((expr.ExprIdent(propname), norm_func))
|
1129
1163
|
else:
|
1130
1164
|
assert isinstance(prop, ObjectProperty)
|
1131
1165
|
if prop.target.db is not None:
|
@@ -1340,6 +1374,10 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
1340
1374
|
expr.ExprIdent("validators"),
|
1341
1375
|
expr.ExprIdent(f"draft{cls.name}Validators"),
|
1342
1376
|
),
|
1377
|
+
(
|
1378
|
+
expr.ExprIdent("normalizers"),
|
1379
|
+
PredefinedFn.dict(prop_normalizers),
|
1380
|
+
),
|
1343
1381
|
]
|
1344
1382
|
+ (
|
1345
1383
|
[
|
@@ -1500,3 +1538,35 @@ def _inject_type_for_invalid_value(tstype: TsTypeWithDep) -> TsTypeWithDep:
|
|
1500
1538
|
return tstype
|
1501
1539
|
|
1502
1540
|
raise NotImplementedError(tstype.type)
|
1541
|
+
|
1542
|
+
|
1543
|
+
def get_normalizer(
|
1544
|
+
tstype: TsTypeWithDep, import_helper: ImportHelper
|
1545
|
+
) -> Optional[expr.ExprIdent]:
|
1546
|
+
if tstype.type == "number":
|
1547
|
+
return import_helper.use("normalizers.normalizeNumber")
|
1548
|
+
if tstype.type == "number | undefined":
|
1549
|
+
return import_helper.use("normalizers.normalizeOptionalNumber")
|
1550
|
+
|
1551
|
+
assert "number" not in tstype.type, tstype.type
|
1552
|
+
return None
|
1553
|
+
|
1554
|
+
|
1555
|
+
def get_norm_func(
|
1556
|
+
tstype: TsTypeWithDep, import_helper: ImportHelper
|
1557
|
+
) -> Callable[[expr.Expr], expr.Expr]:
|
1558
|
+
"""
|
1559
|
+
Get the normalizer function for the given TypeScript type.
|
1560
|
+
If no normalizer is available, return None.
|
1561
|
+
"""
|
1562
|
+
norm_func = get_normalizer(tstype, import_helper)
|
1563
|
+
if norm_func is not None:
|
1564
|
+
|
1565
|
+
def modify_expr(value: expr.Expr) -> expr.Expr:
|
1566
|
+
return expr.ExprFuncCall(
|
1567
|
+
norm_func,
|
1568
|
+
[value],
|
1569
|
+
)
|
1570
|
+
|
1571
|
+
return modify_expr
|
1572
|
+
return identity # Return the value as is if no normalizer is available
|
sera/misc/__init__.py
CHANGED
sera/misc/_utils.py
CHANGED
sera/models/_parse.py
CHANGED
sera/models/_property.py
CHANGED
@@ -150,6 +150,12 @@ class DataPropDBInfo:
|
|
150
150
|
is_unique: bool = False
|
151
151
|
# whether this property is indexed or not
|
152
152
|
is_indexed: bool = False
|
153
|
+
# this is used in conjunction with is_primary_key = True for the case of
|
154
|
+
# extending a table with frequently updated properties. The value for the `foreign_key`
|
155
|
+
# will be a target class. The cardinality is one-to-one, on target class deletion,
|
156
|
+
# this class will be deleted as well (because it's an extended table of the target class).
|
157
|
+
# on source (this class) deletion, the target class will not be deleted.
|
158
|
+
foreign_key: Optional[Class] = None
|
153
159
|
|
154
160
|
|
155
161
|
@dataclass(kw_only=True)
|
sera/typing.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
sera/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
sera/constants.py,sha256=mzAaMyIx8TJK0-RYYJ5I24C4s0Uvj26OLMJmBo0pxHI,123
|
3
3
|
sera/exports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
sera/exports/schema.py,sha256=
|
4
|
+
sera/exports/schema.py,sha256=wEBUrDOyuCoCJC8X4RlmoWpeqSugaboG-9Q1UQ8HEzk,7824
|
5
5
|
sera/exports/test.py,sha256=jK1EJmLGiy7eREpnY_68IIVRH43uH8S_u5Z7STPbXOM,2002
|
6
6
|
sera/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
7
|
sera/libs/api_helper.py,sha256=47y1kcwk3Xd2ZEMnUj_0OwCuUmgwOs5kYrE95BDVUn4,5411
|
@@ -17,12 +17,12 @@ sera/make/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
sera/make/__main__.py,sha256=HRfOR53p351h6KblVvYm3DLhDIfEtk6R0kjl78_S_S8,1453
|
18
18
|
sera/make/make_app.py,sha256=n9NtW73O3s_5Q31VHIRmnd-jEIcpDO7ksAsOdovde2s,5999
|
19
19
|
sera/make/make_python_api.py,sha256=iXGbKQ3IJvsY1ur_fhurr_THFNnH66E3Wl85o0emUbw,26853
|
20
|
-
sera/make/make_python_model.py,sha256=
|
20
|
+
sera/make/make_python_model.py,sha256=Nc4vDGgM8icgWBqzNnMgEkLadf5EsZwbbHs3WLW9_co,62778
|
21
21
|
sera/make/make_python_services.py,sha256=0ZpWLwQ7Nwfn8BXAikAB4JRpNknpSJyJgY5b1cjtxV4,2073
|
22
|
-
sera/make/make_typescript_model.py,sha256=
|
23
|
-
sera/misc/__init__.py,sha256=
|
22
|
+
sera/make/make_typescript_model.py,sha256=1ouYFCeqOlwEzsGBiXUn4VZtLJjJW7GSacdOSlQzhjI,67012
|
23
|
+
sera/misc/__init__.py,sha256=mPKkik00j3tO_m45VPDJBjm8K85NpymRPl36Kh4hBn8,473
|
24
24
|
sera/misc/_formatter.py,sha256=aCGYL08l8f3aLODHxSocxBBwkRYEo3K1QzCDEn3suj0,1685
|
25
|
-
sera/misc/_utils.py,sha256=
|
25
|
+
sera/misc/_utils.py,sha256=pGYv8p7m7opiDTLYbsPrhF0YA4WjFff7beMQQZ9NnEs,4095
|
26
26
|
sera/models/__init__.py,sha256=vJC5Kzo_N7wd16ocNPy1VvAZDGNiWeiAhWJ4ihATKvA,780
|
27
27
|
sera/models/_class.py,sha256=1J4Bd_LanzhhDWwZFHWGtFYD7lupe_alaB3D02ebNDI,2862
|
28
28
|
sera/models/_collection.py,sha256=ZnQEriKC4X88Zz48Kn1AVZKH-1_l8OgWa-zf2kcQOOE,1414
|
@@ -32,10 +32,10 @@ sera/models/_default.py,sha256=ABggW6qdPR4ZDqIPJdJ0GCGQ-7kfsfZmQ_DchgZEa-I,137
|
|
32
32
|
sera/models/_enum.py,sha256=sy0q7E646F-APsqrVQ52r1fAQ_DCAeaNq5YM5QN3zIk,2070
|
33
33
|
sera/models/_module.py,sha256=I-GfnTgAa-5R87qTAvEzOt-VVEGeFBBwubGCgUkXVSw,5159
|
34
34
|
sera/models/_multi_lingual_string.py,sha256=JETN6k00VH4wrA4w5vAHMEJV8fp3SY9bJebskFTjQLA,1186
|
35
|
-
sera/models/_parse.py,sha256=
|
36
|
-
sera/models/_property.py,sha256=
|
35
|
+
sera/models/_parse.py,sha256=ciTLzCkO0q6xA1R_rHbnYJYK3Duo2oh56WeuwxXwJaI,12392
|
36
|
+
sera/models/_property.py,sha256=9yMDxrmbyuF6-29lQjiq163Xzwbk75TlmGBpu0NLpkI,7485
|
37
37
|
sera/models/_schema.py,sha256=VxJEiqgVvbXgcSUK4UW6JnRcggk4nsooVSE6MyXmfNY,1636
|
38
|
-
sera/typing.py,sha256=
|
39
|
-
sera_2-1.
|
40
|
-
sera_2-1.
|
41
|
-
sera_2-1.
|
38
|
+
sera/typing.py,sha256=o_DKfSvs8JpNRQ0kdaTc3BbfdkvibY3uY4tJRt-n2fQ,1023
|
39
|
+
sera_2-1.17.0.dist-info/METADATA,sha256=aIaXid2dkyX8P9nty-1eFHFBuH0Cpy34vOGDi1wTFkI,852
|
40
|
+
sera_2-1.17.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
41
|
+
sera_2-1.17.0.dist-info/RECORD,,
|
File without changes
|