sera-2 1.20.12__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 +3 -3
- sera/misc/__init__.py +2 -0
- sera/misc/_utils.py +89 -9
- sera/models/_datatype.py +15 -0
- {sera_2-1.20.12.dist-info → sera_2-1.20.14.dist-info}/METADATA +1 -1
- {sera_2-1.20.12.dist-info → sera_2-1.20.14.dist-info}/RECORD +7 -7
- {sera_2-1.20.12.dist-info → sera_2-1.20.14.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 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:
|
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.
|
@@ -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,
|
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
|
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[
|
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
|
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
|
-
|
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
|
-
|
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"
|
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
@@ -78,6 +78,19 @@ class PyTypeWithDep:
|
|
78
78
|
return ("TypeConversion.to_float", "sera.libs.api_helper.TypeConversion")
|
79
79
|
if self.type == "bool":
|
80
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
|
+
)
|
81
94
|
else:
|
82
95
|
raise NotImplementedError()
|
83
96
|
|
@@ -159,6 +172,8 @@ class TsTypeWithDep:
|
|
159
172
|
return value
|
160
173
|
if self.type == "Date":
|
161
174
|
return expr.ExprRawTypescript(f"{value.to_typescript()}.toISOString()")
|
175
|
+
if self.type == "Date | undefined":
|
176
|
+
return expr.ExprRawTypescript(f"{value.to_typescript()}?.toISOString()")
|
162
177
|
if any(x.startswith("@.models.enum") for x in self.deps):
|
163
178
|
# enum type, we don't need to do anything as we use strings for enums
|
164
179
|
return value
|
@@ -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
|
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=
|
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=
|
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=
|
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.
|
46
|
-
sera_2-1.20.
|
47
|
-
sera_2-1.20.
|
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,,
|
File without changes
|