sera-2 1.15.0__tar.gz → 1.17.0__tar.gz

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.
Files changed (41) hide show
  1. {sera_2-1.15.0 → sera_2-1.17.0}/PKG-INFO +1 -1
  2. {sera_2-1.15.0 → sera_2-1.17.0}/pyproject.toml +1 -1
  3. {sera_2-1.15.0 → sera_2-1.17.0}/sera/exports/schema.py +14 -0
  4. {sera_2-1.15.0 → sera_2-1.17.0}/sera/make/make_python_model.py +38 -4
  5. {sera_2-1.15.0 → sera_2-1.17.0}/sera/make/make_typescript_model.py +80 -10
  6. {sera_2-1.15.0 → sera_2-1.17.0}/sera/misc/__init__.py +2 -0
  7. {sera_2-1.15.0 → sera_2-1.17.0}/sera/misc/_utils.py +5 -0
  8. {sera_2-1.15.0 → sera_2-1.17.0}/sera/models/_parse.py +1 -0
  9. {sera_2-1.15.0 → sera_2-1.17.0}/sera/models/_property.py +6 -0
  10. {sera_2-1.15.0 → sera_2-1.17.0}/sera/typing.py +1 -0
  11. {sera_2-1.15.0 → sera_2-1.17.0}/README.md +0 -0
  12. {sera_2-1.15.0 → sera_2-1.17.0}/sera/__init__.py +0 -0
  13. {sera_2-1.15.0 → sera_2-1.17.0}/sera/constants.py +0 -0
  14. {sera_2-1.15.0 → sera_2-1.17.0}/sera/exports/__init__.py +0 -0
  15. {sera_2-1.15.0 → sera_2-1.17.0}/sera/exports/test.py +0 -0
  16. {sera_2-1.15.0 → sera_2-1.17.0}/sera/libs/__init__.py +0 -0
  17. {sera_2-1.15.0 → sera_2-1.17.0}/sera/libs/api_helper.py +0 -0
  18. {sera_2-1.15.0 → sera_2-1.17.0}/sera/libs/api_test_helper.py +0 -0
  19. {sera_2-1.15.0 → sera_2-1.17.0}/sera/libs/base_orm.py +0 -0
  20. {sera_2-1.15.0 → sera_2-1.17.0}/sera/libs/base_service.py +0 -0
  21. {sera_2-1.15.0 → sera_2-1.17.0}/sera/libs/dag/__init__.py +0 -0
  22. {sera_2-1.15.0 → sera_2-1.17.0}/sera/libs/dag/_dag.py +0 -0
  23. {sera_2-1.15.0 → sera_2-1.17.0}/sera/libs/middlewares/__init__.py +0 -0
  24. {sera_2-1.15.0 → sera_2-1.17.0}/sera/libs/middlewares/auth.py +0 -0
  25. {sera_2-1.15.0 → sera_2-1.17.0}/sera/libs/middlewares/uscp.py +0 -0
  26. {sera_2-1.15.0 → sera_2-1.17.0}/sera/make/__init__.py +0 -0
  27. {sera_2-1.15.0 → sera_2-1.17.0}/sera/make/__main__.py +0 -0
  28. {sera_2-1.15.0 → sera_2-1.17.0}/sera/make/make_app.py +0 -0
  29. {sera_2-1.15.0 → sera_2-1.17.0}/sera/make/make_python_api.py +0 -0
  30. {sera_2-1.15.0 → sera_2-1.17.0}/sera/make/make_python_services.py +0 -0
  31. {sera_2-1.15.0 → sera_2-1.17.0}/sera/misc/_formatter.py +0 -0
  32. {sera_2-1.15.0 → sera_2-1.17.0}/sera/models/__init__.py +0 -0
  33. {sera_2-1.15.0 → sera_2-1.17.0}/sera/models/_class.py +0 -0
  34. {sera_2-1.15.0 → sera_2-1.17.0}/sera/models/_collection.py +0 -0
  35. {sera_2-1.15.0 → sera_2-1.17.0}/sera/models/_constraints.py +0 -0
  36. {sera_2-1.15.0 → sera_2-1.17.0}/sera/models/_datatype.py +0 -0
  37. {sera_2-1.15.0 → sera_2-1.17.0}/sera/models/_default.py +0 -0
  38. {sera_2-1.15.0 → sera_2-1.17.0}/sera/models/_enum.py +0 -0
  39. {sera_2-1.15.0 → sera_2-1.17.0}/sera/models/_module.py +0 -0
  40. {sera_2-1.15.0 → sera_2-1.17.0}/sera/models/_multi_lingual_string.py +0 -0
  41. {sera_2-1.15.0 → sera_2-1.17.0}/sera/models/_schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sera-2
3
- Version: 1.15.0
3
+ Version: 1.17.0
4
4
  Summary:
5
5
  Author: Binh Vu
6
6
  Author-email: bvu687@gmail.com
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "sera-2"
3
- version = "1.15.0"
3
+ version = "1.17.0"
4
4
  description = ""
5
5
  authors = ["Binh Vu <bvu687@gmail.com>"]
6
6
  readme = "README.md"
@@ -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
@@ -1079,12 +1079,10 @@ def make_python_relational_model(
1079
1079
 
1080
1080
  target_pkg.module("base").write(program)
1081
1081
 
1082
- custom_types: list[ObjectProperty] = []
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
- continue
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
- PredefinedFn.attr_getter(
424
- expr.ExprIdent("this"), expr.ExprIdent(propname)
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 PredefinedFn.attr_getter(
430
- expr.ExprIdent("this"), expr.ExprIdent(propname)
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
- PredefinedFn.attr_getter(
459
- expr.ExprIdent("this"), expr.ExprIdent(propname)
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 PredefinedFn.attr_getter(
465
- expr.ExprIdent("this"), expr.ExprIdent(propname)
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
@@ -3,6 +3,7 @@ from sera.misc._utils import (
3
3
  assert_isinstance,
4
4
  assert_not_null,
5
5
  filter_duplication,
6
+ identity,
6
7
  load_data,
7
8
  to_camel_case,
8
9
  to_pascal_case,
@@ -20,4 +21,5 @@ __all__ = [
20
21
  "Formatter",
21
22
  "File",
22
23
  "load_data",
24
+ "identity",
23
25
  ]
@@ -148,3 +148,8 @@ def load_data(
148
148
  )
149
149
  )
150
150
  session.commit()
151
+
152
+
153
+ def identity(x: T) -> T:
154
+ """Identity function that returns the input unchanged."""
155
+ return x
@@ -160,6 +160,7 @@ def _parse_property(
160
160
  is_indexed=db.get("is_indexed", False)
161
161
  or db.get("is_unique", False)
162
162
  or db.get("is_primary_key", False),
163
+ foreign_key=schema.classes.get(db.get("foreign_key")),
163
164
  )
164
165
  if "db" in prop
165
166
  else None
@@ -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)
@@ -37,4 +37,5 @@ GLOBAL_IDENTS = {
37
37
  "AsyncSession": "sqlalchemy.ext.asyncio.AsyncSession",
38
38
  "ASGIConnection": "litestar.connection.ASGIConnection",
39
39
  "UNSET": "sera.typing.UNSET",
40
+ "ForeignKey": "sqlalchemy.ForeignKey",
40
41
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes