sera-2 1.5.4__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 +2 -1
- sera/make/make_app.py +14 -3
- sera/make/make_python_model.py +48 -1
- sera/make/make_typescript_model.py +87 -11
- sera/misc/__init__.py +4 -0
- sera/misc/_formatter.py +61 -0
- sera/models/__init__.py +3 -2
- sera/models/_enum.py +55 -0
- sera/models/_module.py +5 -14
- sera/models/_parse.py +51 -8
- sera/models/_schema.py +4 -0
- sera/typing.py +6 -0
- {sera_2-1.5.4.dist-info → sera_2-1.6.1.dist-info}/METADATA +2 -2
- {sera_2-1.5.4.dist-info → sera_2-1.6.1.dist-info}/RECORD +15 -13
- {sera_2-1.5.4.dist-info → sera_2-1.6.1.dist-info}/WHEEL +0 -0
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.
|
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,14 +7,21 @@ 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
|
+
make_python_enums,
|
13
15
|
make_python_relational_model,
|
14
16
|
)
|
15
17
|
from sera.make.make_python_services import make_python_service_structure
|
16
|
-
from sera.make.make_typescript_model import
|
17
|
-
|
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
|
18
25
|
|
19
26
|
|
20
27
|
def make_config(app: App):
|
@@ -121,7 +128,7 @@ def make_app(
|
|
121
128
|
),
|
122
129
|
] = [],
|
123
130
|
):
|
124
|
-
schema = parse_schema(schema_files)
|
131
|
+
schema = parse_schema(app_dir.name, schema_files)
|
125
132
|
|
126
133
|
app = App(app_dir.name, app_dir, schema_files, language)
|
127
134
|
|
@@ -137,6 +144,8 @@ def make_app(
|
|
137
144
|
+ parts[1]
|
138
145
|
for path in referenced_schema
|
139
146
|
}
|
147
|
+
|
148
|
+
make_python_enums(schema, app.models.pkg("enums"), referenced_classes)
|
140
149
|
make_python_data_model(schema, app.models.pkg("data"), referenced_classes)
|
141
150
|
referenced_classes = {
|
142
151
|
path.rsplit(".", 1)[1]: (parts := path.rsplit(".", 1))[0]
|
@@ -161,8 +170,10 @@ def make_app(
|
|
161
170
|
# generate services
|
162
171
|
make_python_service_structure(app, collections)
|
163
172
|
elif language == Language.Typescript:
|
173
|
+
make_typescript_enum(schema, app.models)
|
164
174
|
make_typescript_data_model(schema, app.models)
|
165
175
|
|
176
|
+
Formatter.get_instance().process()
|
166
177
|
return app
|
167
178
|
|
168
179
|
|
sera/make/make_python_model.py
CHANGED
@@ -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
|
[
|
@@ -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.
|
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
|
-
|
235
|
-
|
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
|
-
|
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
|
-
|
301
|
-
|
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
|
-
|
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.
|
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
|
]
|
sera/misc/_formatter.py
ADDED
@@ -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.
|
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
ADDED
@@ -0,0 +1,55 @@
|
|
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 enum as if there is a python module created to store this enum only."""
|
43
|
+
return to_snake_case(self.name)
|
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
|
+
|
49
|
+
def is_str_enum(self) -> bool:
|
50
|
+
"""Check if this enum is a string enum."""
|
51
|
+
return all(isinstance(value.value, str) for value in self.values.values())
|
52
|
+
|
53
|
+
def is_int_enum(self) -> bool:
|
54
|
+
"""Check if this enum is a int enum."""
|
55
|
+
return all(isinstance(value.value, int) 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
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
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/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=
|
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(
|
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
|
"""
|
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.
|
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.
|
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,26 +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=
|
9
|
-
sera/make/make_app.py,sha256=
|
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
|
-
sera/make/make_python_model.py,sha256=
|
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=
|
14
|
-
sera/misc/__init__.py,sha256=
|
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=
|
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/
|
23
|
+
sera/models/_enum.py,sha256=sy0q7E646F-APsqrVQ52r1fAQ_DCAeaNq5YM5QN3zIk,2070
|
24
|
+
sera/models/_module.py,sha256=CAk8CCuo3hs_mBkmfXDV-BsMQhv4Wgtp0tcY78JLjUE,4922
|
23
25
|
sera/models/_multi_lingual_string.py,sha256=JETN6k00VH4wrA4w5vAHMEJV8fp3SY9bJebskFTjQLA,1186
|
24
|
-
sera/models/_parse.py,sha256=
|
26
|
+
sera/models/_parse.py,sha256=sJYfQtwek96ltpgxExG4xUbiLnU3qvNYhTP1CeyXGjs,9746
|
25
27
|
sera/models/_property.py,sha256=CmEmgOShtSyNFq05YW3tGupwCIVRzPMKudXWld8utPk,5530
|
26
|
-
sera/models/_schema.py,sha256=
|
27
|
-
sera/typing.py,sha256=
|
28
|
-
sera_2-1.
|
29
|
-
sera_2-1.
|
30
|
-
sera_2-1.
|
28
|
+
sera/models/_schema.py,sha256=r-Gqg9Lb_wR3UrbNvfXXgt_qs5bts0t2Ve7aquuF_OI,1155
|
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
|