sera-2 1.5.3__py3-none-any.whl → 1.6.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/make/make_app.py CHANGED
@@ -10,6 +10,7 @@ from loguru import logger
10
10
  from sera.make.make_python_api import make_python_api
11
11
  from sera.make.make_python_model import (
12
12
  make_python_data_model,
13
+ make_python_enums,
13
14
  make_python_relational_model,
14
15
  )
15
16
  from sera.make.make_python_services import make_python_service_structure
@@ -121,7 +122,7 @@ def make_app(
121
122
  ),
122
123
  ] = [],
123
124
  ):
124
- schema = parse_schema(schema_files)
125
+ schema = parse_schema(app_dir.name, schema_files)
125
126
 
126
127
  app = App(app_dir.name, app_dir, schema_files, language)
127
128
 
@@ -137,6 +138,8 @@ def make_app(
137
138
  + parts[1]
138
139
  for path in referenced_schema
139
140
  }
141
+
142
+ make_python_enums(schema, app.models.pkg("enums"), referenced_classes)
140
143
  make_python_data_model(schema, app.models.pkg("data"), referenced_classes)
141
144
  referenced_classes = {
142
145
  path.rsplit(".", 1)[1]: (parts := path.rsplit(".", 1))[0]
@@ -21,6 +21,53 @@ from sera.models import (
21
21
  from sera.typing import ObjectPath
22
22
 
23
23
 
24
+ def make_python_enums(
25
+ schema: Schema,
26
+ target_pkg: Package,
27
+ reference_objects: dict[str, ObjectPath],
28
+ ):
29
+ """Make enums defined in the schema.
30
+
31
+ Args:
32
+ schema: The schema to generate the classes from.
33
+ target_pkg: The package to write the enums to.
34
+ reference_objects: A dictionary of objects to their references (e.g., the ones that are defined outside and used as referenced such as Tenant).
35
+ """
36
+ for enum in schema.enums.values():
37
+ if enum.name in reference_objects:
38
+ # skip enums that are defined in different apps
39
+ continue
40
+
41
+ program = Program()
42
+ program.import_("__future__.annotations", True)
43
+ program.import_("enum.Enum", True)
44
+
45
+ enum_values = []
46
+ for value in enum.values.values():
47
+ enum_values.append(
48
+ stmt.DefClassVarStatement(
49
+ name=value.name,
50
+ type=None,
51
+ value=expr.ExprConstant(value.value),
52
+ )
53
+ )
54
+
55
+ program.root(
56
+ stmt.LineBreak(),
57
+ lambda ast: ast.class_(
58
+ enum.name,
59
+ (
60
+ [expr.ExprIdent("str")]
61
+ if enum.is_str_enum()
62
+ else [expr.ExprIdent("int")]
63
+ )
64
+ + [expr.ExprIdent("Enum")],
65
+ )(*enum_values),
66
+ )
67
+
68
+ target_pkg.module(enum.get_pymodule_name()).write(program)
69
+
70
+
24
71
  def make_python_data_model(
25
72
  schema: Schema, target_pkg: Package, reference_classes: dict[str, ObjectPath]
26
73
  ):
@@ -158,7 +205,7 @@ def make_python_data_model(
158
205
  if prop.default_value is not None:
159
206
  prop_default_value = expr.ExprConstant(prop.default_value)
160
207
  elif prop.default_factory is not None:
161
- program.import_(prop.default_factory.pyfunc)
208
+ program.import_(prop.default_factory.pyfunc, True)
162
209
  prop_default_value = expr.ExprFuncCall(
163
210
  expr.ExprIdent("msgspec.field"),
164
211
  [
@@ -753,7 +753,7 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
753
753
  ),
754
754
  (
755
755
  expr.ExprIdent("isEmbedded"),
756
- expr.ExprConstant(prop.target.db is not None),
756
+ expr.ExprConstant(prop.target.db is None),
757
757
  ),
758
758
  (
759
759
  expr.ExprIdent("isRequired"),
sera/models/_enum.py ADDED
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from dataclasses import dataclass
5
+
6
+ from sera.misc import to_snake_case
7
+ from sera.models._multi_lingual_string import MultiLingualString
8
+
9
+
10
+ @dataclass
11
+ class EnumValue:
12
+ name: str
13
+ value: str | int
14
+ description: MultiLingualString
15
+
16
+
17
+ @dataclass
18
+ class Enum:
19
+ """Enum class to represent a set of named values."""
20
+
21
+ # name of the enum in the application layer
22
+ name: str
23
+ values: dict[str, EnumValue]
24
+
25
+ def __post_init__(self):
26
+ # Ensure that all `value` attributes in EnumValue are unique
27
+ unique_values = {value.value for value in self.values.values()}
28
+ if len(unique_values) != len(self.values):
29
+ value_counts = Counter(value.value for value in self.values.values())
30
+ duplicates = [val for val, count in value_counts.items() if count > 1]
31
+ raise ValueError(
32
+ f"All 'value' attributes in EnumValue must be unique. Duplicates found: {duplicates}"
33
+ )
34
+
35
+ # Ensure that all `value` attributes in EnumValue are either all strings or all integers
36
+ if not (self.is_str_enum() or self.is_int_enum()):
37
+ raise ValueError(
38
+ "All 'value' attributes in EnumValue must be either all strings or all integers."
39
+ )
40
+
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."""
43
+ return to_snake_case(self.name)
44
+
45
+ def is_str_enum(self) -> bool:
46
+ """Check if this enum is a string enum."""
47
+ return all(isinstance(value.value, str) for value in self.values.values())
48
+
49
+ def is_int_enum(self) -> bool:
50
+ """Check if this enum is a int enum."""
51
+ return all(isinstance(value.value, int) for value in self.values.values())
sera/models/_parse.py CHANGED
@@ -10,12 +10,16 @@ from sera.models._class import Class, ClassDBMapInfo, Index
10
10
  from sera.models._constraints import Constraint, predefined_constraints
11
11
  from sera.models._datatype import (
12
12
  DataType,
13
+ PyTypeWithDep,
14
+ SQLTypeWithDep,
15
+ TsTypeWithDep,
13
16
  predefined_datatypes,
14
17
  predefined_py_datatypes,
15
18
  predefined_sql_datatypes,
16
19
  predefined_ts_datatypes,
17
20
  )
18
21
  from sera.models._default import DefaultFactory
22
+ from sera.models._enum import Enum, EnumValue
19
23
  from sera.models._multi_lingual_string import MultiLingualString
20
24
  from sera.models._property import (
21
25
  Cardinality,
@@ -30,13 +34,16 @@ from sera.models._property import (
30
34
  from sera.models._schema import Schema
31
35
 
32
36
 
33
- def parse_schema(files: Sequence[Path | str]) -> Schema:
34
- schema = Schema(classes={})
37
+ def parse_schema(name: str, files: Sequence[Path | str]) -> Schema:
38
+ schema = Schema(name=name, classes={}, enums={})
35
39
 
36
40
  # parse all classes
37
41
  raw_defs = {}
38
42
  for file in files:
39
43
  for k, v in serde.yaml.deser(file).items():
44
+ if k.startswith("enum:"):
45
+ schema.enums[k[5:]] = _parse_enum(schema, k[5:], v)
46
+ continue
40
47
  cdef = _parse_class_without_prop(schema, k, v)
41
48
  assert k not in schema.classes
42
49
  schema.classes[k] = cdef
@@ -45,7 +52,6 @@ def parse_schema(files: Sequence[Path | str]) -> Schema:
45
52
  # now parse properties of the classes
46
53
  for clsname, v in raw_defs.items():
47
54
  cdef = schema.classes[clsname]
48
-
49
55
  for propname, prop in (v["props"] or {}).items():
50
56
  assert propname not in cdef.properties
51
57
  cdef.properties[propname] = _parse_property(schema, propname, prop)
@@ -75,6 +81,22 @@ def _parse_class_without_prop(schema: Schema, clsname: str, cls: dict) -> Class:
75
81
  )
76
82
 
77
83
 
84
+ def _parse_enum(schema: Schema, enum_name: str, enum: dict) -> Enum:
85
+ values = {}
86
+ for k, v in enum.items():
87
+ if isinstance(v, (str, int)):
88
+ values[k] = EnumValue(
89
+ name=k, value=v, description=MultiLingualString.en("")
90
+ )
91
+ else:
92
+ values[k] = EnumValue(
93
+ name=k,
94
+ value=v["value"],
95
+ description=_parse_multi_lingual_string(v.get("desc", "")),
96
+ )
97
+ return Enum(name=enum_name, values=values)
98
+
99
+
78
100
  def _parse_property(
79
101
  schema: Schema, prop_name: str, prop: dict
80
102
  ) -> DataProperty | ObjectProperty:
@@ -93,14 +115,16 @@ def _parse_property(
93
115
  name=prop_name,
94
116
  label=_parse_multi_lingual_string(prop_name),
95
117
  description=_parse_multi_lingual_string(""),
96
- datatype=_parse_datatype(datatype),
118
+ datatype=_parse_datatype(schema, datatype),
97
119
  )
98
120
 
99
121
  db = prop.get("db", {})
100
122
  _data = prop.get("data", {})
101
123
  data_attrs = PropDataAttrs(
102
124
  is_private=_data.get("is_private", False),
103
- datatype=_parse_datatype(_data["datatype"]) if "datatype" in _data else None,
125
+ datatype=(
126
+ _parse_datatype(schema, _data["datatype"]) if "datatype" in _data else None
127
+ ),
104
128
  constraints=[
105
129
  _parse_constraint(constraint) for constraint in _data.get("constraints", [])
106
130
  ],
@@ -112,7 +136,7 @@ def _parse_property(
112
136
  name=prop_name,
113
137
  label=_parse_multi_lingual_string(prop.get("label", prop_name)),
114
138
  description=_parse_multi_lingual_string(prop.get("desc", "")),
115
- datatype=_parse_datatype(prop["datatype"]),
139
+ datatype=_parse_datatype(schema, prop["datatype"]),
116
140
  data=data_attrs,
117
141
  db=(
118
142
  DataPropDBInfo(
@@ -176,7 +200,7 @@ def _parse_constraint(constraint: str) -> Constraint:
176
200
  return predefined_constraints[constraint]
177
201
 
178
202
 
179
- def _parse_datatype(datatype: dict | str) -> DataType:
203
+ def _parse_datatype(schema: Schema, datatype: dict | str) -> DataType:
180
204
  if isinstance(datatype, str):
181
205
  if datatype.endswith("[]"):
182
206
  datatype = datatype[:-2]
@@ -184,6 +208,25 @@ def _parse_datatype(datatype: dict | str) -> DataType:
184
208
  else:
185
209
  is_list = False
186
210
 
211
+ if datatype.startswith("enum:"):
212
+ enum_name = datatype[5:]
213
+ if enum_name not in schema.enums:
214
+ raise NotImplementedError("Unknown enum: " + enum_name)
215
+ enum = schema.enums[enum_name]
216
+ return DataType(
217
+ # we can't set the correct dependency of this enum type because we do not know
218
+ # the correct package yet.
219
+ pytype=PyTypeWithDep(
220
+ type=enum.name,
221
+ dep=f"{schema.name}.models.enums.{enum.get_pymodule_name()}.{enum.name}",
222
+ ),
223
+ sqltype=SQLTypeWithDep(
224
+ type="String", mapped_pytype="str", deps=["sqlalchemy.String"]
225
+ ),
226
+ tstype=TsTypeWithDep(type=enum.name, dep="@/models/enums"),
227
+ is_list=is_list,
228
+ )
229
+
187
230
  if datatype not in predefined_datatypes:
188
231
  raise NotImplementedError(datatype)
189
232
 
@@ -228,7 +271,7 @@ def _parse_default_value(
228
271
  return default_value
229
272
 
230
273
 
231
- def _parse_default_factory(default_factory: str | None) -> str | None:
274
+ def _parse_default_factory(default_factory: dict | None) -> DefaultFactory | None:
232
275
  if default_factory is None:
233
276
  return None
234
277
  return DefaultFactory(
sera/models/_schema.py CHANGED
@@ -4,12 +4,16 @@ from dataclasses import dataclass
4
4
  from graphlib import TopologicalSorter
5
5
 
6
6
  from sera.models._class import Class
7
+ from sera.models._enum import Enum
7
8
  from sera.models._property import ObjectProperty
8
9
 
9
10
 
10
11
  @dataclass
11
12
  class Schema:
13
+ # top-level application name
14
+ name: str
12
15
  classes: dict[str, Class]
16
+ enums: dict[str, Enum]
13
17
 
14
18
  def topological_sort(self) -> list[Class]:
15
19
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sera-2
3
- Version: 1.5.3
3
+ Version: 1.6.0
4
4
  Summary:
5
5
  Author: Binh Vu
6
6
  Author-email: bvu687@gmail.com
@@ -6,11 +6,11 @@ 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
8
  sera/make/__main__.py,sha256=EvskvcZ5BikA6c0QyUXMbHfKrjr44W0JTmW_-iXFEGg,1416
9
- sera/make/make_app.py,sha256=4iXMmyvOSLvUWzZLrXWUg8jnkwOZyDH-AZVwKcQMfLY,5654
9
+ sera/make/make_app.py,sha256=HB6zIi456FZiLDtNMmO1crsCs3GlMLLUH-aJXHHaZxE,5771
10
10
  sera/make/make_python_api.py,sha256=RuJUm9z-4plBEtjobeOPr12o27OT-0tSeXI4ZlM3IY0,29433
11
- sera/make/make_python_model.py,sha256=WgloqiO7ePF4SNFCSxpp18MPe8EtSMEBK5KIg8pZbMU,33922
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=etj56Fjs2NmMDg0hsK0GirjcyFscoXic5Q3XZ3u_6gI,34039
13
+ sera/make/make_typescript_model.py,sha256=HMwiYjNSQwDNNVXZzzNGJE2wDOW51q4JXafIh03AW_I,34035
14
14
  sera/misc/__init__.py,sha256=G-vpwJtdS596Yle7W007vArHRwmK0QgGmLhzlqTiHMQ,311
15
15
  sera/misc/_utils.py,sha256=V5g4oLGHOhUCR75Kkcn1w01pAvGvaepK-T8Z3pIgHjI,1450
16
16
  sera/models/__init__.py,sha256=Kz3Cg7GIVzsM0qFG5ERB5gobLAxBy2SdxItIl73ui2g,759
@@ -19,12 +19,13 @@ sera/models/_collection.py,sha256=ZnQEriKC4X88Zz48Kn1AVZKH-1_l8OgWa-zf2kcQOOE,14
19
19
  sera/models/_constraints.py,sha256=lZmCh6Py0UVMdhTR7zUOPPzGqJGbmDCzf7xH7yITcbQ,1278
20
20
  sera/models/_datatype.py,sha256=uMxK_8wBLowaIMIAYCb3V17YmkzJrKKc5orjImzqWbA,5818
21
21
  sera/models/_default.py,sha256=ABggW6qdPR4ZDqIPJdJ0GCGQ-7kfsfZmQ_DchgZEa-I,137
22
+ sera/models/_enum.py,sha256=n9e0Nde9RSsO0V5zIAQbY4-qur8Q2euDQJSa6yP_VV8,1853
22
23
  sera/models/_module.py,sha256=4xZO8Ey4La7JTiCRfU50pYw4V6ssECH6dGIuplodwQY,5215
23
24
  sera/models/_multi_lingual_string.py,sha256=JETN6k00VH4wrA4w5vAHMEJV8fp3SY9bJebskFTjQLA,1186
24
- sera/models/_parse.py,sha256=KUGIOjtBBK0tdCNxSmeGK2OuD6mpFJ1ysdI2fAQgCnE,8026
25
+ sera/models/_parse.py,sha256=sJYfQtwek96ltpgxExG4xUbiLnU3qvNYhTP1CeyXGjs,9746
25
26
  sera/models/_property.py,sha256=CmEmgOShtSyNFq05YW3tGupwCIVRzPMKudXWld8utPk,5530
26
- sera/models/_schema.py,sha256=1F_Ict1NLvDcQDUZwcvnRS5MtsOTv584y3ChymUeUYA,1046
27
+ sera/models/_schema.py,sha256=r-Gqg9Lb_wR3UrbNvfXXgt_qs5bts0t2Ve7aquuF_OI,1155
27
28
  sera/typing.py,sha256=QkzT07FtBwoiX2WHXDUrU1zSPLoRmKJgsXa2kLyLMxU,327
28
- sera_2-1.5.3.dist-info/METADATA,sha256=XIYCIB_sKTSxZrrGB5WsIO_omVwaFT4vXqnxRuM7vV0,856
29
- sera_2-1.5.3.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
30
- sera_2-1.5.3.dist-info/RECORD,,
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,,
File without changes