sera-2 1.21.2__py3-none-any.whl → 1.23.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/libs/api_helper.py +1 -100
- sera/libs/api_test_helper.py +1 -1
- sera/libs/base_service.py +168 -80
- sera/libs/search_helper.py +359 -0
- sera/make/make_python_api.py +65 -113
- sera/make/make_python_model.py +184 -17
- sera/make/make_python_services.py +3 -2
- sera/make/make_typescript_model.py +21 -2
- sera/misc/__init__.py +2 -0
- sera/misc/_utils.py +17 -2
- sera/models/_class.py +2 -2
- sera/models/_collection.py +15 -11
- sera/models/_constraints.py +1 -1
- sera/models/_datatype.py +8 -29
- sera/models/_enum.py +2 -2
- sera/models/_module.py +7 -0
- {sera_2-1.21.2.dist-info → sera_2-1.23.0.dist-info}/METADATA +2 -2
- {sera_2-1.21.2.dist-info → sera_2-1.23.0.dist-info}/RECORD +19 -18
- {sera_2-1.21.2.dist-info → sera_2-1.23.0.dist-info}/WHEEL +0 -0
sera/make/make_python_model.py
CHANGED
@@ -883,6 +883,35 @@ def make_python_data_model(
|
|
883
883
|
),
|
884
884
|
)
|
885
885
|
|
886
|
+
def make_data_schema_export():
|
887
|
+
program = Program()
|
888
|
+
program.import_("__future__.annotations", True)
|
889
|
+
|
890
|
+
expose_vars = [expr.ExprConstant(cls.name) for cls in schema.classes.values()]
|
891
|
+
expose_vars.append(expr.ExprConstant("dataschema"))
|
892
|
+
|
893
|
+
output = []
|
894
|
+
for cls in schema.classes.values():
|
895
|
+
program.import_(
|
896
|
+
f"{target_pkg.path}.{cls.get_pymodule_name()}.{cls.name}",
|
897
|
+
True,
|
898
|
+
)
|
899
|
+
output.append((expr.ExprConstant(cls.name), expr.ExprIdent(cls.name)))
|
900
|
+
|
901
|
+
program.root(
|
902
|
+
stmt.LineBreak(),
|
903
|
+
lambda ast: ast.assign(
|
904
|
+
DeferredVar.simple("dataschema"), PredefinedFn.dict(output)
|
905
|
+
),
|
906
|
+
stmt.LineBreak(),
|
907
|
+
lambda ast: ast.assign(
|
908
|
+
DeferredVar.simple("__all__"),
|
909
|
+
PredefinedFn.list(expose_vars),
|
910
|
+
),
|
911
|
+
)
|
912
|
+
|
913
|
+
target_pkg.parent().module("data_schema").write(program)
|
914
|
+
|
886
915
|
for cls in schema.topological_sort():
|
887
916
|
if cls.name in reference_classes:
|
888
917
|
continue
|
@@ -895,6 +924,8 @@ def make_python_data_model(
|
|
895
924
|
make_normal(program, cls)
|
896
925
|
target_pkg.module(cls.get_pymodule_name()).write(program)
|
897
926
|
|
927
|
+
make_data_schema_export()
|
928
|
+
|
898
929
|
|
899
930
|
def make_python_relational_model(
|
900
931
|
schema: Schema,
|
@@ -1098,6 +1129,66 @@ def make_python_relational_model(
|
|
1098
1129
|
|
1099
1130
|
target_pkg.module("base").write(program)
|
1100
1131
|
|
1132
|
+
def make_db_schema_export():
|
1133
|
+
program = Program()
|
1134
|
+
program.import_("__future__.annotations", True)
|
1135
|
+
|
1136
|
+
expose_vars = [
|
1137
|
+
expr.ExprConstant(cls.name)
|
1138
|
+
for cls in schema.classes.values()
|
1139
|
+
if cls.db is not None
|
1140
|
+
]
|
1141
|
+
expose_vars.append(expr.ExprConstant("dbschema"))
|
1142
|
+
|
1143
|
+
for name in ["engine", "async_engine", "get_session", "get_async_session"]:
|
1144
|
+
program.import_(f"{target_pkg.path}.base.{name}", True)
|
1145
|
+
expose_vars.append(expr.ExprConstant(name))
|
1146
|
+
|
1147
|
+
output = []
|
1148
|
+
for cls in schema.classes.values():
|
1149
|
+
if cls.db is None:
|
1150
|
+
continue
|
1151
|
+
program.import_(
|
1152
|
+
f"{target_pkg.path}.{cls.get_pymodule_name()}.{cls.name}",
|
1153
|
+
True,
|
1154
|
+
)
|
1155
|
+
output.append((expr.ExprConstant(cls.name), expr.ExprIdent(cls.name)))
|
1156
|
+
|
1157
|
+
# if there is a MANY-TO-MANY relationship, we need to add an association table as well
|
1158
|
+
for prop in cls.properties.values():
|
1159
|
+
if (
|
1160
|
+
not isinstance(prop, ObjectProperty)
|
1161
|
+
or prop.target.db is None
|
1162
|
+
or prop.cardinality != Cardinality.MANY_TO_MANY
|
1163
|
+
):
|
1164
|
+
continue
|
1165
|
+
|
1166
|
+
program.import_(
|
1167
|
+
f"{target_pkg.path}.{to_snake_case(cls.name + prop.target.name)}.{cls.name}{prop.target.name}",
|
1168
|
+
True,
|
1169
|
+
)
|
1170
|
+
output.append(
|
1171
|
+
(
|
1172
|
+
expr.ExprConstant(f"{cls.name}{prop.target.name}"),
|
1173
|
+
expr.ExprIdent(f"{cls.name}{prop.target.name}"),
|
1174
|
+
)
|
1175
|
+
)
|
1176
|
+
expose_vars.append(expr.ExprConstant(f"{cls.name}{prop.target.name}"))
|
1177
|
+
|
1178
|
+
program.root(
|
1179
|
+
stmt.LineBreak(),
|
1180
|
+
lambda ast: ast.assign(
|
1181
|
+
DeferredVar.simple("dbschema"), PredefinedFn.dict(output)
|
1182
|
+
),
|
1183
|
+
stmt.LineBreak(),
|
1184
|
+
lambda ast: ast.assign(
|
1185
|
+
DeferredVar.simple("__all__"),
|
1186
|
+
PredefinedFn.list(expose_vars),
|
1187
|
+
),
|
1188
|
+
)
|
1189
|
+
|
1190
|
+
target_pkg.module("__init__").write(program)
|
1191
|
+
|
1101
1192
|
def make_orm(cls: Class):
|
1102
1193
|
if cls.db is None or cls.name in reference_classes:
|
1103
1194
|
# skip classes that are not stored in the database
|
@@ -1242,10 +1333,44 @@ def make_python_relational_model(
|
|
1242
1333
|
expr.ExprIdent("mapped_column"), propvalargs
|
1243
1334
|
)
|
1244
1335
|
cls_ast(stmt.DefClassVarStatement(propname, proptype, propval))
|
1336
|
+
|
1337
|
+
if prop.db.foreign_key is not None:
|
1338
|
+
# add a relationship property for foreign key primary key so that we can do eager join in SQLAlchemy
|
1339
|
+
program.import_("sqlalchemy.orm.relationship", True)
|
1340
|
+
if prop.db.foreign_key.name != cls.name:
|
1341
|
+
ident_manager.python_import_for_hint(
|
1342
|
+
target_pkg.path
|
1343
|
+
+ f".{prop.db.foreign_key.get_pymodule_name()}.{prop.db.foreign_key.name}",
|
1344
|
+
True,
|
1345
|
+
)
|
1346
|
+
cls_ast(
|
1347
|
+
stmt.DefClassVarStatement(
|
1348
|
+
propname + "_relobj",
|
1349
|
+
f"Mapped[{prop.db.foreign_key.name}]",
|
1350
|
+
expr.ExprFuncCall(
|
1351
|
+
expr.ExprIdent("relationship"),
|
1352
|
+
[
|
1353
|
+
PredefinedFn.keyword_assignment(
|
1354
|
+
"lazy",
|
1355
|
+
expr.ExprConstant("raise_on_sql"),
|
1356
|
+
),
|
1357
|
+
PredefinedFn.keyword_assignment(
|
1358
|
+
"foreign_keys",
|
1359
|
+
expr.ExprIdent(propname),
|
1360
|
+
),
|
1361
|
+
PredefinedFn.keyword_assignment(
|
1362
|
+
"init",
|
1363
|
+
expr.ExprConstant(False),
|
1364
|
+
),
|
1365
|
+
],
|
1366
|
+
),
|
1367
|
+
)
|
1368
|
+
)
|
1245
1369
|
else:
|
1246
1370
|
assert isinstance(prop, ObjectProperty)
|
1247
1371
|
make_python_relational_object_property(
|
1248
1372
|
program=program,
|
1373
|
+
ident_manager=ident_manager,
|
1249
1374
|
target_pkg=target_pkg,
|
1250
1375
|
target_data_pkg=target_data_pkg,
|
1251
1376
|
cls_ast=cls_ast,
|
@@ -1267,9 +1392,13 @@ def make_python_relational_model(
|
|
1267
1392
|
)
|
1268
1393
|
make_base(custom_types)
|
1269
1394
|
|
1395
|
+
# export the db classes in the __init__ file
|
1396
|
+
make_db_schema_export()
|
1397
|
+
|
1270
1398
|
|
1271
1399
|
def make_python_relational_object_property(
|
1272
1400
|
program: Program,
|
1401
|
+
ident_manager: ImportHelper,
|
1273
1402
|
target_pkg: Package,
|
1274
1403
|
target_data_pkg: Package,
|
1275
1404
|
cls_ast: AST,
|
@@ -1291,6 +1420,14 @@ def make_python_relational_object_property(
|
|
1291
1420
|
if prop.cardinality.is_star_to_many():
|
1292
1421
|
raise NotImplementedError((cls.name, prop.name))
|
1293
1422
|
|
1423
|
+
program.import_("sqlalchemy.orm.relationship", True)
|
1424
|
+
if prop.target.name != cls.name:
|
1425
|
+
ident_manager.python_import_for_hint(
|
1426
|
+
target_pkg.path
|
1427
|
+
+ f".{prop.target.get_pymodule_name()}.{prop.target.name}",
|
1428
|
+
True,
|
1429
|
+
)
|
1430
|
+
|
1294
1431
|
# we store this class in the database
|
1295
1432
|
propname = f"{prop.name}_id"
|
1296
1433
|
idprop = prop.target.get_id_property()
|
@@ -1329,7 +1466,30 @@ def make_python_relational_object_property(
|
|
1329
1466
|
],
|
1330
1467
|
)
|
1331
1468
|
|
1332
|
-
cls_ast(
|
1469
|
+
cls_ast(
|
1470
|
+
stmt.DefClassVarStatement(propname, proptype, propval),
|
1471
|
+
stmt.DefClassVarStatement(
|
1472
|
+
prop.name,
|
1473
|
+
f"Mapped[{prop.target.name}]",
|
1474
|
+
expr.ExprFuncCall(
|
1475
|
+
expr.ExprIdent("relationship"),
|
1476
|
+
[
|
1477
|
+
PredefinedFn.keyword_assignment(
|
1478
|
+
"lazy",
|
1479
|
+
expr.ExprConstant("raise_on_sql"),
|
1480
|
+
),
|
1481
|
+
PredefinedFn.keyword_assignment(
|
1482
|
+
"foreign_keys",
|
1483
|
+
expr.ExprIdent(propname),
|
1484
|
+
),
|
1485
|
+
PredefinedFn.keyword_assignment(
|
1486
|
+
"init",
|
1487
|
+
expr.ExprConstant(False),
|
1488
|
+
),
|
1489
|
+
],
|
1490
|
+
),
|
1491
|
+
),
|
1492
|
+
)
|
1333
1493
|
return
|
1334
1494
|
|
1335
1495
|
# if the target class is not in the database,
|
@@ -1352,16 +1512,16 @@ def make_python_relational_object_property(
|
|
1352
1512
|
program.import_("sqlalchemy.orm.composite", True)
|
1353
1513
|
propvalargs: list[expr.Expr] = [expr.ExprIdent(prop.target.name)]
|
1354
1514
|
for p in prop.target.properties.values():
|
1515
|
+
pdtype = assert_isinstance(p, DataProperty).datatype.get_sqlalchemy_type()
|
1516
|
+
for dep in pdtype.deps:
|
1517
|
+
program.import_(dep, True)
|
1518
|
+
|
1355
1519
|
propvalargs.append(
|
1356
1520
|
expr.ExprFuncCall(
|
1357
1521
|
expr.ExprIdent("mapped_column"),
|
1358
1522
|
[
|
1359
1523
|
expr.ExprConstant(f"{prop.name}_{p.name}"),
|
1360
|
-
expr.ExprIdent(
|
1361
|
-
assert_isinstance(p, DataProperty)
|
1362
|
-
.datatype.get_sqlalchemy_type()
|
1363
|
-
.type
|
1364
|
-
),
|
1524
|
+
expr.ExprIdent(pdtype.type),
|
1365
1525
|
PredefinedFn.keyword_assignment(
|
1366
1526
|
"nullable",
|
1367
1527
|
expr.ExprConstant(prop.is_optional or p.is_optional),
|
@@ -1391,7 +1551,11 @@ def make_python_relational_object_property(
|
|
1391
1551
|
|
1392
1552
|
|
1393
1553
|
def make_python_relational_object_property_many_to_many(
|
1394
|
-
program: Program,
|
1554
|
+
program: Program,
|
1555
|
+
ast: AST,
|
1556
|
+
target_pkg: Package,
|
1557
|
+
cls: Class,
|
1558
|
+
prop: ObjectProperty,
|
1395
1559
|
):
|
1396
1560
|
assert cls.db is not None
|
1397
1561
|
assert prop.db is not None and prop.target.db is not None
|
@@ -1415,16 +1579,19 @@ def make_python_relational_object_property_many_to_many(
|
|
1415
1579
|
newprogram.import_("sqlalchemy.orm.Mapped", True)
|
1416
1580
|
newprogram.import_("sqlalchemy.orm.relationship", True)
|
1417
1581
|
newprogram.import_(f"{target_pkg.path}.base.Base", True)
|
1418
|
-
|
1419
|
-
|
1420
|
-
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
1424
|
-
|
1425
|
-
|
1426
|
-
|
1427
|
-
|
1582
|
+
|
1583
|
+
ident_manager = ImportHelper(
|
1584
|
+
newprogram,
|
1585
|
+
GLOBAL_IDENTS,
|
1586
|
+
)
|
1587
|
+
|
1588
|
+
ident_manager.python_import_for_hint(
|
1589
|
+
target_pkg.path + f".{cls.get_pymodule_name()}.{cls.name}",
|
1590
|
+
is_import_attr=True,
|
1591
|
+
)
|
1592
|
+
ident_manager.python_import_for_hint(
|
1593
|
+
target_pkg.path + f".{prop.target.get_pymodule_name()}.{prop.target.name}",
|
1594
|
+
is_import_attr=True,
|
1428
1595
|
)
|
1429
1596
|
|
1430
1597
|
newprogram.root(
|
@@ -32,11 +32,12 @@ def make_python_service(collection: DataCollection, target_pkg: Package):
|
|
32
32
|
program = Program()
|
33
33
|
program.import_("__future__.annotations", True)
|
34
34
|
program.import_(
|
35
|
-
app.models.db.path + f".{collection.
|
35
|
+
app.models.db.path + f".{collection.name}",
|
36
36
|
True,
|
37
37
|
)
|
38
38
|
program.import_(app.config.path + f".schema", True)
|
39
39
|
program.import_("sera.libs.base_service.BaseAsyncService", True)
|
40
|
+
program.import_(app.models.db.path + ".dbschema", True)
|
40
41
|
|
41
42
|
program.root(
|
42
43
|
stmt.LineBreak(),
|
@@ -55,7 +56,7 @@ def make_python_service(collection: DataCollection, target_pkg: Package):
|
|
55
56
|
expr.ExprIdent("super().__init__"),
|
56
57
|
[
|
57
58
|
expr.ExprRawPython(f"schema.classes['{cls.name}']"),
|
58
|
-
expr.ExprIdent(
|
59
|
+
expr.ExprIdent("dbschema"),
|
59
60
|
],
|
60
61
|
)
|
61
62
|
),
|
@@ -1242,6 +1242,18 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
1242
1242
|
if tstype.type in schema.enums
|
1243
1243
|
else []
|
1244
1244
|
),
|
1245
|
+
*(
|
1246
|
+
[
|
1247
|
+
(
|
1248
|
+
expr.ExprIdent("foreignKeyTarget"),
|
1249
|
+
expr.ExprConstant(prop.db.foreign_key.name),
|
1250
|
+
)
|
1251
|
+
]
|
1252
|
+
if prop.db is not None
|
1253
|
+
and prop.db.is_primary_key
|
1254
|
+
and prop.db.foreign_key is not None
|
1255
|
+
else []
|
1256
|
+
),
|
1245
1257
|
(
|
1246
1258
|
expr.ExprIdent("isRequired"),
|
1247
1259
|
expr.ExprConstant(
|
@@ -1618,14 +1630,19 @@ def _inject_type_for_invalid_value(tstype: TsTypeWithDep) -> TsTypeWithDep:
|
|
1618
1630
|
if m is not None:
|
1619
1631
|
# This is an array type, add string to the inner type
|
1620
1632
|
inner_type = m.group(1)
|
1633
|
+
inner_spectype = assert_not_null(
|
1634
|
+
re.match(r"(\(?[a-zA-Z \|]+\)?)(\[\])", tstype.spectype)
|
1635
|
+
).group(1)
|
1621
1636
|
if "string" not in inner_type:
|
1622
1637
|
if inner_type.startswith("(") and inner_type.endswith(")"):
|
1623
1638
|
# Already has parentheses
|
1624
1639
|
inner_type = f"{inner_type[:-1]} | string)"
|
1640
|
+
inner_spectype = f"{inner_spectype[:-1]} | string)"
|
1625
1641
|
else:
|
1626
1642
|
# Need to add parentheses
|
1627
1643
|
inner_type = f"({inner_type} | string)"
|
1628
|
-
|
1644
|
+
inner_spectype = f"({inner_spectype} | string)"
|
1645
|
+
return TsTypeWithDep(inner_type + "[]", inner_spectype + "[]", tstype.deps)
|
1629
1646
|
|
1630
1647
|
m = re.match(r"^\(?[a-zA-Z \|]+\)?$", tstype.type)
|
1631
1648
|
if m is not None:
|
@@ -1633,10 +1650,12 @@ def _inject_type_for_invalid_value(tstype: TsTypeWithDep) -> TsTypeWithDep:
|
|
1633
1650
|
if tstype.type.startswith("(") and tstype.type.endswith(")"):
|
1634
1651
|
# Already has parentheses
|
1635
1652
|
new_type = f"{tstype.type[:-1]} | string)"
|
1653
|
+
new_spectype = f"{tstype.spectype[:-1]} | string)"
|
1636
1654
|
else:
|
1637
1655
|
# Needs parentheses for clarity
|
1638
1656
|
new_type = f"({tstype.type} | string)"
|
1639
|
-
|
1657
|
+
new_spectype = f"({tstype.spectype} | string)"
|
1658
|
+
return TsTypeWithDep(new_type, new_spectype, tstype.deps)
|
1640
1659
|
return tstype
|
1641
1660
|
|
1642
1661
|
raise NotImplementedError(tstype.type)
|
sera/misc/__init__.py
CHANGED
@@ -12,6 +12,7 @@ from sera.misc._utils import (
|
|
12
12
|
load_data_from_dir,
|
13
13
|
replay_events,
|
14
14
|
to_camel_case,
|
15
|
+
to_kebab_case,
|
15
16
|
to_pascal_case,
|
16
17
|
to_snake_case,
|
17
18
|
)
|
@@ -34,4 +35,5 @@ __all__ = [
|
|
34
35
|
"load_data_from_dir",
|
35
36
|
"replay_events",
|
36
37
|
"auto_import",
|
38
|
+
"to_kebab_case",
|
37
39
|
]
|
sera/misc/_utils.py
CHANGED
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
3
3
|
import inspect
|
4
4
|
import re
|
5
5
|
from collections import defaultdict
|
6
|
+
from curses.ascii import isupper
|
7
|
+
from functools import lru_cache
|
6
8
|
from importlib import import_module
|
7
9
|
from pathlib import Path
|
8
10
|
from typing import (
|
@@ -82,13 +84,16 @@ def import_attr(attr_ident: str):
|
|
82
84
|
return getattr(module, cls)
|
83
85
|
|
84
86
|
|
85
|
-
|
87
|
+
@lru_cache(maxsize=1280)
|
88
|
+
def to_snake_case(name: str) -> str:
|
86
89
|
"""Convert camelCase to snake_case."""
|
87
|
-
snake = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2",
|
90
|
+
snake = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
|
88
91
|
snake = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", snake)
|
92
|
+
snake = snake.replace("-", "_")
|
89
93
|
return snake.lower()
|
90
94
|
|
91
95
|
|
96
|
+
@lru_cache(maxsize=1280)
|
92
97
|
def to_camel_case(snake: str) -> str:
|
93
98
|
"""Convert snake_case to camelCase."""
|
94
99
|
components = snake.split("_")
|
@@ -99,6 +104,7 @@ def to_camel_case(snake: str) -> str:
|
|
99
104
|
return out
|
100
105
|
|
101
106
|
|
107
|
+
@lru_cache(maxsize=1280)
|
102
108
|
def to_pascal_case(snake: str) -> str:
|
103
109
|
"""Convert snake_case to PascalCase."""
|
104
110
|
components = snake.split("_")
|
@@ -109,6 +115,15 @@ def to_pascal_case(snake: str) -> str:
|
|
109
115
|
return out
|
110
116
|
|
111
117
|
|
118
|
+
@lru_cache(maxsize=1280)
|
119
|
+
def to_kebab_case(name: str) -> str:
|
120
|
+
"""Convert a name to kebab-case."""
|
121
|
+
kebab = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1-\2", name)
|
122
|
+
kebab = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", kebab)
|
123
|
+
kebab = kebab.replace("_", "-")
|
124
|
+
return kebab.lower()
|
125
|
+
|
126
|
+
|
112
127
|
def assert_isinstance(x: Any, cls: type[T]) -> T:
|
113
128
|
if not isinstance(x, cls):
|
114
129
|
raise Exception(f"{type(x)} doesn't match with {type(cls)}")
|
sera/models/_class.py
CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
from dataclasses import dataclass, field
|
4
4
|
from typing import Optional
|
5
5
|
|
6
|
-
from sera.misc import to_snake_case
|
6
|
+
from sera.misc import to_kebab_case, to_snake_case
|
7
7
|
from sera.models._multi_lingual_string import MultiLingualString
|
8
8
|
from sera.models._property import DataProperty, ObjectProperty
|
9
9
|
|
@@ -80,4 +80,4 @@ class Class:
|
|
80
80
|
|
81
81
|
def get_tsmodule_name(self) -> str:
|
82
82
|
"""Get the typescript module name of this class as if there is a typescript module created to store this class only."""
|
83
|
-
return self.name
|
83
|
+
return to_kebab_case(self.name)
|
sera/models/_collection.py
CHANGED
@@ -23,7 +23,7 @@ class DataCollection:
|
|
23
23
|
"""Get the python module name of this collection as if there is a python module created to store this collection only."""
|
24
24
|
return self.cls.get_pymodule_name()
|
25
25
|
|
26
|
-
def get_queryable_fields(self) -> list[
|
26
|
+
def get_queryable_fields(self) -> list[str]:
|
27
27
|
"""Get the fields of this collection that can be used in a queries."""
|
28
28
|
output = []
|
29
29
|
for prop in self.cls.properties.values():
|
@@ -48,17 +48,21 @@ class DataCollection:
|
|
48
48
|
# This property is a data property or an object property not stored in the database, so we use its name
|
49
49
|
propname = prop.name
|
50
50
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
51
|
+
output.append(propname)
|
52
|
+
return output
|
53
|
+
|
54
|
+
def get_join_queryable_fields(self) -> dict[str, list[str]]:
|
55
|
+
"""Get the fields of this collection that can be used in join queries."""
|
56
|
+
output = {}
|
57
|
+
for prop in self.cls.properties.values():
|
58
|
+
if isinstance(prop, DataProperty) and prop.db.foreign_key is not None:
|
59
|
+
# This property is a foreign key, so we add it to the output
|
60
|
+
output[prop.name] = DataCollection(
|
61
|
+
prop.db.foreign_key
|
62
|
+
).get_queryable_fields()
|
63
|
+
elif isinstance(prop, ObjectProperty) and prop.target.db is not None:
|
64
|
+
output[prop.name] = DataCollection(prop.target).get_queryable_fields()
|
60
65
|
|
61
|
-
output.append((propname, convert_func))
|
62
66
|
return output
|
63
67
|
|
64
68
|
def get_service_name(self):
|
sera/models/_constraints.py
CHANGED
@@ -25,7 +25,7 @@ class Constraint:
|
|
25
25
|
# the UI will ensure to submit it in E.164 format
|
26
26
|
return r"msgspec.Meta(pattern=r'^\+[1-9]\d{1,14}$')"
|
27
27
|
elif self.name == "email":
|
28
|
-
return r"msgspec.Meta(min_length=3, max_length=254, pattern=r'^[^@]+@[^@]+\.[
|
28
|
+
return r"msgspec.Meta(min_length=3, max_length=254, pattern=r'^[^@]+@[^@]+\.[a-zA-Z\.]+$')"
|
29
29
|
elif self.name == "not_empty":
|
30
30
|
return "msgspec.Meta(min_length=1)"
|
31
31
|
elif self.name == "username":
|
sera/models/_datatype.py
CHANGED
@@ -2,12 +2,10 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import datetime
|
4
4
|
from dataclasses import dataclass, field
|
5
|
-
from typing import
|
5
|
+
from typing import Literal
|
6
6
|
|
7
7
|
from codegen.models import expr
|
8
8
|
|
9
|
-
from sera.misc import identity
|
10
|
-
|
11
9
|
PyDataType = Literal["str", "int", "datetime", "float", "bool", "bytes", "dict"]
|
12
10
|
TypescriptDataType = Literal["string", "number", "boolean"]
|
13
11
|
SQLAlchemyDataType = Literal[
|
@@ -69,30 +67,8 @@ class PyTypeWithDep:
|
|
69
67
|
"""Clone the type with the same dependencies."""
|
70
68
|
return PyTypeWithDep(type=self.type, deps=list(self.deps))
|
71
69
|
|
72
|
-
def
|
73
|
-
|
74
|
-
return ("identity", "sera.misc.identity")
|
75
|
-
if self.type == "int":
|
76
|
-
return ("TypeConversion.to_int", "sera.libs.api_helper.TypeConversion")
|
77
|
-
if self.type == "float":
|
78
|
-
return ("TypeConversion.to_float", "sera.libs.api_helper.TypeConversion")
|
79
|
-
if self.type == "bool":
|
80
|
-
return ("TypeConversion.to_bool", "sera.libs.api_helper.TypeConversion")
|
81
|
-
if any(
|
82
|
-
dep.find(".models.enums.") != -1 and dep.endswith(self.type)
|
83
|
-
for dep in self.deps
|
84
|
-
):
|
85
|
-
# This is an enum type, we directly use the enum constructor as the conversion function
|
86
|
-
return (
|
87
|
-
self.type,
|
88
|
-
[
|
89
|
-
dep
|
90
|
-
for dep in self.deps
|
91
|
-
if dep.find(".models.enums.") != -1 and dep.endswith(self.type)
|
92
|
-
][0],
|
93
|
-
)
|
94
|
-
else:
|
95
|
-
raise NotImplementedError()
|
70
|
+
def is_enum_type(self) -> bool:
|
71
|
+
return any(x.find(".models.enums.") != -1 for x in self.deps)
|
96
72
|
|
97
73
|
|
98
74
|
@dataclass
|
@@ -153,7 +129,7 @@ class TsTypeWithDep:
|
|
153
129
|
return value
|
154
130
|
if self.type == "Date":
|
155
131
|
return expr.ExprRawTypescript(f"new Date({value.to_typescript()})")
|
156
|
-
if
|
132
|
+
if self.is_enum_type():
|
157
133
|
# enum type, we don't need to do anything as we use strings for enums
|
158
134
|
return value
|
159
135
|
raise ValueError(f"Unknown type: {self.type}")
|
@@ -174,11 +150,14 @@ class TsTypeWithDep:
|
|
174
150
|
return expr.ExprRawTypescript(f"{value.to_typescript()}.toISOString()")
|
175
151
|
if self.type == "Date | undefined":
|
176
152
|
return expr.ExprRawTypescript(f"{value.to_typescript()}?.toISOString()")
|
177
|
-
if
|
153
|
+
if self.is_enum_type():
|
178
154
|
# enum type, we don't need to do anything as we use strings for enums
|
179
155
|
return value
|
180
156
|
raise ValueError(f"Unknown type: {self.type}")
|
181
157
|
|
158
|
+
def is_enum_type(self) -> bool:
|
159
|
+
return any(x.startswith("@.models.enums.") for x in self.deps)
|
160
|
+
|
182
161
|
|
183
162
|
@dataclass
|
184
163
|
class SQLTypeWithDep:
|
sera/models/_enum.py
CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
from collections import Counter
|
4
4
|
from dataclasses import dataclass
|
5
5
|
|
6
|
-
from sera.misc import to_snake_case
|
6
|
+
from sera.misc import to_kebab_case, to_snake_case
|
7
7
|
from sera.models._multi_lingual_string import MultiLingualString
|
8
8
|
|
9
9
|
|
@@ -44,7 +44,7 @@ class Enum:
|
|
44
44
|
|
45
45
|
def get_tsmodule_name(self) -> str:
|
46
46
|
"""Get the typescript module name of this enum as if there is a typescript module created to store this enum only."""
|
47
|
-
return self.name
|
47
|
+
return to_kebab_case(self.name)
|
48
48
|
|
49
49
|
def is_str_enum(self) -> bool:
|
50
50
|
"""Check if this enum is a string enum."""
|
sera/models/_module.py
CHANGED
@@ -39,6 +39,13 @@ class Package:
|
|
39
39
|
"""Create a module in this package"""
|
40
40
|
return Module(self, name, self.language)
|
41
41
|
|
42
|
+
def parent(self) -> Package:
|
43
|
+
"""Get the parent package"""
|
44
|
+
assert self.path.count(".") > 0, "Cannot get parent of top-level package"
|
45
|
+
return Package(
|
46
|
+
self.app, self.path.rsplit(".", 1)[0], self.dir.parent, self.language
|
47
|
+
)
|
48
|
+
|
42
49
|
|
43
50
|
@dataclass
|
44
51
|
class Module:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: sera-2
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.23.0
|
4
4
|
Summary:
|
5
5
|
Author: Binh Vu
|
6
6
|
Author-email: bvu687@gmail.com
|
@@ -9,7 +9,7 @@ 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)
|
12
|
-
Requires-Dist: codegen-2 (>=2.
|
12
|
+
Requires-Dist: codegen-2 (>=2.14.0,<3.0.0)
|
13
13
|
Requires-Dist: graph-wrapper (>=1.7.3,<2.0.0)
|
14
14
|
Requires-Dist: isort (==6.0.1)
|
15
15
|
Requires-Dist: litestar (>=2.15.1,<3.0.0)
|