sera-2 1.21.1__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.
@@ -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(stmt.DefClassVarStatement(propname, proptype, propval))
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, ast: AST, target_pkg: Package, cls: Class, prop: ObjectProperty
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
- newprogram.import_("typing.TYPE_CHECKING", True)
1419
- newprogram.import_area.if_(expr.ExprIdent("TYPE_CHECKING"))(
1420
- lambda ast00: ast00.import_(
1421
- target_pkg.path + f".{cls.get_pymodule_name()}.{cls.name}",
1422
- is_import_attr=True,
1423
- ),
1424
- lambda ast10: ast10.import_(
1425
- target_pkg.path + f".{prop.target.get_pymodule_name()}.{prop.target.name}",
1426
- is_import_attr=True,
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.get_pymodule_name()}.{collection.name}",
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(cls.name),
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
- return TsTypeWithDep(inner_type + "[]", tstype.deps)
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
- return TsTypeWithDep(new_type, tstype.deps)
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
- def to_snake_case(camelcase: str) -> str:
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", camelcase)
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)}")
@@ -417,3 +432,5 @@ async def replay_events(
417
432
  await dcg.execute_async(
418
433
  input={innode: (record,)}, context={"session": session}
419
434
  )
435
+
436
+ await session.commit()
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[0].lower() + self.name[1:]
83
+ return to_kebab_case(self.name)
@@ -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[tuple[str, tuple[str, str]]]:
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
- if isinstance(prop, DataProperty):
52
- convert_func = prop.datatype.pytype.get_string_conversion_func()
53
- else:
54
- assert isinstance(prop, ObjectProperty) and prop.target.db is not None
55
- target_idprop = prop.target.get_id_property()
56
- assert target_idprop is not None
57
- convert_func = (
58
- target_idprop.datatype.pytype.get_string_conversion_func()
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):
@@ -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 Callable, Literal
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 get_string_conversion_func(self) -> tuple[str, str]:
73
- if self.type == "str":
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 any(x.startswith("@.models.enum") for x in self.deps):
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 any(x.startswith("@.models.enum") for x in self.deps):
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[0].lower() + self.name[1:]
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.21.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.2,<3.0.0)
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)