sera-2 1.6.0__py3-none-any.whl → 1.6.1__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/make/__main__.py CHANGED
@@ -4,8 +4,9 @@ from pathlib import Path
4
4
  from typing import Annotated
5
5
 
6
6
  import typer
7
+
7
8
  from sera.make.make_app import make_app
8
- from sera.models import Language
9
+ from sera.typing import Language
9
10
 
10
11
  app = typer.Typer(pretty_exceptions_short=True, pretty_exceptions_enable=False)
11
12
 
sera/make/make_app.py CHANGED
@@ -7,6 +7,7 @@ from typing import Annotated
7
7
 
8
8
  from codegen.models import DeferredVar, PredefinedFn, Program, expr, stmt
9
9
  from loguru import logger
10
+
10
11
  from sera.make.make_python_api import make_python_api
11
12
  from sera.make.make_python_model import (
12
13
  make_python_data_model,
@@ -14,8 +15,13 @@ from sera.make.make_python_model import (
14
15
  make_python_relational_model,
15
16
  )
16
17
  from sera.make.make_python_services import make_python_service_structure
17
- from sera.make.make_typescript_model import make_typescript_data_model
18
- from sera.models import App, DataCollection, Language, parse_schema
18
+ from sera.make.make_typescript_model import (
19
+ make_typescript_data_model,
20
+ make_typescript_enum,
21
+ )
22
+ from sera.misc import Formatter
23
+ from sera.models import App, DataCollection, parse_schema
24
+ from sera.typing import Language
19
25
 
20
26
 
21
27
  def make_config(app: App):
@@ -164,8 +170,10 @@ def make_app(
164
170
  # generate services
165
171
  make_python_service_structure(app, collections)
166
172
  elif language == Language.Typescript:
173
+ make_typescript_enum(schema, app.models)
167
174
  make_typescript_data_model(schema, app.models)
168
175
 
176
+ Formatter.get_instance().process()
169
177
  return app
170
178
 
171
179
 
@@ -5,6 +5,7 @@ from typing import Any, Callable
5
5
  from codegen.models import AST, PredefinedFn, Program, expr, stmt
6
6
  from codegen.models.var import DeferredVar
7
7
  from loguru import logger
8
+
8
9
  from sera.misc import (
9
10
  assert_isinstance,
10
11
  assert_not_null,
@@ -15,6 +16,7 @@ from sera.misc import (
15
16
  from sera.models import (
16
17
  Class,
17
18
  DataProperty,
19
+ Enum,
18
20
  ObjectProperty,
19
21
  Package,
20
22
  Schema,
@@ -26,6 +28,15 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
26
28
  """Generate TypeScript data model from the schema. The data model aligns with the public data model in Python, not the database model."""
27
29
  app = target_pkg.app
28
30
 
31
+ # mapping from type alias of idprop to its real type
32
+ idprop_aliases = {}
33
+ for cls in schema.classes.values():
34
+ idprop = cls.get_id_property()
35
+ if idprop is not None:
36
+ idprop_aliases[f"{cls.name}Id"] = (
37
+ idprop.get_data_model_datatype().get_typescript_type()
38
+ )
39
+
29
40
  def clone_prop(prop: DataProperty | ObjectProperty, value: expr.Expr):
30
41
  # detect all complex types is hard, we can assume that any update to this does not mutate
31
42
  # the original object, then it's okay.
@@ -38,7 +49,6 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
38
49
  return
39
50
 
40
51
  idprop = cls.get_id_property()
41
-
42
52
  program = Program()
43
53
 
44
54
  prop_defs = []
@@ -161,7 +171,8 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
161
171
  else None
162
172
  ),
163
173
  stmt.LineBreak(),
164
- lambda ast00: ast00.interface(
174
+ lambda ast00: ast00.class_like(
175
+ "interface",
165
176
  cls.name + "ConstructorArgs",
166
177
  )(*prop_defs),
167
178
  stmt.LineBreak(),
@@ -230,9 +241,9 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
230
241
  update_field_funcs: list[Callable[[AST], Any]] = []
231
242
 
232
243
  for prop in cls.properties.values():
233
- if prop.data.is_private:
234
- # skip private fields as this is for APIs exchange
235
- continue
244
+ # if prop.data.is_private:
245
+ # # skip private fields as this is for APIs exchange
246
+ # continue
236
247
 
237
248
  propname = to_camel_case(prop.name)
238
249
 
@@ -291,15 +302,23 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
291
302
  ):
292
303
  create_propvalue = expr.ExprConstant(-1)
293
304
  else:
294
- create_propvalue = tstype.get_default()
305
+ if tstype.type in idprop_aliases:
306
+ create_propvalue = idprop_aliases[tstype.type].get_default()
307
+ else:
308
+ create_propvalue = tstype.get_default()
295
309
 
296
310
  if prop.db is not None and prop.db.is_primary_key:
297
311
  # for checking if the primary key is from the database or default (create_propvalue)
298
312
  cls_pk = (expr.ExprIdent(propname), create_propvalue)
299
313
 
300
- update_propvalue = PredefinedFn.attr_getter(
301
- expr.ExprIdent("record"), expr.ExprIdent(propname)
302
- )
314
+ # if this field is private, we cannot get it from the normal record
315
+ # we have to create a default value for it.
316
+ if prop.data.is_private:
317
+ update_propvalue = create_propvalue
318
+ else:
319
+ update_propvalue = PredefinedFn.attr_getter(
320
+ expr.ExprIdent("record"), expr.ExprIdent(propname)
321
+ )
303
322
 
304
323
  ser_args.append(
305
324
  (
@@ -333,7 +352,22 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
333
352
  )
334
353
  if prop.cardinality.is_star_to_many():
335
354
  tstype = tstype.as_list_type()
336
- create_propvalue = tstype.get_default()
355
+ create_propvalue = expr.ExprConstant([])
356
+ else:
357
+ # if target class has an auto-increment primary key, we set a different default value
358
+ # to be -1 to avoid start from 0
359
+ target_idprop = prop.target.get_id_property()
360
+ if (
361
+ target_idprop is not None
362
+ and target_idprop.db is not None
363
+ and target_idprop.db.is_primary_key
364
+ and target_idprop.db.is_auto_increment
365
+ ):
366
+ create_propvalue = expr.ExprConstant(-1)
367
+ else:
368
+ assert tstype.type in idprop_aliases
369
+ create_propvalue = idprop_aliases[tstype.type].get_default()
370
+
337
371
  update_propvalue = PredefinedFn.attr_getter(
338
372
  expr.ExprIdent("record"), expr.ExprIdent(propname)
339
373
  )
@@ -474,7 +508,8 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
474
508
  observable_args.sort(key=lambda x: {"observable": 0, "action": 1}[x[1].ident])
475
509
 
476
510
  program.root(
477
- lambda ast00: ast00.interface(
511
+ lambda ast00: ast00.class_like(
512
+ "interface",
478
513
  draft_clsname + "ConstructorArgs",
479
514
  )(*prop_defs),
480
515
  stmt.LineBreak(),
@@ -861,3 +896,44 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
861
896
  make_definition(cls, pkg)
862
897
 
863
898
  make_index(pkg)
899
+
900
+
901
+ def make_typescript_enum(schema: Schema, target_pkg: Package):
902
+ """Make typescript enum for the schema"""
903
+ enum_pkg = target_pkg.pkg("enums")
904
+
905
+ def make_enum(enum: Enum, pkg: Package):
906
+ program = Program()
907
+ program.root(
908
+ stmt.LineBreak(),
909
+ lambda ast: ast.class_like("enum", enum.name)(
910
+ *[
911
+ stmt.DefClassVarStatement(
912
+ name=value.name,
913
+ type=None,
914
+ value=expr.ExprConstant(value.value),
915
+ )
916
+ for value in enum.values.values()
917
+ ]
918
+ ),
919
+ stmt.LineBreak(),
920
+ stmt.TypescriptStatement("export " + enum.name + ";"),
921
+ )
922
+ pkg.module(enum.get_tsmodule_name()).write(program)
923
+
924
+ for enum in schema.enums.values():
925
+ make_enum(enum, enum_pkg)
926
+
927
+ program = Program()
928
+ for enum in schema.enums.values():
929
+ program.import_(f"@.models.enums.{enum.get_tsmodule_name()}.{enum.name}", True)
930
+
931
+ program.root(
932
+ stmt.LineBreak(),
933
+ stmt.TypescriptStatement(
934
+ "export { "
935
+ + ", ".join([enum.name for enum in schema.enums.values()])
936
+ + "};"
937
+ ),
938
+ )
939
+ enum_pkg.module("index").write(program)
sera/misc/__init__.py CHANGED
@@ -1,3 +1,4 @@
1
+ from sera.misc._formatter import File, Formatter
1
2
  from sera.misc._utils import (
2
3
  assert_isinstance,
3
4
  assert_not_null,
@@ -14,4 +15,7 @@ __all__ = [
14
15
  "assert_not_null",
15
16
  "to_snake_case",
16
17
  "to_camel_case",
18
+ "to_pascal_case",
19
+ "Formatter",
20
+ "File",
17
21
  ]
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from tqdm import tqdm
9
+
10
+ from sera.typing import Language
11
+
12
+
13
+ @dataclass
14
+ class File:
15
+ path: Path
16
+ language: Language
17
+
18
+
19
+ class Formatter:
20
+ instance = None
21
+
22
+ def __init__(self):
23
+ self.pending_files: list[File] = []
24
+
25
+ @staticmethod
26
+ def get_instance():
27
+ if Formatter.instance is None:
28
+ Formatter.instance = Formatter()
29
+ return Formatter.instance
30
+
31
+ def register(self, file: File):
32
+ self.pending_files.append(file)
33
+
34
+ def process(self):
35
+ """Format pending files in parallel"""
36
+
37
+ def format_file(file: File):
38
+ if file.language == Language.Typescript:
39
+ try:
40
+ subprocess.check_output(
41
+ ["npx", "prettier", "--write", str(file.path.absolute())],
42
+ cwd=file.path.parent,
43
+ )
44
+ except subprocess.CalledProcessError as e:
45
+ print(f"Error formatting {file.path}: {e}")
46
+ raise
47
+ else:
48
+ raise NotImplementedError(
49
+ f"Formatting not implemented for {file.language}"
50
+ )
51
+
52
+ with ThreadPoolExecutor() as executor:
53
+ list(
54
+ tqdm(
55
+ executor.map(format_file, self.pending_files),
56
+ total=len(self.pending_files),
57
+ desc="Formatting files",
58
+ )
59
+ )
60
+
61
+ self.pending_files.clear()
sera/models/__init__.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from sera.models._class import Class
2
2
  from sera.models._collection import DataCollection
3
3
  from sera.models._datatype import DataType, PyTypeWithDep, TsTypeWithDep
4
- from sera.models._module import App, Language, Module, Package
4
+ from sera.models._enum import Enum
5
+ from sera.models._module import App, Module, Package
5
6
  from sera.models._multi_lingual_string import MultiLingualString
6
7
  from sera.models._parse import parse_schema
7
8
  from sera.models._property import Cardinality, DataProperty, ObjectProperty, Property
@@ -21,7 +22,7 @@ __all__ = [
21
22
  "DataCollection",
22
23
  "Module",
23
24
  "App",
24
- "Language",
25
25
  "PyTypeWithDep",
26
26
  "TsTypeWithDep",
27
+ "Enum",
27
28
  ]
sera/models/_enum.py CHANGED
@@ -39,9 +39,13 @@ class Enum:
39
39
  )
40
40
 
41
41
  def get_pymodule_name(self) -> str:
42
- """Get the python module name of this class as if there is a python module created to store this class only."""
42
+ """Get the python module name of this enum as if there is a python module created to store this enum only."""
43
43
  return to_snake_case(self.name)
44
44
 
45
+ def get_tsmodule_name(self) -> str:
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:]
48
+
45
49
  def is_str_enum(self) -> bool:
46
50
  """Check if this enum is a string enum."""
47
51
  return all(isinstance(value.value, str) for value in self.values.values())
sera/models/_module.py CHANGED
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import subprocess
4
4
  from dataclasses import dataclass
5
- from enum import Enum
6
5
  from pathlib import Path
7
6
  from typing import Sequence
8
7
 
@@ -12,10 +11,8 @@ import isort
12
11
  from codegen.models import Program
13
12
  from loguru import logger
14
13
 
15
-
16
- class Language(str, Enum):
17
- Python = "python"
18
- Typescript = "typescript"
14
+ from sera.misc import File, Formatter
15
+ from sera.typing import Language
19
16
 
20
17
 
21
18
  @dataclass
@@ -106,15 +103,9 @@ class Module:
106
103
  outfile.write_text(copyright_statement + code)
107
104
 
108
105
  if self.language == Language.Typescript:
109
- # invoke prettier to format the code
110
- try:
111
- subprocess.check_output(
112
- ["npx", "prettier", "--write", str(outfile.absolute())],
113
- cwd=self.package.app.root.dir,
114
- )
115
- except Exception as e:
116
- logger.error("Error formatting Typescript file: {}", e)
117
- raise
106
+ Formatter.get_instance().register(
107
+ File(path=outfile, language=self.language)
108
+ )
118
109
 
119
110
  def exists(self) -> bool:
120
111
  """Check if the module exists"""
sera/typing.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from enum import Enum
3
4
  from typing import Annotated, TypeVar
4
5
 
5
6
 
@@ -12,3 +13,8 @@ FieldName = Annotated[str, doc("field name of a class")]
12
13
  ObjectPath = Annotated[
13
14
  str, doc("path of an object (e.g., can be function, class, etc.)")
14
15
  ]
16
+
17
+
18
+ class Language(str, Enum):
19
+ Python = "python"
20
+ Typescript = "typescript"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sera-2
3
- Version: 1.6.0
3
+ Version: 1.6.1
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,<26.0.0)
12
- Requires-Dist: codegen-2 (>=2.5.1,<3.0.0)
12
+ Requires-Dist: codegen-2 (>=2.6.0,<3.0.0)
13
13
  Requires-Dist: isort (>=6.0.1,<7.0.0)
14
14
  Requires-Dist: litestar (>=2.15.1,<3.0.0)
15
15
  Requires-Dist: loguru (>=0.7.0,<0.8.0)
@@ -5,27 +5,28 @@ sera/libs/api_helper.py,sha256=hUEy0INHM18lxTQ348tgbXNceOHcjiAnqmuL_8CRpLQ,2509
5
5
  sera/libs/base_orm.py,sha256=Fte-NOq62-4ulpjKmdO4V5pQKy9JfaL3ryBqpRRKkNQ,2913
6
6
  sera/libs/base_service.py,sha256=CNAdOPxgXfvdANj75qd4GW5EC0WYW-L5Utm0ozfBbio,4075
7
7
  sera/make/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- sera/make/__main__.py,sha256=EvskvcZ5BikA6c0QyUXMbHfKrjr44W0JTmW_-iXFEGg,1416
9
- sera/make/make_app.py,sha256=HB6zIi456FZiLDtNMmO1crsCs3GlMLLUH-aJXHHaZxE,5771
8
+ sera/make/__main__.py,sha256=G5O7s6135-708honwqMFn2yPTs06WbGQTHpupID0eZ4,1417
9
+ sera/make/make_app.py,sha256=3Crq-Y-VzeHKKAaxfrEqguqTZEtdNPOuGojBQpC9QHw,5950
10
10
  sera/make/make_python_api.py,sha256=RuJUm9z-4plBEtjobeOPr12o27OT-0tSeXI4ZlM3IY0,29433
11
11
  sera/make/make_python_model.py,sha256=8U3fmIK_ahpxyMsCypQvISBVoncRUspYEppWX-_MLtU,35402
12
12
  sera/make/make_python_services.py,sha256=RsinYZdfkrTlTn9CT50VgqGs9w6IZawsJx-KEmqfnEY,2062
13
- sera/make/make_typescript_model.py,sha256=HMwiYjNSQwDNNVXZzzNGJE2wDOW51q4JXafIh03AW_I,34035
14
- sera/misc/__init__.py,sha256=G-vpwJtdS596Yle7W007vArHRwmK0QgGmLhzlqTiHMQ,311
13
+ sera/make/make_typescript_model.py,sha256=dT1ZHpsDvegX07jxDRncd_iu4FAiaV_7xyioWBJdbes,36999
14
+ sera/misc/__init__.py,sha256=Dh4uDq0D4N53h3zhvmwfa5a0TPVRSUvLzb0hkFuPirk,411
15
+ sera/misc/_formatter.py,sha256=_m1lK2__wD0I0xnZ7JpjQeaxyf6ygYOZdrYzaYJd4s4,1625
15
16
  sera/misc/_utils.py,sha256=V5g4oLGHOhUCR75Kkcn1w01pAvGvaepK-T8Z3pIgHjI,1450
16
- sera/models/__init__.py,sha256=Kz3Cg7GIVzsM0qFG5ERB5gobLAxBy2SdxItIl73ui2g,759
17
+ sera/models/__init__.py,sha256=vJC5Kzo_N7wd16ocNPy1VvAZDGNiWeiAhWJ4ihATKvA,780
17
18
  sera/models/_class.py,sha256=Wf0e8x6-szG9TzoFkAlqj7_dG0SCICMBw_333n3paxk,2514
18
19
  sera/models/_collection.py,sha256=ZnQEriKC4X88Zz48Kn1AVZKH-1_l8OgWa-zf2kcQOOE,1414
19
20
  sera/models/_constraints.py,sha256=lZmCh6Py0UVMdhTR7zUOPPzGqJGbmDCzf7xH7yITcbQ,1278
20
21
  sera/models/_datatype.py,sha256=uMxK_8wBLowaIMIAYCb3V17YmkzJrKKc5orjImzqWbA,5818
21
22
  sera/models/_default.py,sha256=ABggW6qdPR4ZDqIPJdJ0GCGQ-7kfsfZmQ_DchgZEa-I,137
22
- sera/models/_enum.py,sha256=n9e0Nde9RSsO0V5zIAQbY4-qur8Q2euDQJSa6yP_VV8,1853
23
- sera/models/_module.py,sha256=4xZO8Ey4La7JTiCRfU50pYw4V6ssECH6dGIuplodwQY,5215
23
+ sera/models/_enum.py,sha256=sy0q7E646F-APsqrVQ52r1fAQ_DCAeaNq5YM5QN3zIk,2070
24
+ sera/models/_module.py,sha256=CAk8CCuo3hs_mBkmfXDV-BsMQhv4Wgtp0tcY78JLjUE,4922
24
25
  sera/models/_multi_lingual_string.py,sha256=JETN6k00VH4wrA4w5vAHMEJV8fp3SY9bJebskFTjQLA,1186
25
26
  sera/models/_parse.py,sha256=sJYfQtwek96ltpgxExG4xUbiLnU3qvNYhTP1CeyXGjs,9746
26
27
  sera/models/_property.py,sha256=CmEmgOShtSyNFq05YW3tGupwCIVRzPMKudXWld8utPk,5530
27
28
  sera/models/_schema.py,sha256=r-Gqg9Lb_wR3UrbNvfXXgt_qs5bts0t2Ve7aquuF_OI,1155
28
- sera/typing.py,sha256=QkzT07FtBwoiX2WHXDUrU1zSPLoRmKJgsXa2kLyLMxU,327
29
- sera_2-1.6.0.dist-info/METADATA,sha256=QF_GIRGrujh3P3FFhjE4F-eVuMy3qWEFiJ4SS5r9-QQ,856
30
- sera_2-1.6.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
31
- sera_2-1.6.0.dist-info/RECORD,,
29
+ sera/typing.py,sha256=Ak0Su0EhjFirWOOwbIW8u4YpEwP8apfpIH5Tv9foTRo,430
30
+ sera_2-1.6.1.dist-info/METADATA,sha256=GuYpBmAl_5gq9go1LZ7AcjVsXi3Nx5a21dDYIpEokzg,856
31
+ sera_2-1.6.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
32
+ sera_2-1.6.1.dist-info/RECORD,,
File without changes