sera-2 1.20.13__py3-none-any.whl → 1.20.14__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
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- from typing import Callable, Collection, Generic, TypeVar, cast
4
+ from typing import Callable, Collection, Generic, Mapping, TypeVar, cast
5
5
 
6
6
  from litestar import Request, status_codes
7
7
  from litestar.connection import ASGIConnection
@@ -40,7 +40,7 @@ class TypeConversion:
40
40
 
41
41
  def parse_query(
42
42
  request: Request,
43
- fields: dict[str, Callable[[str], str | int | bool | float]],
43
+ fields: Mapping[str, Callable[[str], str | int | bool | float]],
44
44
  debug: bool,
45
45
  ) -> Query:
46
46
  """Parse query for retrieving records that match a query.
sera/misc/__init__.py CHANGED
@@ -8,6 +8,7 @@ from sera.misc._utils import (
8
8
  get_classpath,
9
9
  identity,
10
10
  load_data,
11
+ load_data_from_dir,
11
12
  to_camel_case,
12
13
  to_pascal_case,
13
14
  to_snake_case,
@@ -28,4 +29,5 @@ __all__ = [
28
29
  "get_classpath",
29
30
  "LoadTableDataArgs",
30
31
  "RelTableIndex",
32
+ "load_data_from_dir",
31
33
  ]
sera/misc/_utils.py CHANGED
@@ -1,14 +1,28 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import importlib
3
4
  import inspect
4
5
  import re
5
6
  from collections import defaultdict
6
7
  from dataclasses import dataclass
7
8
  from pathlib import Path
8
- from typing import Any, Callable, Iterable, Optional, Sequence, Type, TypedDict, TypeVar
9
-
9
+ from typing import (
10
+ Any,
11
+ Callable,
12
+ Iterable,
13
+ Optional,
14
+ Sequence,
15
+ Type,
16
+ TypedDict,
17
+ TypeVar,
18
+ cast,
19
+ )
20
+
21
+ import msgspec
22
+ import orjson
10
23
  import serde.csv
11
24
  import serde.json
25
+ from loguru import logger
12
26
  from sqlalchemy import Engine, text
13
27
  from sqlalchemy.orm import Session
14
28
  from tqdm import tqdm
@@ -57,6 +71,13 @@ reserved_keywords = {
57
71
  }
58
72
 
59
73
 
74
+ def import_attr(attr_ident: str):
75
+ lst = attr_ident.rsplit(".", 1)
76
+ module, cls = lst
77
+ module = importlib.import_module(module)
78
+ return getattr(module, cls)
79
+
80
+
60
81
  def to_snake_case(camelcase: str) -> str:
61
82
  """Convert camelCase to snake_case."""
62
83
  snake = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", camelcase)
@@ -123,10 +144,10 @@ class LoadTableDataArgs(TypedDict, total=False):
123
144
  file: Path
124
145
  files: Sequence[Path]
125
146
  file_deser: Callable[[Path], list[Any]]
126
- record_deser: (
147
+ record_deser: Optional[
127
148
  Callable[[dict], Any | list[Any]]
128
149
  | Callable[[dict, RelTableIndex], Any | list[Any]]
129
- )
150
+ ]
130
151
  table_unique_index: dict[type, list[str]]
131
152
 
132
153
 
@@ -134,7 +155,7 @@ class RelTableIndex:
134
155
  """An index of relational tables to find a record by its unique property."""
135
156
 
136
157
  def __init__(self, cls2index: Optional[dict[str, list[str]]] = None):
137
- self.table2rows: dict[str, dict[str, Any]] = defaultdict(dict)
158
+ self.table2rows: dict[str, dict[int, Any]] = defaultdict(dict)
138
159
  self.table2uniqindex2id: dict[str, dict[str, int]] = defaultdict(dict)
139
160
  self.cls2index = cls2index or {}
140
161
 
@@ -177,7 +198,7 @@ def load_data(
177
198
 
178
199
  reltable_index = RelTableIndex()
179
200
 
180
- for args in tqdm(table_data, disable=not verbose, desc="Loading data"):
201
+ for args in table_data:
181
202
  if "table" in args:
182
203
  tbls = [args["table"]]
183
204
  elif "tables" in args:
@@ -217,18 +238,23 @@ def load_data(
217
238
 
218
239
  assert "record_deser" in args
219
240
  deser = args["record_deser"]
241
+ if deser is None:
242
+ assert len(tbls) == 1
243
+ deser = get_dbclass_deser_func(tbls[0])
220
244
 
221
245
  sig = inspect.signature(deser)
222
246
  param_count = len(sig.parameters)
223
247
  if param_count == 1:
224
- records = [deser(row) for row in raw_records]
248
+ _deser = cast(Callable[[dict], Any], deser)
249
+ records = [_deser(row) for row in raw_records]
225
250
  else:
226
251
  assert param_count == 2
227
- records = [deser(row, reltable_index) for row in raw_records]
252
+ _deser = cast(Callable[[dict, RelTableIndex], Any], deser)
253
+ records = [_deser(row, reltable_index) for row in raw_records]
228
254
 
229
255
  for r in tqdm(
230
256
  records,
231
- desc=f"load {', '.join(tbl.__name__ for tbl in tbls)}",
257
+ desc=f"Load {', '.join(tbl.__tablename__ for tbl in tbls)}",
232
258
  disable=not verbose,
233
259
  ):
234
260
  if isinstance(r, Sequence):
@@ -263,6 +289,40 @@ def load_data(
263
289
  session.commit()
264
290
 
265
291
 
292
+ def load_data_from_dir(
293
+ engine: Engine,
294
+ create_db_and_tables: Callable[[], None],
295
+ data_dir: Path,
296
+ tables: Sequence[type | tuple[type, Callable[[dict], Any]]],
297
+ verbose: bool = False,
298
+ ):
299
+ """Load data into the database from a directory"""
300
+
301
+ load_args = []
302
+
303
+ for tbl in tables:
304
+ if isinstance(tbl, tuple):
305
+ tbl, record_deser = tbl
306
+ else:
307
+ record_deser = None
308
+
309
+ file = data_dir / (tbl.__tablename__ + ".json")
310
+ if not file.exists():
311
+ logger.warning(
312
+ "File {} does not exist, skipping loading for {}", file, tbl.__name__
313
+ )
314
+
315
+ load_args.append(
316
+ LoadTableDataArgs(
317
+ table=tbl,
318
+ file=file,
319
+ record_deser=record_deser,
320
+ )
321
+ )
322
+
323
+ load_data(engine, create_db_and_tables, load_args, verbose)
324
+
325
+
266
326
  def identity(x: T) -> T:
267
327
  """Identity function that returns the input unchanged."""
268
328
  return x
@@ -287,3 +347,23 @@ def get_classpath(type: Type | Callable) -> str:
287
347
  raise NotImplementedError(type)
288
348
 
289
349
  return path
350
+
351
+
352
+ def get_dbclass_deser_func(type: type[T]) -> Callable[[dict], T]:
353
+ """Get a deserializer function for a class in models.db."""
354
+ module, clsname = (
355
+ get_classpath(type)
356
+ .replace(".models.db.", ".models.data.")
357
+ .rsplit(".", maxsplit=1)
358
+ )
359
+ StructType = getattr(importlib.import_module(module), f"Create{clsname}")
360
+
361
+ def deser_func(obj: dict):
362
+ record = msgspec.json.decode(orjson.dumps(obj), type=StructType)
363
+ if hasattr(record, "_is_scp_updated"):
364
+ # Skip updating system-controlled properties
365
+ record._is_scp_updated = True
366
+
367
+ return record.to_db()
368
+
369
+ return deser_func
sera/models/_datatype.py CHANGED
@@ -172,6 +172,8 @@ class TsTypeWithDep:
172
172
  return value
173
173
  if self.type == "Date":
174
174
  return expr.ExprRawTypescript(f"{value.to_typescript()}.toISOString()")
175
+ if self.type == "Date | undefined":
176
+ return expr.ExprRawTypescript(f"{value.to_typescript()}?.toISOString()")
175
177
  if any(x.startswith("@.models.enum") for x in self.deps):
176
178
  # enum type, we don't need to do anything as we use strings for enums
177
179
  return value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sera-2
3
- Version: 1.20.13
3
+ Version: 1.20.14
4
4
  Summary:
5
5
  Author: Binh Vu
6
6
  Author-email: bvu687@gmail.com
@@ -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=lSnnNz-LzNaWmb5g7ic9bpJHl9IQd8u1GqH1kHFR3xk,6677
7
+ sera/libs/api_helper.py,sha256=lWg_-2N8cLU4jCV-lSebO2o1b2gI4Tal0ThUlYkN0nA,6689
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
@@ -26,14 +26,14 @@ sera/make/make_python_api.py,sha256=aOm8QSiXNLe4akiOx_KKsDCwLVwetRzuxOgZaWqEj0w,
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
28
  sera/make/make_typescript_model.py,sha256=CZc_LMigy7e6tP-6bJ5gHZlHYeyRtU_CNR4s64x51xE,71369
29
- sera/misc/__init__.py,sha256=BuwDvVl0nYAA_pYM0MAbRl8DqmuasZzidO46kNiu-5A,601
29
+ sera/misc/__init__.py,sha256=wj59TDqHMQUehdYChO179_NqjZmpqykfkoHaVOtNm_Y,651
30
30
  sera/misc/_formatter.py,sha256=aCGYL08l8f3aLODHxSocxBBwkRYEo3K1QzCDEn3suj0,1685
31
- sera/misc/_utils.py,sha256=bum84Eh6gVsax0Gl4SxkmD6hUWazEljxsV16Q3BNcN8,9019
31
+ sera/misc/_utils.py,sha256=yuWnlFA1LbiVKSS4RgnD18EtGCVCzQBj0vRZjxrbcEc,11072
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=RUKXwOjTr4YTloB0KQcOOAARXamoqMDXGtZ_xjldwD4,11095
36
+ sera/models/_datatype.py,sha256=_pmhCu3e1qQQuOio5joRzAkPOpIDoVRoDJ4_OrZl0Pw,11224
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
@@ -42,6 +42,6 @@ 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.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,,
45
+ sera_2-1.20.14.dist-info/METADATA,sha256=nCwKDJULbmdtdp-eXbklxPV1UHUUa1owIzuB8zLhpwo,937
46
+ sera_2-1.20.14.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
47
+ sera_2-1.20.14.dist-info/RECORD,,