sera-2 1.14.7__py3-none-any.whl → 1.16.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.
@@ -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
sera/misc/__init__.py CHANGED
@@ -3,6 +3,8 @@ from sera.misc._utils import (
3
3
  assert_isinstance,
4
4
  assert_not_null,
5
5
  filter_duplication,
6
+ identity,
7
+ load_data,
6
8
  to_camel_case,
7
9
  to_pascal_case,
8
10
  to_snake_case,
@@ -18,4 +20,6 @@ __all__ = [
18
20
  "to_pascal_case",
19
21
  "Formatter",
20
22
  "File",
23
+ "load_data",
24
+ "identity",
21
25
  ]
sera/misc/_utils.py CHANGED
@@ -1,8 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
+ from pathlib import Path
4
5
  from typing import Any, Callable, Iterable, Optional, TypeVar
5
6
 
7
+ import serde.csv
8
+ from sqlalchemy import Engine, text
9
+ from sqlalchemy.orm import Session
10
+ from tqdm import tqdm
11
+
6
12
  T = TypeVar("T")
7
13
  reserved_keywords = {
8
14
  "and",
@@ -102,3 +108,48 @@ def filter_duplication(
102
108
  keys.add(k)
103
109
  new_lst.append(k)
104
110
  return new_lst
111
+
112
+
113
+ def load_data(
114
+ engine: Engine,
115
+ create_db_and_tables: Callable[[], None],
116
+ table_files: list[tuple[type, Path]],
117
+ table_desers: dict[type, Callable[[dict], Any]],
118
+ verbose: bool = False,
119
+ ):
120
+ """
121
+ Load data into the database from specified CSV files.
122
+
123
+ Args:
124
+ engine: SQLAlchemy engine to connect to the database.
125
+ create_db_and_tables: Function to create database and tables.
126
+ table_files: List of tuples containing the class type and the corresponding CSV file path.
127
+ table_desers: Dictionary mapping class types to their deserializer functions.
128
+ verbose: If True, show progress bars during loading.
129
+ """
130
+ with Session(engine) as session:
131
+ create_db_and_tables()
132
+
133
+ for tbl, file in tqdm(table_files, disable=not verbose, desc="Loading data"):
134
+ if file.name.endswith(".csv"):
135
+ records = serde.csv.deser(file, deser_as_record=True)
136
+ else:
137
+ raise ValueError(f"Unsupported file format: {file.name}")
138
+ deser = table_desers[tbl]
139
+ records = [deser(row) for row in records]
140
+ for r in tqdm(records, desc=f"load {tbl.__name__}", disable=not verbose):
141
+ session.merge(r)
142
+ session.flush()
143
+
144
+ # Reset the sequence for each table
145
+ session.execute(
146
+ text(
147
+ f"SELECT setval('{tbl.__tablename__}_id_seq', (SELECT MAX(id) FROM \"{tbl.__tablename__}\"));"
148
+ )
149
+ )
150
+ session.commit()
151
+
152
+
153
+ def identity(x: T) -> T:
154
+ """Identity function that returns the input unchanged."""
155
+ return x
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sera-2
3
- Version: 1.14.7
3
+ Version: 1.16.0
4
4
  Summary:
5
5
  Author: Binh Vu
6
6
  Author-email: bvu687@gmail.com
@@ -19,10 +19,10 @@ sera/make/make_app.py,sha256=n9NtW73O3s_5Q31VHIRmnd-jEIcpDO7ksAsOdovde2s,5999
19
19
  sera/make/make_python_api.py,sha256=iXGbKQ3IJvsY1ur_fhurr_THFNnH66E3Wl85o0emUbw,26853
20
20
  sera/make/make_python_model.py,sha256=cRb-fuHX0WH7XPAAliu6lycC0iEjE5kcKg4bBU40GwQ,61275
21
21
  sera/make/make_python_services.py,sha256=0ZpWLwQ7Nwfn8BXAikAB4JRpNknpSJyJgY5b1cjtxV4,2073
22
- sera/make/make_typescript_model.py,sha256=3F-1TAbVb2-vidj5wKIi_U442JkBXZU3UYCZkodWRRM,64466
23
- sera/misc/__init__.py,sha256=Dh4uDq0D4N53h3zhvmwfa5a0TPVRSUvLzb0hkFuPirk,411
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=vBfbEChf7IMdLDj3CbdOXTUAdWNQTLLpAWwBoUcF3u0,2315
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
@@ -36,6 +36,6 @@ sera/models/_parse.py,sha256=MaGZty29lsVUFumWqNanIjAwptBNOMbVZMXLZ2A9_2g,12317
36
36
  sera/models/_property.py,sha256=2rSLs9JjSesrxrEugE7krYaBQOivKU882I8mZN94FlI,7017
37
37
  sera/models/_schema.py,sha256=VxJEiqgVvbXgcSUK4UW6JnRcggk4nsooVSE6MyXmfNY,1636
38
38
  sera/typing.py,sha256=Fl4-UzLJu1GdLLk_g87fA7nT9wQGelNnGzag6dg_0gs,980
39
- sera_2-1.14.7.dist-info/METADATA,sha256=r_FKuEQXlq3cQU-xQqcbdd61agMD-dvcg7Ar0n2Hq4s,852
40
- sera_2-1.14.7.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
41
- sera_2-1.14.7.dist-info/RECORD,,
39
+ sera_2-1.16.0.dist-info/METADATA,sha256=wZvclB-3wd_K5sN9Q64s832aMVXueS3p2xbDJKCplnk,852
40
+ sera_2-1.16.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
41
+ sera_2-1.16.0.dist-info/RECORD,,