sera-2 1.19.2__py3-none-any.whl → 1.20.4__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/libs/api_helper.py +41 -4
- sera/libs/base_service.py +41 -15
- sera/make/make_python_api.py +12 -1
- sera/make/make_python_model.py +7 -1
- sera/make/make_typescript_model.py +93 -45
- sera/models/_collection.py +17 -4
- sera/models/_datatype.py +29 -0
- sera/models/_parse.py +6 -1
- {sera_2-1.19.2.dist-info → sera_2-1.20.4.dist-info}/METADATA +2 -2
- {sera_2-1.19.2.dist-info → sera_2-1.20.4.dist-info}/RECORD +11 -11
- {sera_2-1.19.2.dist-info → sera_2-1.20.4.dist-info}/WHEEL +0 -0
sera/libs/api_helper.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import re
|
4
|
-
from typing import Collection, Generic, cast
|
4
|
+
from typing import Callable, Collection, Generic, TypeVar, cast
|
5
5
|
|
6
6
|
from litestar import Request, status_codes
|
7
7
|
from litestar.connection import ASGIConnection
|
@@ -19,12 +19,30 @@ from sera.libs.middlewares.uscp import STATE_SYSTEM_CONTROLLED_PROP_KEY
|
|
19
19
|
from sera.typing import T
|
20
20
|
|
21
21
|
# for parsing field names and operations from query string
|
22
|
-
FIELD_REG = re.compile(r"(?P<name>[a-zA-Z_0-9]+)(?:\[(?P<op>[a-zA-
|
22
|
+
FIELD_REG = re.compile(r"(?P<name>[a-zA-Z_0-9]+)(?:\[(?P<op>[a-zA-Z_0-9]+)\])?")
|
23
23
|
QUERY_OPS = {op.value for op in QueryOp}
|
24
24
|
KEYWORDS = {"field", "limit", "offset", "unique", "sorted_by", "group_by"}
|
25
25
|
|
26
26
|
|
27
|
-
|
27
|
+
class TypeConversion:
|
28
|
+
|
29
|
+
to_int = int
|
30
|
+
to_float = float
|
31
|
+
|
32
|
+
@staticmethod
|
33
|
+
def to_bool(value: str) -> bool:
|
34
|
+
if value == "1":
|
35
|
+
return True
|
36
|
+
elif value == "0":
|
37
|
+
return False
|
38
|
+
raise ValueError(f"Invalid boolean value: {value}")
|
39
|
+
|
40
|
+
|
41
|
+
def parse_query(
|
42
|
+
request: Request,
|
43
|
+
fields: dict[str, Callable[[str], str | int | bool | float]],
|
44
|
+
debug: bool,
|
45
|
+
) -> Query:
|
28
46
|
"""Parse query for retrieving records that match a query.
|
29
47
|
|
30
48
|
If a field name collides with a keyword, you can add `_` to the field name.
|
@@ -56,6 +74,7 @@ def parse_query(request: Request, fields: set[str], debug: bool) -> Query:
|
|
56
74
|
continue
|
57
75
|
|
58
76
|
# Process based on operation or default to equality check
|
77
|
+
# TODO: validate if the operation is allowed for the field
|
59
78
|
if not operation:
|
60
79
|
operation = QueryOp.eq
|
61
80
|
else:
|
@@ -65,6 +84,21 @@ def parse_query(request: Request, fields: set[str], debug: bool) -> Query:
|
|
65
84
|
detail=f"Invalid operation: {operation}",
|
66
85
|
)
|
67
86
|
operation = QueryOp(operation)
|
87
|
+
|
88
|
+
try:
|
89
|
+
norm_func = fields[field_name]
|
90
|
+
if isinstance(v, list):
|
91
|
+
v = [norm_func(x) for x in v]
|
92
|
+
else:
|
93
|
+
v = norm_func(v)
|
94
|
+
except ValueError:
|
95
|
+
if debug:
|
96
|
+
raise HTTPException(
|
97
|
+
status_code=status_codes.HTTP_400_BAD_REQUEST,
|
98
|
+
detail=f"Invalid value for field {field_name}: {v}",
|
99
|
+
)
|
100
|
+
continue
|
101
|
+
|
68
102
|
query[field_name] = {operation: v}
|
69
103
|
else:
|
70
104
|
# Invalid field name format
|
@@ -78,7 +112,10 @@ def parse_query(request: Request, fields: set[str], debug: bool) -> Query:
|
|
78
112
|
return query
|
79
113
|
|
80
114
|
|
81
|
-
|
115
|
+
S = TypeVar("S", bound=Struct)
|
116
|
+
|
117
|
+
|
118
|
+
class SingleAutoUSCP(MsgspecDTO[S], Generic[S]):
|
82
119
|
"""Auto Update System Controlled Property DTO"""
|
83
120
|
|
84
121
|
@classmethod
|
sera/libs/base_service.py
CHANGED
@@ -17,15 +17,15 @@ from sera.typing import FieldName, T, doc
|
|
17
17
|
|
18
18
|
|
19
19
|
class QueryOp(str, Enum):
|
20
|
-
lt = "
|
21
|
-
lte = "
|
22
|
-
gt = "
|
23
|
-
gte = "
|
24
|
-
eq = "
|
25
|
-
ne = "
|
20
|
+
lt = "lt"
|
21
|
+
lte = "lte"
|
22
|
+
gt = "gt"
|
23
|
+
gte = "gte"
|
24
|
+
eq = "eq"
|
25
|
+
ne = "ne"
|
26
26
|
# select records where values are in the given list
|
27
27
|
in_ = "in"
|
28
|
-
not_in = "
|
28
|
+
not_in = "not_in"
|
29
29
|
# for full text search
|
30
30
|
fuzzy = "fuzzy"
|
31
31
|
|
@@ -94,15 +94,41 @@ class BaseAsyncService(Generic[ID, R]):
|
|
94
94
|
)
|
95
95
|
if unique:
|
96
96
|
q = q.distinct()
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
97
|
+
for field in sorted_by:
|
98
|
+
if field.startswith("-"):
|
99
|
+
q = q.order_by(getattr(self.orm_cls, field[1:]).desc())
|
100
|
+
else:
|
101
|
+
q = q.order_by(getattr(self.orm_cls, field))
|
102
|
+
for field in group_by:
|
103
|
+
q = q.group_by(getattr(self.orm_cls, field))
|
104
|
+
|
105
|
+
for field, conditions in query.items():
|
106
|
+
for op, value in conditions.items():
|
107
|
+
# TODO: check if the operation is valid for the field.
|
108
|
+
if op == QueryOp.eq:
|
109
|
+
q = q.where(getattr(self.orm_cls, field) == value)
|
110
|
+
elif op == QueryOp.ne:
|
111
|
+
q = q.where(getattr(self.orm_cls, field) != value)
|
112
|
+
elif op == QueryOp.lt:
|
113
|
+
q = q.where(getattr(self.orm_cls, field) < value)
|
114
|
+
elif op == QueryOp.lte:
|
115
|
+
q = q.where(getattr(self.orm_cls, field) <= value)
|
116
|
+
elif op == QueryOp.gt:
|
117
|
+
q = q.where(getattr(self.orm_cls, field) > value)
|
118
|
+
elif op == QueryOp.gte:
|
119
|
+
q = q.where(getattr(self.orm_cls, field) >= value)
|
120
|
+
elif op == QueryOp.in_:
|
121
|
+
q = q.where(getattr(self.orm_cls, field).in_(value))
|
122
|
+
elif op == QueryOp.not_in:
|
123
|
+
q = q.where(~getattr(self.orm_cls, field).in_(value))
|
101
124
|
else:
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
125
|
+
assert op == QueryOp.fuzzy
|
126
|
+
# Assuming fuzzy search is implemented as a full-text search
|
127
|
+
q = q.where(
|
128
|
+
func.to_tsvector(getattr(self.orm_cls, field)).match(value)
|
129
|
+
)
|
130
|
+
|
131
|
+
print(">>>", q)
|
106
132
|
|
107
133
|
cq = select(func.count()).select_from(q.subquery())
|
108
134
|
rq = q.limit(limit).offset(offset)
|
sera/make/make_python_api.py
CHANGED
@@ -4,6 +4,7 @@ from typing import Sequence
|
|
4
4
|
|
5
5
|
from codegen.models import DeferredVar, ImportHelper, PredefinedFn, Program, expr, stmt
|
6
6
|
from loguru import logger
|
7
|
+
from msgspec import convert
|
7
8
|
|
8
9
|
from sera.misc import assert_not_null, to_snake_case
|
9
10
|
from sera.models import App, DataCollection, Module, Package
|
@@ -134,11 +135,21 @@ def make_python_get_api(
|
|
134
135
|
|
135
136
|
func_name = "get_"
|
136
137
|
|
138
|
+
queryable_fields = []
|
139
|
+
for propname, (
|
140
|
+
convert_func,
|
141
|
+
convert_func_import,
|
142
|
+
) in collection.get_queryable_fields():
|
143
|
+
program.import_(convert_func_import, True)
|
144
|
+
queryable_fields.append(
|
145
|
+
(expr.ExprConstant(propname), expr.ExprIdent(convert_func))
|
146
|
+
)
|
147
|
+
|
137
148
|
program.root(
|
138
149
|
stmt.LineBreak(),
|
139
150
|
lambda ast00: ast00.assign(
|
140
151
|
DeferredVar.simple("QUERYABLE_FIELDS"),
|
141
|
-
|
152
|
+
PredefinedFn.dict(queryable_fields),
|
142
153
|
),
|
143
154
|
stmt.PythonDecoratorStatement(
|
144
155
|
expr.ExprFuncCall(
|
sera/make/make_python_model.py
CHANGED
@@ -749,6 +749,7 @@ def make_python_data_model(
|
|
749
749
|
# skip private fields as this is for APIs exchange
|
750
750
|
continue
|
751
751
|
|
752
|
+
propname = prop.name
|
752
753
|
if isinstance(prop, DataProperty):
|
753
754
|
pytype = prop.get_data_model_datatype().get_python_type()
|
754
755
|
if prop.is_optional:
|
@@ -760,6 +761,7 @@ def make_python_data_model(
|
|
760
761
|
cls_ast(stmt.DefClassVarStatement(prop.name, pytype.type))
|
761
762
|
elif isinstance(prop, ObjectProperty):
|
762
763
|
if prop.target.db is not None:
|
764
|
+
propname = prop.name + "_id"
|
763
765
|
pytype = (
|
764
766
|
assert_not_null(prop.target.get_id_property())
|
765
767
|
.get_data_model_datatype()
|
@@ -781,7 +783,7 @@ def make_python_data_model(
|
|
781
783
|
for dep in pytype.deps:
|
782
784
|
program.import_(dep, True)
|
783
785
|
|
784
|
-
cls_ast(stmt.DefClassVarStatement(
|
786
|
+
cls_ast(stmt.DefClassVarStatement(propname, pytype.type))
|
785
787
|
|
786
788
|
cls_ast(
|
787
789
|
stmt.LineBreak(),
|
@@ -1294,6 +1296,10 @@ def make_python_relational_object_property(
|
|
1294
1296
|
idprop = prop.target.get_id_property()
|
1295
1297
|
assert idprop is not None
|
1296
1298
|
idprop_pytype = idprop.datatype.get_sqlalchemy_type()
|
1299
|
+
|
1300
|
+
if prop.is_optional:
|
1301
|
+
idprop_pytype = idprop_pytype.as_optional_type()
|
1302
|
+
|
1297
1303
|
for dep in idprop_pytype.deps:
|
1298
1304
|
program.import_(dep, True)
|
1299
1305
|
|
@@ -50,6 +50,66 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
50
50
|
# the original object, then it's okay.
|
51
51
|
return value
|
52
52
|
|
53
|
+
def get_normal_deser_args(
|
54
|
+
prop: DataProperty | ObjectProperty,
|
55
|
+
) -> expr.Expr:
|
56
|
+
"""Extract the value from the data record from the server response to set to the class property in the client."""
|
57
|
+
handle_optional = lambda value: expr.ExprTernary(
|
58
|
+
expr.ExprNotEqual(value, expr.ExprConstant(None)),
|
59
|
+
value,
|
60
|
+
expr.ExprConstant("undefined"),
|
61
|
+
)
|
62
|
+
|
63
|
+
if isinstance(prop, DataProperty):
|
64
|
+
value = PredefinedFn.attr_getter(
|
65
|
+
expr.ExprIdent("data"), expr.ExprIdent(prop.name)
|
66
|
+
)
|
67
|
+
if prop.is_optional:
|
68
|
+
value = handle_optional(value)
|
69
|
+
return value
|
70
|
+
|
71
|
+
assert isinstance(prop, ObjectProperty)
|
72
|
+
if prop.target.db is not None:
|
73
|
+
value = PredefinedFn.attr_getter(
|
74
|
+
expr.ExprIdent("data"), expr.ExprIdent(prop.name + "_id")
|
75
|
+
)
|
76
|
+
if prop.is_optional:
|
77
|
+
value = handle_optional(value)
|
78
|
+
return value
|
79
|
+
else:
|
80
|
+
if prop.cardinality.is_star_to_many():
|
81
|
+
# optional type for a list is simply an empty list, we don't need to check for None
|
82
|
+
value = PredefinedFn.map_list(
|
83
|
+
PredefinedFn.attr_getter(
|
84
|
+
expr.ExprIdent("data"),
|
85
|
+
expr.ExprIdent(prop.name),
|
86
|
+
),
|
87
|
+
lambda item: expr.ExprMethodCall(
|
88
|
+
expr.ExprIdent(
|
89
|
+
assert_isinstance(prop, ObjectProperty).target.name
|
90
|
+
),
|
91
|
+
"deser",
|
92
|
+
[item],
|
93
|
+
),
|
94
|
+
)
|
95
|
+
return value
|
96
|
+
else:
|
97
|
+
value = expr.ExprFuncCall(
|
98
|
+
PredefinedFn.attr_getter(
|
99
|
+
expr.ExprIdent(prop.target.name),
|
100
|
+
expr.ExprIdent("deser"),
|
101
|
+
),
|
102
|
+
[
|
103
|
+
PredefinedFn.attr_getter(
|
104
|
+
expr.ExprIdent("data"),
|
105
|
+
expr.ExprIdent(prop.name),
|
106
|
+
)
|
107
|
+
],
|
108
|
+
)
|
109
|
+
if prop.is_optional:
|
110
|
+
value = handle_optional(value)
|
111
|
+
return value
|
112
|
+
|
53
113
|
def make_normal(cls: Class, pkg: Package):
|
54
114
|
"""Make a data model for the normal Python data model"""
|
55
115
|
if not cls.is_public:
|
@@ -89,9 +149,7 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
89
149
|
deser_args.append(
|
90
150
|
(
|
91
151
|
expr.ExprIdent(propname),
|
92
|
-
|
93
|
-
expr.ExprIdent("data"), expr.ExprIdent(prop.name)
|
94
|
-
),
|
152
|
+
get_normal_deser_args(prop),
|
95
153
|
)
|
96
154
|
)
|
97
155
|
else:
|
@@ -101,9 +159,13 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
101
159
|
propname = propname + "Id"
|
102
160
|
tstype = TsTypeWithDep(
|
103
161
|
f"{prop.target.name}Id",
|
104
|
-
|
105
|
-
|
106
|
-
|
162
|
+
(
|
163
|
+
[
|
164
|
+
f"@.models.{prop.target.get_tsmodule_name()}.{prop.target.name}.{prop.target.name}Id"
|
165
|
+
]
|
166
|
+
if prop.target.name != cls.name
|
167
|
+
else []
|
168
|
+
),
|
107
169
|
)
|
108
170
|
if prop.cardinality.is_star_to_many():
|
109
171
|
tstype = tstype.as_list_type()
|
@@ -113,10 +175,7 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
113
175
|
deser_args.append(
|
114
176
|
(
|
115
177
|
expr.ExprIdent(propname),
|
116
|
-
|
117
|
-
expr.ExprIdent("data"),
|
118
|
-
expr.ExprIdent(prop.name + "_id"),
|
119
|
-
),
|
178
|
+
get_normal_deser_args(prop),
|
120
179
|
)
|
121
180
|
)
|
122
181
|
else:
|
@@ -132,21 +191,7 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
132
191
|
deser_args.append(
|
133
192
|
(
|
134
193
|
expr.ExprIdent(propname),
|
135
|
-
|
136
|
-
PredefinedFn.attr_getter(
|
137
|
-
expr.ExprIdent("data"),
|
138
|
-
expr.ExprIdent(prop.name),
|
139
|
-
),
|
140
|
-
lambda item: expr.ExprMethodCall(
|
141
|
-
expr.ExprIdent(
|
142
|
-
assert_isinstance(
|
143
|
-
prop, ObjectProperty
|
144
|
-
).target.name
|
145
|
-
),
|
146
|
-
"deser",
|
147
|
-
[item],
|
148
|
-
),
|
149
|
-
),
|
194
|
+
get_normal_deser_args(prop),
|
150
195
|
)
|
151
196
|
)
|
152
197
|
else:
|
@@ -156,18 +201,7 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
156
201
|
deser_args.append(
|
157
202
|
(
|
158
203
|
expr.ExprIdent(propname),
|
159
|
-
|
160
|
-
PredefinedFn.attr_getter(
|
161
|
-
expr.ExprIdent(prop.target.name),
|
162
|
-
expr.ExprIdent("deser"),
|
163
|
-
),
|
164
|
-
[
|
165
|
-
PredefinedFn.attr_getter(
|
166
|
-
expr.ExprIdent("data"),
|
167
|
-
expr.ExprIdent(prop.name),
|
168
|
-
)
|
169
|
-
],
|
170
|
-
),
|
204
|
+
get_normal_deser_args(prop),
|
171
205
|
)
|
172
206
|
)
|
173
207
|
|
@@ -378,8 +412,13 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
378
412
|
if tstype.type in idprop_aliases:
|
379
413
|
create_propvalue = idprop_aliases[tstype.type].get_default()
|
380
414
|
elif tstype.type in schema.enums:
|
381
|
-
|
382
|
-
|
415
|
+
enum_value = next(
|
416
|
+
iter(schema.enums[tstype.type].values.values())
|
417
|
+
).value
|
418
|
+
# TODO: handle enum value integer
|
419
|
+
assert isinstance(enum_value, str)
|
420
|
+
create_propvalue = expr.ExprIdent(
|
421
|
+
tstype.type + "." + enum_value
|
383
422
|
)
|
384
423
|
else:
|
385
424
|
create_propvalue = tstype.get_default()
|
@@ -1131,7 +1170,13 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
1131
1170
|
# if prop.data.is_private:
|
1132
1171
|
# # skip private fields as this is for APIs exchange
|
1133
1172
|
# continue
|
1134
|
-
|
1173
|
+
tspropname = to_camel_case(prop.name)
|
1174
|
+
pypropname = prop.name
|
1175
|
+
if isinstance(prop, ObjectProperty) and prop.target.db is not None:
|
1176
|
+
# this is a database object, we append id to the property name
|
1177
|
+
tspropname = tspropname + "Id"
|
1178
|
+
pypropname = prop.name + "_id"
|
1179
|
+
|
1135
1180
|
tsprop = {}
|
1136
1181
|
|
1137
1182
|
if isinstance(prop, DataProperty):
|
@@ -1169,7 +1214,7 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
1169
1214
|
norm_func = get_normalizer(tstype, import_helper)
|
1170
1215
|
if norm_func is not None:
|
1171
1216
|
# we have a normalizer for this type
|
1172
|
-
prop_normalizers.append((expr.ExprIdent(
|
1217
|
+
prop_normalizers.append((expr.ExprIdent(tspropname), norm_func))
|
1173
1218
|
else:
|
1174
1219
|
assert isinstance(prop, ObjectProperty)
|
1175
1220
|
if prop.target.db is not None:
|
@@ -1224,11 +1269,11 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
1224
1269
|
prop_defs.append(
|
1225
1270
|
(
|
1226
1271
|
prop,
|
1227
|
-
expr.ExprIdent(
|
1272
|
+
expr.ExprIdent(tspropname),
|
1228
1273
|
PredefinedFn.dict(
|
1229
1274
|
[
|
1230
|
-
(expr.ExprIdent("name"), expr.ExprConstant(
|
1231
|
-
(expr.ExprIdent("tsName"), expr.ExprConstant(
|
1275
|
+
(expr.ExprIdent("name"), expr.ExprConstant(pypropname)),
|
1276
|
+
(expr.ExprIdent("tsName"), expr.ExprConstant(tspropname)),
|
1232
1277
|
(
|
1233
1278
|
expr.ExprIdent("updateFuncName"),
|
1234
1279
|
expr.ExprConstant(f"update{to_pascal_case(prop.name)}"),
|
@@ -1298,7 +1343,10 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
1298
1343
|
" | ".join(
|
1299
1344
|
[
|
1300
1345
|
expr.ExprConstant(
|
1301
|
-
to_camel_case(prop.name)
|
1346
|
+
to_camel_case(prop.name) + "Id"
|
1347
|
+
if isinstance(prop, ObjectProperty)
|
1348
|
+
and prop.target.db is not None
|
1349
|
+
else to_camel_case(prop.name)
|
1302
1350
|
).to_typescript()
|
1303
1351
|
for prop in cls.properties.values()
|
1304
1352
|
if not prop.data.is_private
|
sera/models/_collection.py
CHANGED
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
|
5
|
+
from codegen.models import ImportHelper
|
6
|
+
|
5
7
|
from sera.models._class import Class
|
6
8
|
from sera.models._property import DataProperty, ObjectProperty
|
7
9
|
|
@@ -21,9 +23,9 @@ class DataCollection:
|
|
21
23
|
"""Get the python module name of this collection as if there is a python module created to store this collection only."""
|
22
24
|
return self.cls.get_pymodule_name()
|
23
25
|
|
24
|
-
def get_queryable_fields(self) ->
|
26
|
+
def get_queryable_fields(self) -> list[tuple[str, tuple[str, str]]]:
|
25
27
|
"""Get the fields of this collection that can be used in a queries."""
|
26
|
-
|
28
|
+
output = []
|
27
29
|
for prop in self.cls.properties.values():
|
28
30
|
if prop.db is None or prop.data.is_private:
|
29
31
|
# This property is not stored in the database or it's private, so we skip it
|
@@ -45,8 +47,19 @@ class DataCollection:
|
|
45
47
|
else:
|
46
48
|
# This property is a data property or an object property not stored in the database, so we use its name
|
47
49
|
propname = prop.name
|
48
|
-
|
49
|
-
|
50
|
+
|
51
|
+
if isinstance(prop, DataProperty):
|
52
|
+
convert_func = prop.datatype.pytype.get_string_conversion_func()
|
53
|
+
else:
|
54
|
+
assert isinstance(prop, ObjectProperty) and prop.target.db is not None
|
55
|
+
target_idprop = prop.target.get_id_property()
|
56
|
+
assert target_idprop is not None
|
57
|
+
convert_func = (
|
58
|
+
target_idprop.datatype.pytype.get_string_conversion_func()
|
59
|
+
)
|
60
|
+
|
61
|
+
output.append((propname, convert_func))
|
62
|
+
return output
|
50
63
|
|
51
64
|
def get_service_name(self):
|
52
65
|
return f"{self.name}Service"
|
sera/models/_datatype.py
CHANGED
@@ -67,6 +67,18 @@ class PyTypeWithDep:
|
|
67
67
|
"""Clone the type with the same dependencies."""
|
68
68
|
return PyTypeWithDep(type=self.type, deps=list(self.deps))
|
69
69
|
|
70
|
+
def get_string_conversion_func(self) -> tuple[str, str]:
|
71
|
+
if self.type == "str":
|
72
|
+
return ("identity", "sera.misc.identity")
|
73
|
+
if self.type == "int":
|
74
|
+
return ("TypeConversion.to_int", "sera.libs.api_helper.TypeConversion")
|
75
|
+
if self.type == "float":
|
76
|
+
return ("TypeConversion.to_float", "sera.libs.api_helper.TypeConversion")
|
77
|
+
if self.type == "bool":
|
78
|
+
return ("TypeConversion.to_bool", "sera.libs.api_helper.TypeConversion")
|
79
|
+
else:
|
80
|
+
raise NotImplementedError()
|
81
|
+
|
70
82
|
|
71
83
|
@dataclass
|
72
84
|
class TsTypeWithDep:
|
@@ -122,6 +134,23 @@ class SQLTypeWithDep:
|
|
122
134
|
mapped_pytype=f"list[{self.mapped_pytype}]",
|
123
135
|
)
|
124
136
|
|
137
|
+
def as_optional_type(self) -> SQLTypeWithDep:
|
138
|
+
"""Convert the type to an optional type."""
|
139
|
+
if "typing.Optional" not in self.deps:
|
140
|
+
deps = self.deps + ["typing.Optional"]
|
141
|
+
else:
|
142
|
+
deps = self.deps
|
143
|
+
|
144
|
+
if "Optional[" in self.mapped_pytype:
|
145
|
+
raise NotImplementedError(
|
146
|
+
f"Have not handle nested optional yet: {self.mapped_pytype}"
|
147
|
+
)
|
148
|
+
return SQLTypeWithDep(
|
149
|
+
type=self.type,
|
150
|
+
mapped_pytype=f"Optional[{self.mapped_pytype}]",
|
151
|
+
deps=deps,
|
152
|
+
)
|
153
|
+
|
125
154
|
|
126
155
|
@dataclass
|
127
156
|
class DataType:
|
sera/models/_parse.py
CHANGED
@@ -238,7 +238,12 @@ def _parse_datatype(schema: Schema, datatype: dict | str) -> DataType:
|
|
238
238
|
],
|
239
239
|
),
|
240
240
|
sqltype=SQLTypeWithDep(
|
241
|
-
type="
|
241
|
+
type=f"Enum({enum.name})",
|
242
|
+
mapped_pytype=enum.name,
|
243
|
+
deps=[
|
244
|
+
"sqlalchemy.Enum",
|
245
|
+
f"{schema.name}.models.enums.{enum.get_pymodule_name()}.{enum.name}",
|
246
|
+
],
|
242
247
|
),
|
243
248
|
tstype=TsTypeWithDep(
|
244
249
|
type=enum.name, deps=[f"@.models.enums.{enum.name}"]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: sera-2
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.20.4
|
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)
|
12
|
-
Requires-Dist: codegen-2 (>=2.12.
|
12
|
+
Requires-Dist: codegen-2 (>=2.12.2,<3.0.0)
|
13
13
|
Requires-Dist: graph-wrapper (>=1.7.2,<2.0.0)
|
14
14
|
Requires-Dist: isort (==6.0.1)
|
15
15
|
Requires-Dist: litestar (>=2.15.1,<3.0.0)
|
@@ -4,10 +4,10 @@ sera/exports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
sera/exports/schema.py,sha256=wEBUrDOyuCoCJC8X4RlmoWpeqSugaboG-9Q1UQ8HEzk,7824
|
5
5
|
sera/exports/test.py,sha256=jK1EJmLGiy7eREpnY_68IIVRH43uH8S_u5Z7STPbXOM,2002
|
6
6
|
sera/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
-
sera/libs/api_helper.py,sha256=
|
7
|
+
sera/libs/api_helper.py,sha256=VZsYDeeMxzH4-7quM8W-1rl3zLFEEVShb8kLHS2UdHE,6382
|
8
8
|
sera/libs/api_test_helper.py,sha256=3tRr8sLN4dBSrHgKAHMmyoENI0xh7K_JLel8AvujU7k,1323
|
9
9
|
sera/libs/base_orm.py,sha256=5hOH_diUeaABm3cpE2-9u50VRqG1QW2osPQnvVHIhIA,3365
|
10
|
-
sera/libs/base_service.py,sha256=
|
10
|
+
sera/libs/base_service.py,sha256=V6wug1QA5KD0FsQU9r1XQmLUTB85E6oIR2e12Fi1IE0,6457
|
11
11
|
sera/libs/directed_computing_graph/__init__.py,sha256=xiF5_I1y9HtQ-cyq02iwkRYgEZvxBB8YIvysCHCLBco,1290
|
12
12
|
sera/libs/directed_computing_graph/_dcg.py,sha256=AGTzKVSl-EsSOJlNKPOA1Io7pIxfq0SMXuumq1IExl0,14902
|
13
13
|
sera/libs/directed_computing_graph/_edge.py,sha256=iBq6cpLWWyuD99QWTHVEh8naWUJrR4WJJuq5iuCrwHo,1026
|
@@ -22,26 +22,26 @@ sera/libs/middlewares/uscp.py,sha256=H5umW8iEQSCdb_MJ5Im49kxg1E7TpxSg1p2_2A5zI1U
|
|
22
22
|
sera/make/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
23
23
|
sera/make/__main__.py,sha256=HRfOR53p351h6KblVvYm3DLhDIfEtk6R0kjl78_S_S8,1453
|
24
24
|
sera/make/make_app.py,sha256=n9NtW73O3s_5Q31VHIRmnd-jEIcpDO7ksAsOdovde2s,5999
|
25
|
-
sera/make/make_python_api.py,sha256=
|
26
|
-
sera/make/make_python_model.py,sha256=
|
25
|
+
sera/make/make_python_api.py,sha256=aOm8QSiXNLe4akiOx_KKsDCwLVwetRzuxOgZaWqEj0w,27172
|
26
|
+
sera/make/make_python_model.py,sha256=t_2RUaO6WL4b5FtYj-Ly70njmcGGYwoddbL_oim9378,63479
|
27
27
|
sera/make/make_python_services.py,sha256=0ZpWLwQ7Nwfn8BXAikAB4JRpNknpSJyJgY5b1cjtxV4,2073
|
28
|
-
sera/make/make_typescript_model.py,sha256=
|
28
|
+
sera/make/make_typescript_model.py,sha256=u9pbe9JnSwiV1wLAS_H3p2mdOL_FKXjsZRCRuPu2QBY,69156
|
29
29
|
sera/misc/__init__.py,sha256=rOmGMv7QNzpSKZSyxChbRmEnBr3O443UlLGS0FIs3AI,561
|
30
30
|
sera/misc/_formatter.py,sha256=aCGYL08l8f3aLODHxSocxBBwkRYEo3K1QzCDEn3suj0,1685
|
31
31
|
sera/misc/_utils.py,sha256=_-17XbK6qp3HobcI9iLF4xfaATvFg1ckUzgg7r7Ctmw,7135
|
32
32
|
sera/models/__init__.py,sha256=vJC5Kzo_N7wd16ocNPy1VvAZDGNiWeiAhWJ4ihATKvA,780
|
33
33
|
sera/models/_class.py,sha256=1J4Bd_LanzhhDWwZFHWGtFYD7lupe_alaB3D02ebNDI,2862
|
34
|
-
sera/models/_collection.py,sha256=
|
34
|
+
sera/models/_collection.py,sha256=nLlP85OfEhfj4pFAYxB5ZvtBN6EdPSWPJ5ZFh5SSEC4,2686
|
35
35
|
sera/models/_constraints.py,sha256=RpWDU-TfCslXaMUaTG9utWbl5z8Z6nzvF_fhqlek6ew,1987
|
36
|
-
sera/models/_datatype.py,sha256=
|
36
|
+
sera/models/_datatype.py,sha256=6s2TIhUX8Eb1w3XXKUcOoVybF_lrmKtNAriDgzv2JAE,8407
|
37
37
|
sera/models/_default.py,sha256=ABggW6qdPR4ZDqIPJdJ0GCGQ-7kfsfZmQ_DchgZEa-I,137
|
38
38
|
sera/models/_enum.py,sha256=sy0q7E646F-APsqrVQ52r1fAQ_DCAeaNq5YM5QN3zIk,2070
|
39
39
|
sera/models/_module.py,sha256=I-GfnTgAa-5R87qTAvEzOt-VVEGeFBBwubGCgUkXVSw,5159
|
40
40
|
sera/models/_multi_lingual_string.py,sha256=JETN6k00VH4wrA4w5vAHMEJV8fp3SY9bJebskFTjQLA,1186
|
41
|
-
sera/models/_parse.py,sha256=
|
41
|
+
sera/models/_parse.py,sha256=sm-TuhFuru4e-g8HYmPo694sGJooBtIFZeky24X7YYk,12588
|
42
42
|
sera/models/_property.py,sha256=9yMDxrmbyuF6-29lQjiq163Xzwbk75TlmGBpu0NLpkI,7485
|
43
43
|
sera/models/_schema.py,sha256=VxJEiqgVvbXgcSUK4UW6JnRcggk4nsooVSE6MyXmfNY,1636
|
44
44
|
sera/typing.py,sha256=m4rir-fB6Cgcm7_ZSXXcNdla2LJgq96WXxtTTrDaJno,1058
|
45
|
-
sera_2-1.
|
46
|
-
sera_2-1.
|
47
|
-
sera_2-1.
|
45
|
+
sera_2-1.20.4.dist-info/METADATA,sha256=2HIbHZt_ScaEAzPZGlpNNX7jEmiv1sGmMVgmK-7nJU4,936
|
46
|
+
sera_2-1.20.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
47
|
+
sera_2-1.20.4.dist-info/RECORD,,
|
File without changes
|