sera-2 1.5.4__tar.gz → 1.6.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.
- {sera_2-1.5.4 → sera_2-1.6.0}/PKG-INFO +1 -1
- {sera_2-1.5.4 → sera_2-1.6.0}/pyproject.toml +1 -1
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/make/make_app.py +4 -1
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/make/make_python_model.py +48 -1
- sera_2-1.6.0/sera/models/_enum.py +51 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/models/_parse.py +51 -8
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/models/_schema.py +4 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/README.md +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/__init__.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/constants.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/libs/__init__.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/libs/api_helper.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/libs/base_orm.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/libs/base_service.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/make/__init__.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/make/__main__.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/make/make_python_api.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/make/make_python_services.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/make/make_typescript_model.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/misc/__init__.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/misc/_utils.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/models/__init__.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/models/_class.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/models/_collection.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/models/_constraints.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/models/_datatype.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/models/_default.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/models/_module.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/models/_multi_lingual_string.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/models/_property.py +0 -0
- {sera_2-1.5.4 → sera_2-1.6.0}/sera/typing.py +0 -0
@@ -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
|
[
|
@@ -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())
|
@@ -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=
|
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:
|
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(
|
@@ -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
|
"""
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|