sera-2 1.20.16__py3-none-any.whl → 1.21.1__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/base_service.py +0 -2
- sera/libs/directed_computing_graph/_dcg.py +7 -16
- sera/misc/__init__.py +4 -0
- sera/misc/_utils.py +97 -48
- sera/models/_constraints.py +3 -3
- {sera_2-1.20.16.dist-info → sera_2-1.21.1.dist-info}/METADATA +1 -1
- {sera_2-1.20.16.dist-info → sera_2-1.21.1.dist-info}/RECORD +8 -8
- {sera_2-1.20.16.dist-info → sera_2-1.21.1.dist-info}/WHEEL +0 -0
sera/libs/base_service.py
CHANGED
@@ -128,8 +128,6 @@ class BaseAsyncService(Generic[ID, R]):
|
|
128
128
|
func.to_tsvector(getattr(self.orm_cls, field)).match(value)
|
129
129
|
)
|
130
130
|
|
131
|
-
print(">>>", q)
|
132
|
-
|
133
131
|
cq = select(func.count()).select_from(q.subquery())
|
134
132
|
rq = q.limit(limit).offset(offset)
|
135
133
|
records = self._process_result(await session.execute(rq)).scalars().all()
|
@@ -1,18 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import
|
4
|
-
import inspect
|
5
|
-
from dataclasses import dataclass
|
6
|
-
from enum import Enum
|
7
|
-
from typing import (
|
8
|
-
Annotated,
|
9
|
-
Any,
|
10
|
-
Awaitable,
|
11
|
-
Callable,
|
12
|
-
MutableSequence,
|
13
|
-
Optional,
|
14
|
-
Sequence,
|
15
|
-
)
|
3
|
+
from typing import Annotated, Any, Callable, MutableSequence, Optional, Sequence
|
16
4
|
|
17
5
|
from graph.retworkx import RetworkXStrDiGraph
|
18
6
|
|
@@ -201,7 +189,7 @@ class DirectedComputingGraph:
|
|
201
189
|
|
202
190
|
Args:
|
203
191
|
input: A dictionary mapping function identifiers to their input arguments.
|
204
|
-
output: A set of function identifiers that should be
|
192
|
+
output: A set of function identifiers that their output should be captured.
|
205
193
|
context: An optional context that can be a dictionary of functions or a single function.
|
206
194
|
"""
|
207
195
|
assert all(
|
@@ -296,7 +284,7 @@ class DirectedComputingGraph:
|
|
296
284
|
async def execute_async(
|
297
285
|
self,
|
298
286
|
input: dict[ComputeFnId, tuple],
|
299
|
-
output: set[str],
|
287
|
+
output: Optional[set[str]] = None,
|
300
288
|
context: Optional[
|
301
289
|
dict[str, Callable | Any] | Callable[[], dict[str, Any]]
|
302
290
|
] = None,
|
@@ -307,7 +295,7 @@ class DirectedComputingGraph:
|
|
307
295
|
|
308
296
|
Args:
|
309
297
|
input: A dictionary mapping function identifiers to their input arguments.
|
310
|
-
output: A set of function identifiers that should be
|
298
|
+
output: A set of function identifiers that their output should be captured.
|
311
299
|
context: An optional context that can be a dictionary of functions or a single function.
|
312
300
|
"""
|
313
301
|
assert all(
|
@@ -321,6 +309,9 @@ class DirectedComputingGraph:
|
|
321
309
|
else:
|
322
310
|
context = {k: v() if callable(v) else v for k, v in context.items()}
|
323
311
|
|
312
|
+
if output is None:
|
313
|
+
output = set()
|
314
|
+
|
324
315
|
# This is a quick reactive algorithm, we may be able to do it better.
|
325
316
|
# The idea is when all inputs of a function is available, we can execute a function.
|
326
317
|
# We assume that the memory is large enough to hold all the functions and their inputs
|
sera/misc/__init__.py
CHANGED
@@ -4,11 +4,13 @@ from sera.misc._utils import (
|
|
4
4
|
RelTableIndex,
|
5
5
|
assert_isinstance,
|
6
6
|
assert_not_null,
|
7
|
+
auto_import,
|
7
8
|
filter_duplication,
|
8
9
|
get_classpath,
|
9
10
|
identity,
|
10
11
|
load_data,
|
11
12
|
load_data_from_dir,
|
13
|
+
replay_events,
|
12
14
|
to_camel_case,
|
13
15
|
to_pascal_case,
|
14
16
|
to_snake_case,
|
@@ -30,4 +32,6 @@ __all__ = [
|
|
30
32
|
"LoadTableDataArgs",
|
31
33
|
"RelTableIndex",
|
32
34
|
"load_data_from_dir",
|
35
|
+
"replay_events",
|
36
|
+
"auto_import",
|
33
37
|
]
|
sera/misc/_utils.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import importlib
|
4
3
|
import inspect
|
5
4
|
import re
|
6
5
|
from collections import defaultdict
|
7
|
-
from
|
6
|
+
from importlib import import_module
|
8
7
|
from pathlib import Path
|
9
8
|
from typing import (
|
9
|
+
TYPE_CHECKING,
|
10
10
|
Any,
|
11
11
|
Callable,
|
12
12
|
Iterable,
|
@@ -23,10 +23,14 @@ import orjson
|
|
23
23
|
import serde.csv
|
24
24
|
import serde.json
|
25
25
|
from loguru import logger
|
26
|
-
from sqlalchemy import Engine, text
|
26
|
+
from sqlalchemy import Engine, select, text
|
27
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
27
28
|
from sqlalchemy.orm import Session
|
28
29
|
from tqdm import tqdm
|
29
30
|
|
31
|
+
if TYPE_CHECKING:
|
32
|
+
from sera.libs.directed_computing_graph import DirectedComputingGraph
|
33
|
+
|
30
34
|
T = TypeVar("T")
|
31
35
|
|
32
36
|
TYPE_ALIASES = {"typing.List": "list", "typing.Dict": "dict", "typing.Set": "set"}
|
@@ -74,7 +78,7 @@ reserved_keywords = {
|
|
74
78
|
def import_attr(attr_ident: str):
|
75
79
|
lst = attr_ident.rsplit(".", 1)
|
76
80
|
module, cls = lst
|
77
|
-
module =
|
81
|
+
module = import_module(module)
|
78
82
|
return getattr(module, cls)
|
79
83
|
|
80
84
|
|
@@ -138,6 +142,75 @@ def filter_duplication(
|
|
138
142
|
return new_lst
|
139
143
|
|
140
144
|
|
145
|
+
def identity(x: T) -> T:
|
146
|
+
"""Identity function that returns the input unchanged."""
|
147
|
+
return x
|
148
|
+
|
149
|
+
|
150
|
+
def get_classpath(type: Type | Callable) -> str:
|
151
|
+
if hasattr(type, "__module__") and type.__module__ == "builtins":
|
152
|
+
return type.__qualname__
|
153
|
+
|
154
|
+
if hasattr(type, "__qualname__"):
|
155
|
+
return type.__module__ + "." + type.__qualname__
|
156
|
+
|
157
|
+
# typically a class from the typing module
|
158
|
+
if hasattr(type, "_name") and type._name is not None:
|
159
|
+
path = type.__module__ + "." + type._name
|
160
|
+
if path in TYPE_ALIASES:
|
161
|
+
path = TYPE_ALIASES[path]
|
162
|
+
elif hasattr(type, "__origin__") and hasattr(type.__origin__, "_name"):
|
163
|
+
# found one case which is typing.Union
|
164
|
+
path = type.__module__ + "." + type.__origin__._name
|
165
|
+
else:
|
166
|
+
raise NotImplementedError(type)
|
167
|
+
|
168
|
+
return path
|
169
|
+
|
170
|
+
|
171
|
+
def get_dbclass_deser_func(type: type[T]) -> Callable[[dict], T]:
|
172
|
+
"""Get a deserializer function for a class in models.db."""
|
173
|
+
module, clsname = (
|
174
|
+
get_classpath(type)
|
175
|
+
.replace(".models.db.", ".models.data.")
|
176
|
+
.rsplit(".", maxsplit=1)
|
177
|
+
)
|
178
|
+
StructType = getattr(import_module(module), f"Create{clsname}")
|
179
|
+
|
180
|
+
def deser_func(obj: dict):
|
181
|
+
record = msgspec.json.decode(orjson.dumps(obj), type=StructType)
|
182
|
+
if hasattr(record, "_is_scp_updated"):
|
183
|
+
# Skip updating system-controlled properties
|
184
|
+
record._is_scp_updated = True
|
185
|
+
|
186
|
+
return record.to_db()
|
187
|
+
|
188
|
+
return deser_func
|
189
|
+
|
190
|
+
|
191
|
+
def auto_import(module: type):
|
192
|
+
"""Auto-import all submodules of a given module."""
|
193
|
+
mdir = Path(module.__path__[0])
|
194
|
+
for py_file in mdir.rglob("*.py"):
|
195
|
+
if py_file.name == "__init__.py":
|
196
|
+
continue
|
197
|
+
|
198
|
+
# Get the path of the submodule relative to the parent module's directory
|
199
|
+
relative_path = py_file.relative_to(mdir)
|
200
|
+
|
201
|
+
# Create the module import string from the file path
|
202
|
+
# e.g., for a file like `sub/module.py`, this creates `sub.module`
|
203
|
+
module_parts = list(relative_path.parts)
|
204
|
+
module_parts[-1] = relative_path.stem # remove .py extension
|
205
|
+
relative_module_name = ".".join(module_parts)
|
206
|
+
|
207
|
+
# Construct the full module path
|
208
|
+
full_module_path = f"{module.__name__}.{relative_module_name}"
|
209
|
+
|
210
|
+
# Dynamically import the module
|
211
|
+
import_module(full_module_path)
|
212
|
+
|
213
|
+
|
141
214
|
class LoadTableDataArgs(TypedDict, total=False):
|
142
215
|
table: type
|
143
216
|
tables: Sequence[type]
|
@@ -324,47 +397,23 @@ def load_data_from_dir(
|
|
324
397
|
load_data(engine, create_db_and_tables, load_args, verbose)
|
325
398
|
|
326
399
|
|
327
|
-
def
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
else:
|
348
|
-
raise NotImplementedError(type)
|
349
|
-
|
350
|
-
return path
|
351
|
-
|
352
|
-
|
353
|
-
def get_dbclass_deser_func(type: type[T]) -> Callable[[dict], T]:
|
354
|
-
"""Get a deserializer function for a class in models.db."""
|
355
|
-
module, clsname = (
|
356
|
-
get_classpath(type)
|
357
|
-
.replace(".models.db.", ".models.data.")
|
358
|
-
.rsplit(".", maxsplit=1)
|
359
|
-
)
|
360
|
-
StructType = getattr(importlib.import_module(module), f"Create{clsname}")
|
361
|
-
|
362
|
-
def deser_func(obj: dict):
|
363
|
-
record = msgspec.json.decode(orjson.dumps(obj), type=StructType)
|
364
|
-
if hasattr(record, "_is_scp_updated"):
|
365
|
-
# Skip updating system-controlled properties
|
366
|
-
record._is_scp_updated = True
|
367
|
-
|
368
|
-
return record.to_db()
|
369
|
-
|
370
|
-
return deser_func
|
400
|
+
async def replay_events(
|
401
|
+
engine: AsyncEngine,
|
402
|
+
dcg: DirectedComputingGraph,
|
403
|
+
tables: Sequence[type],
|
404
|
+
verbose: bool = False,
|
405
|
+
):
|
406
|
+
"""Replay the events in the DirectedComputingGraph. This is useful to re-run the workflows
|
407
|
+
that computes derived data after initial data loading.
|
408
|
+
"""
|
409
|
+
async with AsyncSession(engine, expire_on_commit=False) as session:
|
410
|
+
for tbl in tables:
|
411
|
+
innode = f"{tbl.__tablename__}.create"
|
412
|
+
for record in tqdm(
|
413
|
+
(await session.execute(select(tbl))).scalars(),
|
414
|
+
desc=f"Replaying events for {tbl.__tablename__}",
|
415
|
+
disable=not verbose,
|
416
|
+
):
|
417
|
+
await dcg.execute_async(
|
418
|
+
input={innode: (record,)}, context={"session": session}
|
419
|
+
)
|
sera/models/_constraints.py
CHANGED
@@ -10,8 +10,8 @@ ConstraintName = Literal[
|
|
10
10
|
"not_empty",
|
11
11
|
"username",
|
12
12
|
"password",
|
13
|
-
"whole_number",
|
14
13
|
"positive_number",
|
14
|
+
"non_negative_number",
|
15
15
|
]
|
16
16
|
|
17
17
|
|
@@ -34,7 +34,7 @@ class Constraint:
|
|
34
34
|
)
|
35
35
|
elif self.name == "password":
|
36
36
|
return "msgspec.Meta(min_length=8, max_length=40)"
|
37
|
-
elif self.name == "
|
37
|
+
elif self.name == "non_negative_number":
|
38
38
|
return "msgspec.Meta(ge=0)"
|
39
39
|
elif self.name == "positive_number":
|
40
40
|
return "msgspec.Meta(gt=0)"
|
@@ -58,7 +58,7 @@ predefined_constraints: dict[ConstraintName, Constraint] = {
|
|
58
58
|
"not_empty": Constraint("not_empty", ()),
|
59
59
|
"username": Constraint("username", ()),
|
60
60
|
"password": Constraint("password", ()),
|
61
|
-
"
|
61
|
+
"non_negative_number": Constraint("non_negative_number", ()),
|
62
62
|
"positive_number": Constraint("positive_number", ()),
|
63
63
|
"url": Constraint("url", ()),
|
64
64
|
}
|
@@ -7,9 +7,9 @@ sera/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
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
|
-
sera/libs/base_service.py,sha256=
|
10
|
+
sera/libs/base_service.py,sha256=NY-PLiaS1SXs2Pp3yVsdGnpJGUxKbHb0qzCCguhgPP4,6432
|
11
11
|
sera/libs/directed_computing_graph/__init__.py,sha256=xiF5_I1y9HtQ-cyq02iwkRYgEZvxBB8YIvysCHCLBco,1290
|
12
|
-
sera/libs/directed_computing_graph/_dcg.py,sha256=
|
12
|
+
sera/libs/directed_computing_graph/_dcg.py,sha256=miclcxik67CbJpb8slvUOcdWLQH67msqder7JpOoiTw,14870
|
13
13
|
sera/libs/directed_computing_graph/_edge.py,sha256=iBq6cpLWWyuD99QWTHVEh8naWUJrR4WJJuq5iuCrwHo,1026
|
14
14
|
sera/libs/directed_computing_graph/_flow.py,sha256=6v39yKPIDYrQ3KvFqjeAWs88-oQSnDTaED2F3LF2z_I,478
|
15
15
|
sera/libs/directed_computing_graph/_fn_signature.py,sha256=73iPUITcRKW0-l6sqjwMSk_FZnJESaKOmUKDGHTOh9Q,1598
|
@@ -26,13 +26,13 @@ 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=L70kdXM5WmtMpgLlQISnGmrLgn241TQF0zFXFPi3Xzk,727
|
30
30
|
sera/misc/_formatter.py,sha256=aCGYL08l8f3aLODHxSocxBBwkRYEo3K1QzCDEn3suj0,1685
|
31
|
-
sera/misc/_utils.py,sha256=
|
31
|
+
sera/misc/_utils.py,sha256=7vDhG1_hEdA4enTtL6gg5evBEg397kuxZ5ZtXC9PBWo,12939
|
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
|
-
sera/models/_constraints.py,sha256=
|
35
|
+
sera/models/_constraints.py,sha256=oRniaFXUsnQ_Q8lwXVcnBhvHG-Gw6eY31eIkFd1UXI4,2015
|
36
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
|
@@ -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.
|
46
|
-
sera_2-1.
|
47
|
-
sera_2-1.
|
45
|
+
sera_2-1.21.1.dist-info/METADATA,sha256=ojxFGoSzu_QLUSvtKfl6pE86Qd2eCg6QStYYiFbykGw,936
|
46
|
+
sera_2-1.21.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
47
|
+
sera_2-1.21.1.dist-info/RECORD,,
|
File without changes
|