sera-2 1.20.4__py3-none-any.whl → 1.20.13__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 CHANGED
@@ -15,7 +15,7 @@ from litestar.typing import FieldDefinition
15
15
  from msgspec import Struct
16
16
 
17
17
  from sera.libs.base_service import Query, QueryOp
18
- from sera.libs.middlewares.uscp import STATE_SYSTEM_CONTROLLED_PROP_KEY
18
+ from sera.libs.middlewares.uscp import SKIP_UPDATE_SYSTEM_CONTROLLED_PROPS_KEY
19
19
  from sera.typing import T
20
20
 
21
21
  # for parsing field names and operations from query string
@@ -91,7 +91,7 @@ def parse_query(
91
91
  v = [norm_func(x) for x in v]
92
92
  else:
93
93
  v = norm_func(v)
94
- except ValueError:
94
+ except (ValueError, KeyError):
95
95
  if debug:
96
96
  raise HTTPException(
97
97
  status_code=status_codes.HTTP_400_BAD_REQUEST,
@@ -136,9 +136,14 @@ class SingleAutoUSCP(MsgspecDTO[S], Generic[S]):
136
136
  "data_backend"
137
137
  ] # pyright: ignore
138
138
  obj = backend.populate_data_from_raw(value, self.asgi_connection)
139
- obj.update_system_controlled_props(
140
- self.asgi_connection.scope["state"][STATE_SYSTEM_CONTROLLED_PROP_KEY]
141
- )
139
+ if self.asgi_connection.scope["state"][SKIP_UPDATE_SYSTEM_CONTROLLED_PROPS_KEY]:
140
+ # Skip updating system-controlled properties
141
+ # TODO: dirty fix as this assumes every struct has _is_scp_updated property. find a
142
+ # better solution and fix me!
143
+ obj._is_scp_updated = True
144
+ return obj
145
+
146
+ obj.update_system_controlled_props(self.asgi_connection)
142
147
  return obj
143
148
 
144
149
 
@@ -6,7 +6,7 @@ from litestar.connection.base import UserT
6
6
  from litestar.middleware import AbstractMiddleware
7
7
  from litestar.types import ASGIApp, Message, Receive, Scope, Scopes, Send
8
8
 
9
- STATE_SYSTEM_CONTROLLED_PROP_KEY = "system_controlled_properties"
9
+ SKIP_UPDATE_SYSTEM_CONTROLLED_PROPS_KEY = "skip_uscp_1157"
10
10
 
11
11
 
12
12
  class USCPMiddleware(AbstractMiddleware):
@@ -21,7 +21,6 @@ class USCPMiddleware(AbstractMiddleware):
21
21
  def __init__(
22
22
  self,
23
23
  app: ASGIApp,
24
- get_system_controlled_props: Callable[[UserT], dict],
25
24
  skip_update_system_controlled_props: Callable[[UserT], bool],
26
25
  exclude: str | list[str] | None = None,
27
26
  exclude_opt_key: str | None = None,
@@ -39,17 +38,13 @@ class USCPMiddleware(AbstractMiddleware):
39
38
  either or both 'ScopeType.HTTP' and 'ScopeType.WEBSOCKET'.
40
39
  """
41
40
  super().__init__(app, exclude, exclude_opt_key, scopes)
42
- self.get_system_controlled_props = get_system_controlled_props
43
41
  self.skip_update_system_controlled_props = skip_update_system_controlled_props
44
42
 
45
43
  async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
46
44
  user = scope["user"]
47
- if self.skip_update_system_controlled_props(user):
48
- scope["state"][STATE_SYSTEM_CONTROLLED_PROP_KEY] = None
49
- else:
50
- scope["state"][STATE_SYSTEM_CONTROLLED_PROP_KEY] = (
51
- self.get_system_controlled_props(user)
52
- )
45
+ scope["state"][SKIP_UPDATE_SYSTEM_CONTROLLED_PROPS_KEY] = (
46
+ self.skip_update_system_controlled_props(user)
47
+ )
53
48
  await self.app(scope, receive, send)
54
49
 
55
50
 
@@ -29,6 +29,8 @@ from sera.typing import is_set
29
29
  TS_GLOBAL_IDENTS = {
30
30
  "normalizers.normalizeNumber": "sera-db.normalizers",
31
31
  "normalizers.normalizeOptionalNumber": "sera-db.normalizers",
32
+ "normalizers.normalizeDate": "sera-db.normalizers",
33
+ "normalizers.normalizeOptionalDate": "sera-db.normalizers",
32
34
  }
33
35
 
34
36
 
@@ -66,6 +68,14 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
66
68
  )
67
69
  if prop.is_optional:
68
70
  value = handle_optional(value)
71
+ value.true_expr = (
72
+ prop.datatype.get_typescript_type().get_json_deser_func(
73
+ value.true_expr
74
+ )
75
+ )
76
+ else:
77
+ value = prop.datatype.get_typescript_type().get_json_deser_func(value)
78
+
69
79
  return value
70
80
 
71
81
  assert isinstance(prop, ObjectProperty)
@@ -140,7 +150,9 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
140
150
 
141
151
  if idprop is not None and prop.name == idprop.name:
142
152
  # use id type alias
143
- tstype = TsTypeWithDep(f"{cls.name}Id")
153
+ tstype = TsTypeWithDep(
154
+ type=f"{cls.name}Id", spectype=tstype.spectype
155
+ )
144
156
 
145
157
  if prop.is_optional:
146
158
  # convert type to optional
@@ -158,8 +170,12 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
158
170
  # this class is stored in the database, we store the id instead
159
171
  propname = propname + "Id"
160
172
  tstype = TsTypeWithDep(
161
- f"{prop.target.name}Id",
162
- (
173
+ type=f"{prop.target.name}Id",
174
+ spectype=assert_not_null(prop.target.get_id_property())
175
+ .get_data_model_datatype()
176
+ .get_typescript_type()
177
+ .spectype,
178
+ deps=(
163
179
  [
164
180
  f"@.models.{prop.target.get_tsmodule_name()}.{prop.target.name}.{prop.target.name}Id"
165
181
  ]
@@ -181,8 +197,9 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
181
197
  else:
182
198
  # we are going to store the whole object
183
199
  tstype = TsTypeWithDep(
184
- prop.target.name,
185
- [
200
+ type=prop.target.name,
201
+ spectype=prop.target.name,
202
+ deps=[
186
203
  f"@.models.{prop.target.get_tsmodule_name()}.{prop.target.name}.{prop.target.name}"
187
204
  ],
188
205
  )
@@ -249,6 +266,17 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
249
266
  ],
250
267
  )(*prop_constructor_assigns),
251
268
  stmt.LineBreak(),
269
+ lambda ast12: ast12.func(
270
+ "className",
271
+ [],
272
+ expr.ExprIdent("string"),
273
+ is_static=True,
274
+ modifiers=["get"],
275
+ comment="Name of the class in the Schema",
276
+ )(
277
+ stmt.ReturnStatement(expr.ExprConstant(cls.name)),
278
+ ),
279
+ stmt.LineBreak(),
252
280
  lambda ast12: ast12.func(
253
281
  "deser",
254
282
  [
@@ -382,10 +410,10 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
382
410
  if idprop is not None and prop.name == idprop.name:
383
411
  # use id type alias
384
412
  tstype = TsTypeWithDep(
385
- f"{cls.name}Id",
413
+ type=f"{cls.name}Id",
414
+ spectype=tstype.spectype,
386
415
  deps=[f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}Id"],
387
416
  )
388
- original_tstype = tstype
389
417
  elif tstype.type not in schema.enums:
390
418
  # for none id & none enum properties, we need to include a type for "invalid" value
391
419
  tstype = _inject_type_for_invalid_value(tstype)
@@ -455,7 +483,10 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
455
483
  expr.ExprIdent("record"), expr.ExprIdent(propname)
456
484
  )
457
485
 
458
- if original_tstype.type != tstype.type:
486
+ if (
487
+ original_tstype.type != tstype.type
488
+ and tstype.type != f"{cls.name}Id"
489
+ ):
459
490
  norm_func = get_norm_func(original_tstype, import_helper)
460
491
  else:
461
492
  norm_func = identity
@@ -480,17 +511,22 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
480
511
  ),
481
512
  expr.ExprIdent("isValid"),
482
513
  ),
483
- norm_func(
484
- PredefinedFn.attr_getter(
485
- expr.ExprIdent("this"), expr.ExprIdent(propname)
514
+ original_tstype.get_json_ser_func(
515
+ norm_func(
516
+ PredefinedFn.attr_getter(
517
+ expr.ExprIdent("this"),
518
+ expr.ExprIdent(propname),
519
+ )
486
520
  )
487
521
  ),
488
522
  expr.ExprIdent("undefined"),
489
523
  )
490
524
  if prop.is_optional
491
- else norm_func(
492
- PredefinedFn.attr_getter(
493
- expr.ExprIdent("this"), expr.ExprIdent(propname)
525
+ else original_tstype.get_json_ser_func(
526
+ norm_func(
527
+ PredefinedFn.attr_getter(
528
+ expr.ExprIdent("this"), expr.ExprIdent(propname)
529
+ )
494
530
  )
495
531
  )
496
532
  ),
@@ -555,8 +591,12 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
555
591
  if prop.target.db is not None:
556
592
  # this class is stored in the database, we store the id instead
557
593
  tstype = TsTypeWithDep(
558
- f"{prop.target.name}Id",
559
- [
594
+ type=f"{prop.target.name}Id",
595
+ spectype=assert_not_null(prop.target.get_id_property())
596
+ .get_data_model_datatype()
597
+ .get_typescript_type()
598
+ .spectype,
599
+ deps=[
560
600
  f"@.models.{prop.target.get_tsmodule_name()}.{prop.target.name}.{prop.target.name}Id"
561
601
  ],
562
602
  )
@@ -607,8 +647,9 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
607
647
  else:
608
648
  # we are going to store the whole object
609
649
  tstype = TsTypeWithDep(
610
- f"Draft{prop.target.name}",
611
- [
650
+ type=f"Draft{prop.target.name}",
651
+ spectype=f"Draft{prop.target.name}",
652
+ deps=[
612
653
  f"@.models.{prop.target.get_tsmodule_name()}.Draft{prop.target.name}.Draft{prop.target.name}"
613
654
  ],
614
655
  )
@@ -1191,7 +1232,7 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
1191
1232
  (
1192
1233
  expr.ExprIdent("datatype"),
1193
1234
  (
1194
- expr.ExprConstant(tstype.type)
1235
+ expr.ExprConstant(tstype.spectype)
1195
1236
  if tstype.type not in schema.enums
1196
1237
  else expr.ExprConstant("enum")
1197
1238
  ),
@@ -1227,8 +1268,9 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
1227
1268
  else:
1228
1269
  # we are going to store the whole object
1229
1270
  tstype = TsTypeWithDep(
1230
- prop.target.name,
1231
- [
1271
+ type=prop.target.name,
1272
+ spectype=prop.target.name,
1273
+ deps=[
1232
1274
  f"@.models.{prop.target.get_tsmodule_name()}.{prop.target.name}.{prop.target.name}"
1233
1275
  ],
1234
1276
  )
@@ -1249,7 +1291,9 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
1249
1291
  (
1250
1292
  expr.ExprIdent("datatype"),
1251
1293
  expr.ExprConstant(
1252
- tstype.type if prop.target.db is not None else "undefined"
1294
+ tstype.spectype
1295
+ if prop.target.db is not None
1296
+ else "undefined"
1253
1297
  ),
1254
1298
  ),
1255
1299
  (
@@ -1605,6 +1649,10 @@ def get_normalizer(
1605
1649
  return import_helper.use("normalizers.normalizeNumber")
1606
1650
  if tstype.type == "number | undefined":
1607
1651
  return import_helper.use("normalizers.normalizeOptionalNumber")
1652
+ if tstype.type == "Date":
1653
+ return import_helper.use("normalizers.normalizeDate")
1654
+ if tstype.type == "Date | undefined":
1655
+ return import_helper.use("normalizers.normalizeOptionalDate")
1608
1656
 
1609
1657
  assert "number" not in tstype.type, tstype.type
1610
1658
  return None
sera/misc/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from sera.misc._formatter import File, Formatter
2
2
  from sera.misc._utils import (
3
3
  LoadTableDataArgs,
4
+ RelTableIndex,
4
5
  assert_isinstance,
5
6
  assert_not_null,
6
7
  filter_duplication,
@@ -26,4 +27,5 @@ __all__ = [
26
27
  "identity",
27
28
  "get_classpath",
28
29
  "LoadTableDataArgs",
30
+ "RelTableIndex",
29
31
  ]
sera/misc/_utils.py CHANGED
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import inspect
3
4
  import re
5
+ from collections import defaultdict
4
6
  from dataclasses import dataclass
5
7
  from pathlib import Path
6
8
  from typing import Any, Callable, Iterable, Optional, Sequence, Type, TypedDict, TypeVar
@@ -120,8 +122,38 @@ class LoadTableDataArgs(TypedDict, total=False):
120
122
  tables: Sequence[type]
121
123
  file: Path
122
124
  files: Sequence[Path]
123
- file_deser: Callable[[Path], list[dict]]
124
- record_deser: Callable[[dict], Any | list[Any]]
125
+ file_deser: Callable[[Path], list[Any]]
126
+ record_deser: (
127
+ Callable[[dict], Any | list[Any]]
128
+ | Callable[[dict, RelTableIndex], Any | list[Any]]
129
+ )
130
+ table_unique_index: dict[type, list[str]]
131
+
132
+
133
+ class RelTableIndex:
134
+ """An index of relational tables to find a record by its unique property."""
135
+
136
+ def __init__(self, cls2index: Optional[dict[str, list[str]]] = None):
137
+ self.table2rows: dict[str, dict[str, Any]] = defaultdict(dict)
138
+ self.table2uniqindex2id: dict[str, dict[str, int]] = defaultdict(dict)
139
+ self.cls2index = cls2index or {}
140
+
141
+ def set_index(self, clsname: str, props: list[str]):
142
+ """Set the unique index for a class."""
143
+ self.cls2index[clsname] = props
144
+
145
+ def add(self, record: Any):
146
+ clsname = record.__class__.__name__
147
+ self.table2rows[clsname][record.id] = record
148
+ if clsname in self.cls2index:
149
+ for prop in self.cls2index[clsname]:
150
+ self.table2uniqindex2id[clsname][getattr(record, prop)] = record.id
151
+
152
+ def get_record(self, clsname: str, uniq_prop: str) -> Optional[Any]:
153
+ tbl = self.table2uniqindex2id[clsname]
154
+ if uniq_prop not in tbl:
155
+ return None
156
+ return self.table2rows[clsname][tbl[uniq_prop]]
125
157
 
126
158
 
127
159
  def load_data(
@@ -143,6 +175,8 @@ def load_data(
143
175
  with Session(engine) as session:
144
176
  create_db_and_tables()
145
177
 
178
+ reltable_index = RelTableIndex()
179
+
146
180
  for args in tqdm(table_data, disable=not verbose, desc="Loading data"):
147
181
  if "table" in args:
148
182
  tbls = [args["table"]]
@@ -162,6 +196,12 @@ def load_data(
162
196
  else:
163
197
  raise ValueError("Either 'file' or 'files' must be provided in args.")
164
198
 
199
+ if "table_unique_index" in args:
200
+ for tbl in tbls:
201
+ reltable_index.set_index(
202
+ tbl.__name__, args["table_unique_index"].get(tbl, [])
203
+ )
204
+
165
205
  raw_records = []
166
206
  if "file_deser" not in args:
167
207
  for file in files:
@@ -175,8 +215,17 @@ def load_data(
175
215
  for file in files:
176
216
  raw_records.extend(args["file_deser"](file))
177
217
 
218
+ assert "record_deser" in args
178
219
  deser = args["record_deser"]
179
- records = [deser(row) for row in raw_records]
220
+
221
+ sig = inspect.signature(deser)
222
+ param_count = len(sig.parameters)
223
+ if param_count == 1:
224
+ records = [deser(row) for row in raw_records]
225
+ else:
226
+ assert param_count == 2
227
+ records = [deser(row, reltable_index) for row in raw_records]
228
+
180
229
  for r in tqdm(
181
230
  records,
182
231
  desc=f"load {', '.join(tbl.__name__ for tbl in tbls)}",
@@ -185,8 +234,11 @@ def load_data(
185
234
  if isinstance(r, Sequence):
186
235
  for x in r:
187
236
  session.merge(x)
237
+ reltable_index.add(x)
188
238
  else:
189
239
  session.merge(r)
240
+ reltable_index.add(r)
241
+
190
242
  session.flush()
191
243
 
192
244
  # Reset the sequence for each table
sera/models/_datatype.py CHANGED
@@ -2,10 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  import datetime
4
4
  from dataclasses import dataclass, field
5
- from typing import Literal
5
+ from typing import Callable, Literal
6
6
 
7
7
  from codegen.models import expr
8
8
 
9
+ from sera.misc import identity
10
+
9
11
  PyDataType = Literal["str", "int", "datetime", "float", "bool", "bytes", "dict"]
10
12
  TypescriptDataType = Literal["string", "number", "boolean"]
11
13
  SQLAlchemyDataType = Literal[
@@ -76,6 +78,19 @@ class PyTypeWithDep:
76
78
  return ("TypeConversion.to_float", "sera.libs.api_helper.TypeConversion")
77
79
  if self.type == "bool":
78
80
  return ("TypeConversion.to_bool", "sera.libs.api_helper.TypeConversion")
81
+ if any(
82
+ dep.find(".models.enums.") != -1 and dep.endswith(self.type)
83
+ for dep in self.deps
84
+ ):
85
+ # This is an enum type, we directly use the enum constructor as the conversion function
86
+ return (
87
+ self.type,
88
+ [
89
+ dep
90
+ for dep in self.deps
91
+ if dep.find(".models.enums.") != -1 and dep.endswith(self.type)
92
+ ][0],
93
+ )
79
94
  else:
80
95
  raise NotImplementedError()
81
96
 
@@ -83,6 +98,9 @@ class PyTypeWithDep:
83
98
  @dataclass
84
99
  class TsTypeWithDep:
85
100
  type: str
101
+ # the specific type of the value, to provide more details for the type because typescript use
102
+ # number for both int and float, date for both date and datetime.
103
+ spectype: str
86
104
  deps: list[str] = field(default_factory=list)
87
105
 
88
106
  def get_default(self) -> expr.ExprConstant:
@@ -94,6 +112,8 @@ class TsTypeWithDep:
94
112
  return expr.ExprConstant(0)
95
113
  if self.type == "boolean":
96
114
  return expr.ExprConstant(False)
115
+ if self.type == "Date":
116
+ return expr.ExprConstant("new Date()")
97
117
  if self.type.endswith("| undefined"):
98
118
  return expr.ExprConstant("undefined")
99
119
  if self.type.endswith("| string)") or self.type.endswith("| string"):
@@ -108,16 +128,54 @@ class TsTypeWithDep:
108
128
  if not all(c.isalnum() or c == "_" for c in self.type.strip()):
109
129
  # Type contains special chars like | or spaces, wrap in parentheses
110
130
  list_type = f"({self.type})[]"
131
+ list_spectype = f"({self.spectype})[]"
111
132
  else:
112
133
  list_type = f"{self.type}[]"
113
- return TsTypeWithDep(type=list_type, deps=self.deps)
134
+ list_spectype = f"{self.spectype}[]"
135
+ return TsTypeWithDep(type=list_type, spectype=list_spectype, deps=self.deps)
114
136
 
115
137
  def as_optional_type(self) -> TsTypeWithDep:
116
138
  if "undefined" in self.type:
117
139
  raise NotImplementedError(
118
140
  f"Have not handle nested optional yet: {self.type}"
119
141
  )
120
- return TsTypeWithDep(type=f"{self.type} | undefined", deps=self.deps)
142
+ return TsTypeWithDep(
143
+ type=f"{self.type} | undefined",
144
+ # not changing the spectype because we convert to optional when the value is missing
145
+ # spectype is used to tell the main type of the value when it is present.
146
+ spectype=self.spectype,
147
+ deps=self.deps,
148
+ )
149
+
150
+ def get_json_deser_func(self, value: expr.Expr) -> expr.Expr:
151
+ """Get the typescript expression to convert the value from json format to the correct type."""
152
+ if self.type in {"string", "number", "boolean", "string[]"}:
153
+ return value
154
+ if self.type == "Date":
155
+ return expr.ExprRawTypescript(f"new Date({value.to_typescript()})")
156
+ if any(x.startswith("@.models.enum") for x in self.deps):
157
+ # enum type, we don't need to do anything as we use strings for enums
158
+ return value
159
+ raise ValueError(f"Unknown type: {self.type}")
160
+
161
+ def get_json_ser_func(self, value: expr.Expr) -> expr.Expr:
162
+ """Get the typescript expression to convert the value to json format."""
163
+ if self.type in {
164
+ "string",
165
+ "number",
166
+ "boolean",
167
+ "string[]",
168
+ "number | undefined",
169
+ "boolean | undefined",
170
+ "string | undefined",
171
+ }:
172
+ return value
173
+ if self.type == "Date":
174
+ return expr.ExprRawTypescript(f"{value.to_typescript()}.toISOString()")
175
+ if any(x.startswith("@.models.enum") for x in self.deps):
176
+ # enum type, we don't need to do anything as we use strings for enums
177
+ return value
178
+ raise ValueError(f"Unknown type: {self.type}")
121
179
 
122
180
 
123
181
  @dataclass
@@ -185,7 +243,7 @@ predefined_datatypes = {
185
243
  sqltype=SQLTypeWithDep(
186
244
  type="String", mapped_pytype="str", deps=["sqlalchemy.String"]
187
245
  ),
188
- tstype=TsTypeWithDep(type="string"),
246
+ tstype=TsTypeWithDep(type="string", spectype="string"),
189
247
  is_list=False,
190
248
  ),
191
249
  "integer": DataType(
@@ -193,7 +251,7 @@ predefined_datatypes = {
193
251
  sqltype=SQLTypeWithDep(
194
252
  type="Integer", mapped_pytype="int", deps=["sqlalchemy.Integer"]
195
253
  ),
196
- tstype=TsTypeWithDep(type="number"),
254
+ tstype=TsTypeWithDep(type="number", spectype="integer"),
197
255
  is_list=False,
198
256
  ),
199
257
  "date": DataType(
@@ -203,7 +261,7 @@ predefined_datatypes = {
203
261
  mapped_pytype="date",
204
262
  deps=["sqlalchemy.Date", "datetime.date"],
205
263
  ),
206
- tstype=TsTypeWithDep(type="string"),
264
+ tstype=TsTypeWithDep(type="Date", spectype="date"),
207
265
  is_list=False,
208
266
  ),
209
267
  "datetime": DataType(
@@ -213,7 +271,7 @@ predefined_datatypes = {
213
271
  mapped_pytype="datetime",
214
272
  deps=["sqlalchemy.DateTime", "datetime.datetime"],
215
273
  ),
216
- tstype=TsTypeWithDep(type="string"),
274
+ tstype=TsTypeWithDep(type="Date", spectype="datetime"),
217
275
  is_list=False,
218
276
  ),
219
277
  "float": DataType(
@@ -221,7 +279,7 @@ predefined_datatypes = {
221
279
  sqltype=SQLTypeWithDep(
222
280
  type="Float", mapped_pytype="float", deps=["sqlalchemy.Float"]
223
281
  ),
224
- tstype=TsTypeWithDep(type="number"),
282
+ tstype=TsTypeWithDep(type="number", spectype="float"),
225
283
  is_list=False,
226
284
  ),
227
285
  "boolean": DataType(
@@ -229,7 +287,7 @@ predefined_datatypes = {
229
287
  sqltype=SQLTypeWithDep(
230
288
  type="Boolean", mapped_pytype="bool", deps=["sqlalchemy.Boolean"]
231
289
  ),
232
- tstype=TsTypeWithDep(type="boolean"),
290
+ tstype=TsTypeWithDep(type="boolean", spectype="boolean"),
233
291
  is_list=False,
234
292
  ),
235
293
  "bytes": DataType(
@@ -237,7 +295,7 @@ predefined_datatypes = {
237
295
  sqltype=SQLTypeWithDep(
238
296
  type="LargeBinary", mapped_pytype="bytes", deps=["sqlalchemy.LargeBinary"]
239
297
  ),
240
- tstype=TsTypeWithDep(type="string"),
298
+ tstype=TsTypeWithDep(type="string", spectype="bytes"),
241
299
  is_list=False,
242
300
  ),
243
301
  "dict": DataType(
@@ -245,7 +303,7 @@ predefined_datatypes = {
245
303
  sqltype=SQLTypeWithDep(
246
304
  type="JSON", mapped_pytype="dict", deps=["sqlalchemy.JSON"]
247
305
  ),
248
- tstype=TsTypeWithDep(type="string"),
306
+ tstype=TsTypeWithDep(type="string", spectype="dict"),
249
307
  is_list=False,
250
308
  ),
251
309
  }
@@ -257,5 +315,5 @@ predefined_sql_datatypes = {
257
315
  ),
258
316
  }
259
317
  predefined_ts_datatypes = {
260
- "string": TsTypeWithDep(type="string"),
318
+ "string": TsTypeWithDep(type="string", spectype="string"),
261
319
  }
sera/models/_parse.py CHANGED
@@ -246,7 +246,9 @@ def _parse_datatype(schema: Schema, datatype: dict | str) -> DataType:
246
246
  ],
247
247
  ),
248
248
  tstype=TsTypeWithDep(
249
- type=enum.name, deps=[f"@.models.enums.{enum.name}"]
249
+ type=enum.name,
250
+ spectype=enum.name,
251
+ deps=[f"@.models.enums.{enum.name}"],
250
252
  ),
251
253
  is_list=is_list,
252
254
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sera-2
3
- Version: 1.20.4
3
+ Version: 1.20.13
4
4
  Summary:
5
5
  Author: Binh Vu
6
6
  Author-email: bvu687@gmail.com
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3.12
10
10
  Classifier: Programming Language :: Python :: 3.13
11
11
  Requires-Dist: black (==25.1.0)
12
12
  Requires-Dist: codegen-2 (>=2.12.2,<3.0.0)
13
- Requires-Dist: graph-wrapper (>=1.7.2,<2.0.0)
13
+ Requires-Dist: graph-wrapper (>=1.7.3,<2.0.0)
14
14
  Requires-Dist: isort (==6.0.1)
15
15
  Requires-Dist: litestar (>=2.15.1,<3.0.0)
16
16
  Requires-Dist: loguru (>=0.7.0,<0.8.0)
@@ -18,7 +18,7 @@ Requires-Dist: msgspec (>=0.19.0,<0.20.0)
18
18
  Requires-Dist: serde2 (>=1.9.2,<2.0.0)
19
19
  Requires-Dist: sqlalchemy[asyncio] (>=2.0.41,<3.0.0)
20
20
  Requires-Dist: tqdm (>=4.67.1,<5.0.0)
21
- Requires-Dist: typer (>=0.12.3,<0.13.0)
21
+ Requires-Dist: typer (>=0.16.0,<0.17.0)
22
22
  Project-URL: Repository, https://github.com/binh-vu/sera
23
23
  Description-Content-Type: text/markdown
24
24
 
@@ -4,7 +4,7 @@ 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=VZsYDeeMxzH4-7quM8W-1rl3zLFEEVShb8kLHS2UdHE,6382
7
+ sera/libs/api_helper.py,sha256=lSnnNz-LzNaWmb5g7ic9bpJHl9IQd8u1GqH1kHFR3xk,6677
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
10
  sera/libs/base_service.py,sha256=V6wug1QA5KD0FsQU9r1XQmLUTB85E6oIR2e12Fi1IE0,6457
@@ -18,30 +18,30 @@ sera/libs/directed_computing_graph/_runtime.py,sha256=76Ccl1Rj31SkzRJPWFvYNu9ZzU
18
18
  sera/libs/directed_computing_graph/_type_conversion.py,sha256=_XGvDidOJVmHS4gqdPlhJGzdV34YtNiPF5Kr2nV6ZgE,6806
19
19
  sera/libs/middlewares/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  sera/libs/middlewares/auth.py,sha256=r6aix1ZBwxMd1Jv5hMCTB8a_gFOJQ6egvxIrf3DWEOs,2323
21
- sera/libs/middlewares/uscp.py,sha256=H5umW8iEQSCdb_MJ5Im49kxg1E7TpxSg1p2_2A5zI1U,2600
21
+ sera/libs/middlewares/uscp.py,sha256=DRy99nmS3qS5HLjRMIGP0oNUtQIci_a4hL5xQh-lXNY,2322
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
25
  sera/make/make_python_api.py,sha256=aOm8QSiXNLe4akiOx_KKsDCwLVwetRzuxOgZaWqEj0w,27172
26
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=u9pbe9JnSwiV1wLAS_H3p2mdOL_FKXjsZRCRuPu2QBY,69156
29
- sera/misc/__init__.py,sha256=rOmGMv7QNzpSKZSyxChbRmEnBr3O443UlLGS0FIs3AI,561
28
+ sera/make/make_typescript_model.py,sha256=CZc_LMigy7e6tP-6bJ5gHZlHYeyRtU_CNR4s64x51xE,71369
29
+ sera/misc/__init__.py,sha256=BuwDvVl0nYAA_pYM0MAbRl8DqmuasZzidO46kNiu-5A,601
30
30
  sera/misc/_formatter.py,sha256=aCGYL08l8f3aLODHxSocxBBwkRYEo3K1QzCDEn3suj0,1685
31
- sera/misc/_utils.py,sha256=_-17XbK6qp3HobcI9iLF4xfaATvFg1ckUzgg7r7Ctmw,7135
31
+ sera/misc/_utils.py,sha256=bum84Eh6gVsax0Gl4SxkmD6hUWazEljxsV16Q3BNcN8,9019
32
32
  sera/models/__init__.py,sha256=vJC5Kzo_N7wd16ocNPy1VvAZDGNiWeiAhWJ4ihATKvA,780
33
33
  sera/models/_class.py,sha256=1J4Bd_LanzhhDWwZFHWGtFYD7lupe_alaB3D02ebNDI,2862
34
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=6s2TIhUX8Eb1w3XXKUcOoVybF_lrmKtNAriDgzv2JAE,8407
36
+ sera/models/_datatype.py,sha256=RUKXwOjTr4YTloB0KQcOOAARXamoqMDXGtZ_xjldwD4,11095
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=sm-TuhFuru4e-g8HYmPo694sGJooBtIFZeky24X7YYk,12588
41
+ sera/models/_parse.py,sha256=9iaW-8Ajq8e440FrTJ-X9oivHJJ447kPU-ZqffFxSvw,12649
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.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,,
45
+ sera_2-1.20.13.dist-info/METADATA,sha256=Z0AAY0cKuVh4__UTmDqCXW0LQygzBeVrM50JOYp1-wY,937
46
+ sera_2-1.20.13.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
47
+ sera_2-1.20.13.dist-info/RECORD,,