sera-2 1.2.0__py3-none-any.whl → 1.4.2__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/constants.py +4 -0
- sera/libs/base_orm.py +1 -1
- sera/libs/base_service.py +44 -8
- sera/make/__main__.py +14 -1
- sera/make/make_app.py +46 -16
- sera/make/make_python_api.py +63 -44
- sera/make/make_python_model.py +262 -120
- sera/make/make_typescript_model.py +846 -0
- sera/misc/__init__.py +4 -0
- sera/misc/_utils.py +12 -0
- sera/models/__init__.py +3 -1
- sera/models/_class.py +18 -1
- sera/models/_collection.py +10 -2
- sera/models/_datatype.py +150 -45
- sera/models/_module.py +37 -6
- sera/models/_parse.py +69 -18
- sera/models/_property.py +27 -3
- sera/typing.py +3 -0
- {sera_2-1.2.0.dist-info → sera_2-1.4.2.dist-info}/METADATA +3 -3
- sera_2-1.4.2.dist-info/RECORD +30 -0
- {sera_2-1.2.0.dist-info → sera_2-1.4.2.dist-info}/WHEEL +1 -1
- sera/.DS_Store +0 -0
- sera/make/.DS_Store +0 -0
- sera_2-1.2.0.dist-info/RECORD +0 -31
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
|
+
to_camel_case,
|
7
|
+
to_pascal_case,
|
6
8
|
to_snake_case,
|
7
9
|
)
|
8
10
|
|
@@ -13,4 +15,6 @@ __all__ = [
|
|
13
15
|
"assert_isinstance",
|
14
16
|
"filter_duplication",
|
15
17
|
"assert_not_null",
|
18
|
+
"to_snake_case",
|
19
|
+
"to_camel_case",
|
16
20
|
]
|
sera/misc/_utils.py
CHANGED
@@ -13,6 +13,18 @@ def to_snake_case(camelcase: str) -> str:
|
|
13
13
|
return snake.lower()
|
14
14
|
|
15
15
|
|
16
|
+
def to_camel_case(snake: str) -> str:
|
17
|
+
"""Convert snake_case to camelCase."""
|
18
|
+
components = snake.split("_")
|
19
|
+
return components[0] + "".join(x.title() for x in components[1:])
|
20
|
+
|
21
|
+
|
22
|
+
def to_pascal_case(snake: str) -> str:
|
23
|
+
"""Convert snake_case to PascalCase."""
|
24
|
+
components = snake.split("_")
|
25
|
+
return "".join(x.title() for x in components)
|
26
|
+
|
27
|
+
|
16
28
|
def assert_isinstance(x: Any, cls: type[T]) -> T:
|
17
29
|
if not isinstance(x, cls):
|
18
30
|
raise Exception(f"{type(x)} doesn't match with {type(cls)}")
|
sera/models/__init__.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from sera.models._class import Class
|
2
2
|
from sera.models._collection import DataCollection
|
3
|
-
from sera.models._datatype import DataType
|
3
|
+
from sera.models._datatype import DataType, PyTypeWithDep, TsTypeWithDep
|
4
4
|
from sera.models._module import App, Language, Module, Package
|
5
5
|
from sera.models._multi_lingual_string import MultiLingualString
|
6
6
|
from sera.models._parse import parse_schema
|
@@ -22,4 +22,6 @@ __all__ = [
|
|
22
22
|
"Module",
|
23
23
|
"App",
|
24
24
|
"Language",
|
25
|
+
"PyTypeWithDep",
|
26
|
+
"TsTypeWithDep",
|
25
27
|
]
|
sera/models/_class.py
CHANGED
@@ -8,12 +8,20 @@ from sera.models._multi_lingual_string import MultiLingualString
|
|
8
8
|
from sera.models._property import DataProperty, ObjectProperty
|
9
9
|
|
10
10
|
|
11
|
+
@dataclass(kw_only=True)
|
12
|
+
class Index:
|
13
|
+
name: str
|
14
|
+
columns: list[str]
|
15
|
+
unique: bool = False
|
16
|
+
|
17
|
+
|
11
18
|
@dataclass(kw_only=True)
|
12
19
|
class ClassDBMapInfo:
|
13
20
|
"""Represent database information for a class."""
|
14
21
|
|
15
22
|
# name of a corresponding table in the database for this class
|
16
23
|
table_name: str
|
24
|
+
indices: list[Index] = field(default_factory=list)
|
17
25
|
|
18
26
|
|
19
27
|
@dataclass(kw_only=True)
|
@@ -43,16 +51,25 @@ class Class:
|
|
43
51
|
Get the ID property of this class.
|
44
52
|
The ID property is the one tagged with is_primary_key
|
45
53
|
"""
|
46
|
-
assert self.db is not None, "This class is not stored in the database"
|
47
54
|
for prop in self.properties.values():
|
48
55
|
if (
|
49
56
|
isinstance(prop, DataProperty)
|
50
57
|
and prop.db is not None
|
51
58
|
and prop.db.is_primary_key
|
52
59
|
):
|
60
|
+
assert (
|
61
|
+
self.db is not None
|
62
|
+
), "This class is not stored in the database and thus, cannot have a primary key"
|
53
63
|
return prop
|
64
|
+
assert (
|
65
|
+
self.db is None
|
66
|
+
), "This class is stored in the database and thus, must have a primary key"
|
54
67
|
return None
|
55
68
|
|
56
69
|
def get_pymodule_name(self) -> str:
|
57
70
|
"""Get the python module name of this class as if there is a python module created to store this class only."""
|
58
71
|
return to_snake_case(self.name)
|
72
|
+
|
73
|
+
def get_tsmodule_name(self) -> str:
|
74
|
+
"""Get the typescript module name of this class as if there is a typescript module created to store this class only."""
|
75
|
+
return self.name[0].lower() + self.name[1:]
|
sera/models/_collection.py
CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
|
5
5
|
from sera.models._class import Class
|
6
|
+
from sera.models._property import DataProperty
|
6
7
|
|
7
8
|
|
8
9
|
@dataclass
|
@@ -24,8 +25,15 @@ class DataCollection:
|
|
24
25
|
"""Get the fields of this collection that can be used in a queries."""
|
25
26
|
field_names = set()
|
26
27
|
for prop in self.cls.properties.values():
|
27
|
-
if prop.db is None:
|
28
|
-
# This property is not stored in the database, so we skip it
|
28
|
+
if prop.db is None or prop.data.is_private:
|
29
|
+
# This property is not stored in the database or it's private, so we skip it
|
30
|
+
continue
|
31
|
+
if (
|
32
|
+
isinstance(prop, DataProperty)
|
33
|
+
and prop.db is not None
|
34
|
+
and not prop.db.is_indexed
|
35
|
+
):
|
36
|
+
# This property is not indexed, so we skip it
|
29
37
|
continue
|
30
38
|
field_names.add(prop.name)
|
31
39
|
return field_names
|
sera/models/_datatype.py
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import datetime
|
4
|
-
from dataclasses import dataclass
|
4
|
+
from dataclasses import dataclass, field
|
5
5
|
from enum import Enum
|
6
6
|
from typing import Literal
|
7
7
|
|
8
|
+
from codegen.models import expr
|
9
|
+
|
8
10
|
PyDataType = Literal["str", "int", "datetime", "float", "bool", "bytes", "dict"]
|
9
11
|
TypescriptDataType = Literal["string", "number", "boolean"]
|
10
12
|
SQLAlchemyDataType = Literal[
|
@@ -20,73 +22,176 @@ SQLAlchemyDataType = Literal[
|
|
20
22
|
|
21
23
|
|
22
24
|
@dataclass
|
23
|
-
class
|
25
|
+
class PyTypeWithDep:
|
24
26
|
type: str
|
25
27
|
dep: str | None = None
|
26
28
|
|
27
|
-
|
28
|
-
@dataclass
|
29
|
-
class PyTypeWithDep(TypeWithDep):
|
30
|
-
|
31
29
|
def get_python_type(self) -> type:
|
32
30
|
"""Get the Python type from the type string for typing annotation in Python."""
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
31
|
+
type = {
|
32
|
+
"str": str,
|
33
|
+
"int": int,
|
34
|
+
"float": float,
|
35
|
+
"bool": bool,
|
36
|
+
"bytes": bytes,
|
37
|
+
"dict": dict,
|
38
|
+
"datetime": datetime.datetime,
|
39
|
+
"list[str]": list[str],
|
40
|
+
"list[int]": list[int],
|
41
|
+
"list[float]": list[float],
|
42
|
+
"list[bool]": list[bool],
|
43
|
+
"list[bytes]": list[bytes],
|
44
|
+
"list[dict]": list[dict],
|
45
|
+
"list[datetime]": list[datetime.datetime],
|
46
|
+
}.get(self.type, None)
|
47
|
+
if type is None:
|
48
48
|
raise ValueError(f"Unknown type: {self.type}")
|
49
|
+
return type
|
50
|
+
|
51
|
+
def as_list_type(self) -> PyTypeWithDep:
|
52
|
+
"""Convert the type to a list type."""
|
53
|
+
return PyTypeWithDep(type=f"list[{self.type}]", dep=self.dep)
|
54
|
+
|
55
|
+
|
56
|
+
@dataclass
|
57
|
+
class TsTypeWithDep:
|
58
|
+
type: str
|
59
|
+
dep: str | None = None
|
60
|
+
|
61
|
+
def get_default(self) -> expr.ExprConstant:
|
62
|
+
if self.type.endswith("[]"):
|
63
|
+
return expr.ExprConstant([])
|
64
|
+
if self.type == "string":
|
65
|
+
return expr.ExprConstant("")
|
66
|
+
if self.type == "number":
|
67
|
+
return expr.ExprConstant(0)
|
68
|
+
if self.type == "boolean":
|
69
|
+
return expr.ExprConstant(False)
|
70
|
+
if self.type == "string | undefined":
|
71
|
+
return expr.ExprConstant("undefined")
|
72
|
+
raise ValueError(f"Unknown type: {self.type}")
|
73
|
+
|
74
|
+
def as_list_type(self) -> TsTypeWithDep:
|
75
|
+
return TsTypeWithDep(type=f"{self.type}[]", dep=self.dep)
|
76
|
+
|
77
|
+
|
78
|
+
@dataclass
|
79
|
+
class SQLTypeWithDep:
|
80
|
+
type: str
|
81
|
+
mapped_pytype: str
|
82
|
+
deps: list[str] = field(default_factory=list)
|
83
|
+
|
84
|
+
def as_list_type(self) -> SQLTypeWithDep:
|
85
|
+
"""Convert the type to a list type."""
|
86
|
+
return SQLTypeWithDep(
|
87
|
+
type=f"ARRAY({self.type})",
|
88
|
+
deps=self.deps + ["sqlalchemy.ARRAY"],
|
89
|
+
mapped_pytype=f"list[{self.mapped_pytype}]",
|
90
|
+
)
|
49
91
|
|
50
92
|
|
51
93
|
@dataclass
|
52
94
|
class DataType:
|
53
|
-
pytype:
|
54
|
-
sqltype:
|
55
|
-
tstype:
|
95
|
+
pytype: PyTypeWithDep
|
96
|
+
sqltype: SQLTypeWithDep
|
97
|
+
tstype: TsTypeWithDep
|
56
98
|
|
57
99
|
is_list: bool = False
|
58
100
|
|
59
|
-
def get_python_type(self) ->
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
raise NotImplementedError(self.pytype)
|
101
|
+
def get_python_type(self) -> PyTypeWithDep:
|
102
|
+
pytype = self.pytype
|
103
|
+
if self.is_list:
|
104
|
+
return pytype.as_list_type()
|
105
|
+
return pytype
|
65
106
|
|
66
|
-
def get_sqlalchemy_type(self) ->
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
107
|
+
def get_sqlalchemy_type(self) -> SQLTypeWithDep:
|
108
|
+
sqltype = self.sqltype
|
109
|
+
if self.is_list:
|
110
|
+
return sqltype.as_list_type()
|
111
|
+
return sqltype
|
112
|
+
|
113
|
+
def get_typescript_type(self) -> TsTypeWithDep:
|
114
|
+
tstype = self.tstype
|
115
|
+
if self.is_list:
|
116
|
+
return tstype.as_list_type()
|
117
|
+
return tstype
|
74
118
|
|
75
119
|
|
76
120
|
predefined_datatypes = {
|
77
|
-
"string": DataType(
|
121
|
+
"string": DataType(
|
122
|
+
pytype=PyTypeWithDep(type="str"),
|
123
|
+
sqltype=SQLTypeWithDep(
|
124
|
+
type="String", mapped_pytype="str", deps=["sqlalchemy.String"]
|
125
|
+
),
|
126
|
+
tstype=TsTypeWithDep(type="string"),
|
127
|
+
is_list=False,
|
128
|
+
),
|
129
|
+
"optional[string]": DataType(
|
130
|
+
pytype=PyTypeWithDep(type="Optional[str]", dep="typing.Optional"),
|
131
|
+
sqltype=SQLTypeWithDep(
|
132
|
+
type="String",
|
133
|
+
mapped_pytype="Optional[str]",
|
134
|
+
deps=["sqlalchemy.String", "typing.Optional"],
|
135
|
+
),
|
136
|
+
tstype=TsTypeWithDep(type="string | undefined"),
|
137
|
+
is_list=False,
|
138
|
+
),
|
78
139
|
"integer": DataType(
|
79
|
-
pytype="int",
|
140
|
+
pytype=PyTypeWithDep(type="int"),
|
141
|
+
sqltype=SQLTypeWithDep(
|
142
|
+
type="Integer", mapped_pytype="int", deps=["sqlalchemy.Integer"]
|
143
|
+
),
|
144
|
+
tstype=TsTypeWithDep(type="number"),
|
145
|
+
is_list=False,
|
80
146
|
),
|
81
147
|
"datetime": DataType(
|
82
|
-
pytype="datetime",
|
148
|
+
pytype=PyTypeWithDep(type="datetime", dep="datetime.datetime"),
|
149
|
+
sqltype=SQLTypeWithDep(
|
150
|
+
type="DateTime", mapped_pytype="datetime", deps=["sqlalchemy.DateTime"]
|
151
|
+
),
|
152
|
+
tstype=TsTypeWithDep(type="string"),
|
153
|
+
is_list=False,
|
154
|
+
),
|
155
|
+
"float": DataType(
|
156
|
+
pytype=PyTypeWithDep(type="float"),
|
157
|
+
sqltype=SQLTypeWithDep(
|
158
|
+
type="Float", mapped_pytype="float", deps=["sqlalchemy.Float"]
|
159
|
+
),
|
160
|
+
tstype=TsTypeWithDep(type="number"),
|
161
|
+
is_list=False,
|
83
162
|
),
|
84
|
-
"float": DataType(pytype="float", sqltype="Float", tstype="number", is_list=False),
|
85
163
|
"boolean": DataType(
|
86
|
-
pytype="bool",
|
164
|
+
pytype=PyTypeWithDep(type="bool"),
|
165
|
+
sqltype=SQLTypeWithDep(
|
166
|
+
type="Boolean", mapped_pytype="bool", deps=["sqlalchemy.Boolean"]
|
167
|
+
),
|
168
|
+
tstype=TsTypeWithDep(type="boolean"),
|
169
|
+
is_list=False,
|
87
170
|
),
|
88
171
|
"bytes": DataType(
|
89
|
-
pytype="bytes",
|
172
|
+
pytype=PyTypeWithDep(type="bytes"),
|
173
|
+
sqltype=SQLTypeWithDep(
|
174
|
+
type="LargeBinary", mapped_pytype="bytes", deps=["sqlalchemy.LargeBinary"]
|
175
|
+
),
|
176
|
+
tstype=TsTypeWithDep(type="string"),
|
177
|
+
is_list=False,
|
178
|
+
),
|
179
|
+
"dict": DataType(
|
180
|
+
pytype=PyTypeWithDep(type="dict"),
|
181
|
+
sqltype=SQLTypeWithDep(
|
182
|
+
type="JSON", mapped_pytype="dict", deps=["sqlalchemy.JSON"]
|
183
|
+
),
|
184
|
+
tstype=TsTypeWithDep(type="string"),
|
185
|
+
is_list=False,
|
186
|
+
),
|
187
|
+
}
|
188
|
+
|
189
|
+
predefined_py_datatypes = {"bytes": PyTypeWithDep(type="bytes")}
|
190
|
+
predefined_sql_datatypes = {
|
191
|
+
"bit": SQLTypeWithDep(
|
192
|
+
type="BIT", mapped_pytype="bytes", deps=["sqlalchemy.dialects.postgresql.BIT"]
|
90
193
|
),
|
91
|
-
|
194
|
+
}
|
195
|
+
predefined_ts_datatypes = {
|
196
|
+
"string": TsTypeWithDep(type="string"),
|
92
197
|
}
|
sera/models/_module.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import
|
3
|
+
import subprocess
|
4
4
|
from dataclasses import dataclass
|
5
5
|
from enum import Enum
|
6
6
|
from pathlib import Path
|
@@ -75,22 +75,53 @@ class Module:
|
|
75
75
|
raise
|
76
76
|
else:
|
77
77
|
assert self.language == Language.Typescript
|
78
|
-
|
78
|
+
try:
|
79
|
+
code = program.root.to_typescript()
|
80
|
+
except:
|
81
|
+
logger.error("Error writing module {}", self.path)
|
82
|
+
print(">>> Program")
|
83
|
+
print(program.root.to_typescript())
|
84
|
+
print("<<<")
|
85
|
+
raise
|
86
|
+
|
87
|
+
if self.language == Language.Python:
|
88
|
+
outfile = self.package.dir / f"{self.name}.py"
|
89
|
+
copyright_statement = f"# Generated by SERA. All rights reserved.\n\n"
|
90
|
+
else:
|
91
|
+
assert self.language == Language.Typescript
|
92
|
+
outfile = self.package.dir / f"{self.name}.ts"
|
93
|
+
copyright_statement = f"/// Generated by SERA. All rights reserved.\n\n"
|
79
94
|
|
80
|
-
outfile = self.package.dir / f"{self.name}.py"
|
81
95
|
if outfile.exists():
|
82
|
-
|
96
|
+
outfile_content = outfile.read_text()
|
97
|
+
if outfile_content.startswith("# sera:skip") or outfile_content.startswith(
|
98
|
+
"/// sera:skip"
|
99
|
+
):
|
83
100
|
logger.info(
|
84
101
|
"`{}` already exists and is in manual edit mode. Skip updating it.",
|
85
102
|
outfile,
|
86
103
|
)
|
87
104
|
return
|
88
|
-
|
105
|
+
|
89
106
|
outfile.write_text(copyright_statement + code)
|
90
107
|
|
108
|
+
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
|
118
|
+
|
91
119
|
def exists(self) -> bool:
|
92
120
|
"""Check if the module exists"""
|
93
|
-
|
121
|
+
if self.language == Language.Python:
|
122
|
+
return (self.package.dir / f"{self.name}.py").exists()
|
123
|
+
else:
|
124
|
+
return (self.package.dir / f"{self.name}.ts").exists()
|
94
125
|
|
95
126
|
|
96
127
|
@dataclass
|
sera/models/_parse.py
CHANGED
@@ -1,12 +1,19 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import re
|
3
4
|
from copy import deepcopy
|
4
5
|
from pathlib import Path
|
5
6
|
from typing import Sequence
|
6
7
|
|
7
8
|
import serde.yaml
|
8
|
-
from sera.models._class import Class, ClassDBMapInfo
|
9
|
-
from sera.models._datatype import
|
9
|
+
from sera.models._class import Class, ClassDBMapInfo, Index
|
10
|
+
from sera.models._datatype import (
|
11
|
+
DataType,
|
12
|
+
predefined_datatypes,
|
13
|
+
predefined_py_datatypes,
|
14
|
+
predefined_sql_datatypes,
|
15
|
+
predefined_ts_datatypes,
|
16
|
+
)
|
10
17
|
from sera.models._multi_lingual_string import MultiLingualString
|
11
18
|
from sera.models._property import (
|
12
19
|
Cardinality,
|
@@ -16,6 +23,7 @@ from sera.models._property import (
|
|
16
23
|
ForeignKeyOnUpdate,
|
17
24
|
ObjectPropDBInfo,
|
18
25
|
ObjectProperty,
|
26
|
+
PropDataAttrs,
|
19
27
|
)
|
20
28
|
from sera.models._schema import Schema
|
21
29
|
|
@@ -46,7 +54,16 @@ def parse_schema(files: Sequence[Path | str]) -> Schema:
|
|
46
54
|
def _parse_class_without_prop(schema: Schema, clsname: str, cls: dict) -> Class:
|
47
55
|
db = None
|
48
56
|
if "db" in cls:
|
49
|
-
|
57
|
+
indices = []
|
58
|
+
for idx in cls["db"].get("indices", []):
|
59
|
+
index = Index(
|
60
|
+
name=idx.get("name", "_".join(idx["columns"]) + "_index"),
|
61
|
+
columns=idx["columns"],
|
62
|
+
unique=idx.get("unique", False),
|
63
|
+
)
|
64
|
+
indices.append(index)
|
65
|
+
db = ClassDBMapInfo(table_name=cls["db"]["table_name"], indices=indices)
|
66
|
+
|
50
67
|
return Class(
|
51
68
|
name=clsname,
|
52
69
|
label=_parse_multi_lingual_string(cls["label"]),
|
@@ -68,7 +85,6 @@ def _parse_property(
|
|
68
85
|
description=_parse_multi_lingual_string(""),
|
69
86
|
target=schema.classes[datatype],
|
70
87
|
cardinality=Cardinality.ONE_TO_ONE,
|
71
|
-
is_private=False,
|
72
88
|
)
|
73
89
|
else:
|
74
90
|
return DataProperty(
|
@@ -76,10 +92,14 @@ def _parse_property(
|
|
76
92
|
label=_parse_multi_lingual_string(prop_name),
|
77
93
|
description=_parse_multi_lingual_string(""),
|
78
94
|
datatype=_parse_datatype(datatype),
|
79
|
-
is_private=False,
|
80
95
|
)
|
81
96
|
|
82
97
|
db = prop.get("db", {})
|
98
|
+
_data = prop.get("data", {})
|
99
|
+
data_attrs = PropDataAttrs(
|
100
|
+
is_private=_data.get("is_private", False),
|
101
|
+
datatype=_parse_datatype(_data["datatype"]) if "datatype" in _data else None,
|
102
|
+
)
|
83
103
|
|
84
104
|
assert isinstance(prop, dict), prop
|
85
105
|
if "datatype" in prop:
|
@@ -88,12 +108,16 @@ def _parse_property(
|
|
88
108
|
label=_parse_multi_lingual_string(prop.get("label", prop_name)),
|
89
109
|
description=_parse_multi_lingual_string(prop.get("desc", "")),
|
90
110
|
datatype=_parse_datatype(prop["datatype"]),
|
91
|
-
|
111
|
+
data=data_attrs,
|
92
112
|
db=(
|
93
113
|
DataPropDBInfo(
|
94
114
|
is_primary_key=db.get("is_primary_key", False),
|
95
115
|
is_auto_increment=db.get("is_auto_increment", False),
|
96
116
|
is_unique=db.get("is_unique", False),
|
117
|
+
is_indexed=db.get("is_indexed", False)
|
118
|
+
or db.get("is_unique", False)
|
119
|
+
or db.get("is_primary_key", False),
|
120
|
+
is_nullable=db.get("is_nullable", False),
|
97
121
|
)
|
98
122
|
if "db" in prop
|
99
123
|
else None
|
@@ -108,7 +132,7 @@ def _parse_property(
|
|
108
132
|
target=schema.classes[prop["target"]],
|
109
133
|
cardinality=Cardinality(prop.get("cardinality", "1:1")),
|
110
134
|
is_optional=prop.get("is_optional", False),
|
111
|
-
|
135
|
+
data=data_attrs,
|
112
136
|
db=(
|
113
137
|
ObjectPropDBInfo(
|
114
138
|
is_embedded=db.get("is_embedded", None),
|
@@ -139,16 +163,43 @@ def _parse_multi_lingual_string(o: dict | str) -> MultiLingualString:
|
|
139
163
|
return MultiLingualString(lang2value=o, lang="en")
|
140
164
|
|
141
165
|
|
142
|
-
def _parse_datatype(datatype: str) -> DataType:
|
143
|
-
if datatype
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
166
|
+
def _parse_datatype(datatype: dict | str) -> DataType:
|
167
|
+
if isinstance(datatype, str):
|
168
|
+
if datatype.endswith("[]"):
|
169
|
+
datatype = datatype[:-2]
|
170
|
+
is_list = True
|
171
|
+
else:
|
172
|
+
is_list = False
|
173
|
+
|
174
|
+
if datatype not in predefined_datatypes:
|
175
|
+
raise NotImplementedError(datatype)
|
176
|
+
|
177
|
+
dt = deepcopy(predefined_datatypes[datatype])
|
178
|
+
dt.is_list = is_list
|
179
|
+
return dt
|
180
|
+
if isinstance(datatype, dict):
|
181
|
+
is_list = datatype.get("is_list", False)
|
182
|
+
|
183
|
+
# Parse SQL type and argument if present
|
184
|
+
m = re.match(r"^([a-zA-Z0-9_]+)(\([^)]+\))?$", datatype["sqltype"])
|
185
|
+
if m is not None:
|
186
|
+
sql_type_name = m.group(1)
|
187
|
+
sql_type_arg = m.group(2)
|
188
|
+
# Use the extracted type to get the predefined SQL type
|
189
|
+
if sql_type_name not in predefined_sql_datatypes:
|
190
|
+
raise NotImplementedError(sql_type_name)
|
191
|
+
sql_type = predefined_sql_datatypes[sql_type_name]
|
192
|
+
if sql_type_arg is not None:
|
193
|
+
# process the argument
|
194
|
+
sql_type.type = sql_type.type + sql_type_arg
|
195
|
+
else:
|
196
|
+
raise ValueError(f"Invalid SQL type format: {datatype['sqltype']}")
|
148
197
|
|
149
|
-
|
150
|
-
|
198
|
+
return DataType(
|
199
|
+
pytype=predefined_py_datatypes[datatype["pytype"]],
|
200
|
+
sqltype=sql_type,
|
201
|
+
tstype=predefined_ts_datatypes[datatype["tstype"]],
|
202
|
+
is_list=is_list,
|
203
|
+
)
|
151
204
|
|
152
|
-
|
153
|
-
dt.is_list = is_list
|
154
|
-
return dt
|
205
|
+
raise NotImplementedError(datatype)
|
sera/models/_property.py
CHANGED
@@ -54,6 +54,19 @@ class Cardinality(str, Enum):
|
|
54
54
|
]
|
55
55
|
|
56
56
|
|
57
|
+
@dataclass(kw_only=True)
|
58
|
+
class PropDataAttrs:
|
59
|
+
"""Storing other attributes for generating data model (upsert & public) -- this is different from a db model"""
|
60
|
+
|
61
|
+
# whether this property is private and cannot be accessed by the end users
|
62
|
+
# meaning the public data model will not include this property
|
63
|
+
# default it is false
|
64
|
+
is_private: bool = False
|
65
|
+
|
66
|
+
# whether this data model has a different data type than the one from the database
|
67
|
+
datatype: Optional[DataType] = None
|
68
|
+
|
69
|
+
|
57
70
|
@dataclass(kw_only=True)
|
58
71
|
class Property:
|
59
72
|
"""Represent a property of a class."""
|
@@ -68,9 +81,8 @@ class Property:
|
|
68
81
|
label: MultiLingualString
|
69
82
|
# human-readable description of the property
|
70
83
|
description: MultiLingualString
|
71
|
-
#
|
72
|
-
|
73
|
-
is_private: bool = field(default=False)
|
84
|
+
# other attributes for generating data model such as upsert and return.
|
85
|
+
data: PropDataAttrs = field(default_factory=PropDataAttrs)
|
74
86
|
|
75
87
|
|
76
88
|
@dataclass(kw_only=True)
|
@@ -83,6 +95,10 @@ class DataPropDBInfo:
|
|
83
95
|
is_auto_increment: bool = False
|
84
96
|
# whether this property contains unique values
|
85
97
|
is_unique: bool = False
|
98
|
+
# whether this property is indexed or not
|
99
|
+
is_indexed: bool = False
|
100
|
+
# whether this property is nullable or not
|
101
|
+
is_nullable: bool = False
|
86
102
|
|
87
103
|
|
88
104
|
@dataclass(kw_only=True)
|
@@ -92,6 +108,14 @@ class DataProperty(Property):
|
|
92
108
|
# other database properties of this property
|
93
109
|
db: Optional[DataPropDBInfo] = None
|
94
110
|
|
111
|
+
def get_data_model_datatype(self) -> DataType:
|
112
|
+
if self.data.datatype is not None:
|
113
|
+
return self.data.datatype
|
114
|
+
return self.datatype
|
115
|
+
|
116
|
+
def is_diff_data_model_datatype(self):
|
117
|
+
return self.data.datatype is not None
|
118
|
+
|
95
119
|
|
96
120
|
@dataclass(kw_only=True)
|
97
121
|
class ObjectPropDBInfo:
|
sera/typing.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: sera-2
|
3
|
-
Version: 1.2
|
3
|
+
Version: 1.4.2
|
4
4
|
Summary:
|
5
5
|
Author: Binh Vu
|
6
6
|
Author-email: bvu687@gmail.com
|
@@ -8,10 +8,10 @@ Requires-Python: >=3.12,<4.0
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
9
|
Classifier: Programming Language :: Python :: 3.12
|
10
10
|
Classifier: Programming Language :: Python :: 3.13
|
11
|
-
Requires-Dist: black (>=25.0.1,<26.0.0)
|
12
11
|
Requires-Dist: codegen-2 (>=2.1.4,<3.0.0)
|
13
12
|
Requires-Dist: litestar (>=2.15.1,<3.0.0)
|
14
13
|
Requires-Dist: msgspec (>=0.19.0,<0.20.0)
|
14
|
+
Project-URL: Repository, https://github.com/binh-vu/sera
|
15
15
|
Description-Content-Type: text/markdown
|
16
16
|
|
17
17
|
# Overview
|