sera-2 1.21.0__tar.gz → 1.21.2__tar.gz
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_2-1.21.0 → sera_2-1.21.2}/PKG-INFO +1 -1
- {sera_2-1.21.0 → sera_2-1.21.2}/pyproject.toml +1 -1
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_dcg.py +46 -46
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/misc/__init__.py +2 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/misc/_utils.py +40 -8
- {sera_2-1.21.0 → sera_2-1.21.2}/README.md +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/__init__.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/constants.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/exports/__init__.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/exports/schema.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/exports/test.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/__init__.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/api_helper.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/api_test_helper.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/base_orm.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/base_service.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/__init__.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_edge.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_flow.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_fn_signature.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_node.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_runtime.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_type_conversion.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/middlewares/__init__.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/middlewares/auth.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/middlewares/uscp.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/__init__.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/__main__.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/make_app.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/make_python_api.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/make_python_model.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/make_python_services.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/make_typescript_model.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/misc/_formatter.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/__init__.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_class.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_collection.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_constraints.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_datatype.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_default.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_enum.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_module.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_multi_lingual_string.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_parse.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_property.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_schema.py +0 -0
- {sera_2-1.21.0 → sera_2-1.21.2}/sera/typing.py +0 -0
@@ -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
|
|
@@ -53,6 +41,11 @@ class DirectedComputingGraph:
|
|
53
41
|
):
|
54
42
|
self.graph = graph
|
55
43
|
self.type_service = type_service
|
44
|
+
self.node2descendants: dict[str, list[DCGNode]] = {}
|
45
|
+
|
46
|
+
for u in graph.iter_nodes():
|
47
|
+
self.node2descendants[u.id] = graph.descendants(u.id)
|
48
|
+
self.node2descendants[u.id].append(u)
|
56
49
|
|
57
50
|
@staticmethod
|
58
51
|
def from_flows(
|
@@ -191,7 +184,7 @@ class DirectedComputingGraph:
|
|
191
184
|
def execute(
|
192
185
|
self,
|
193
186
|
input: dict[ComputeFnId, tuple],
|
194
|
-
output: set[str],
|
187
|
+
output: Optional[set[str]] = None,
|
195
188
|
context: Optional[
|
196
189
|
dict[str, Callable | Any] | Callable[[], dict[str, Any]]
|
197
190
|
] = None,
|
@@ -215,6 +208,9 @@ class DirectedComputingGraph:
|
|
215
208
|
else:
|
216
209
|
context = {k: v() if callable(v) else v for k, v in context.items()}
|
217
210
|
|
211
|
+
if output is None:
|
212
|
+
output = set()
|
213
|
+
|
218
214
|
# This is a quick reactive algorithm, we may be able to do it better.
|
219
215
|
# The idea is when all inputs of a function is available, we can execute a function.
|
220
216
|
# We assume that the memory is large enough to hold all the functions and their inputs
|
@@ -223,25 +219,25 @@ class DirectedComputingGraph:
|
|
223
219
|
# we execute the computing nodes
|
224
220
|
# when it's finished, we put the outgoing edges into a stack.
|
225
221
|
runtimes: dict[NodeId, NodeRuntime] = {}
|
222
|
+
for id in input.keys():
|
223
|
+
for u in self.node2descendants[id]:
|
224
|
+
if u.id in input:
|
225
|
+
# user provided input should supersede the context
|
226
|
+
n_provided_args = len(input[u.id])
|
227
|
+
n_consumed_context = n_provided_args - len(u.required_args)
|
228
|
+
else:
|
229
|
+
n_consumed_context = 0
|
226
230
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
node_context = tuple(
|
236
|
-
(
|
237
|
-
context[name]
|
238
|
-
if name in context
|
239
|
-
else u.required_context_default_args[name]
|
231
|
+
node_context = tuple(
|
232
|
+
(
|
233
|
+
context[name]
|
234
|
+
if name in context
|
235
|
+
else u.required_context_default_args[name]
|
236
|
+
)
|
237
|
+
for name in u.required_context[n_consumed_context:]
|
240
238
|
)
|
241
|
-
for name in u.required_context[n_consumed_context:]
|
242
|
-
)
|
243
239
|
|
244
|
-
|
240
|
+
runtimes[u.id] = NodeRuntime.from_node(self.graph, u, node_context)
|
245
241
|
stack: list[NodeId] = []
|
246
242
|
|
247
243
|
for id, args in input.items():
|
@@ -296,7 +292,7 @@ class DirectedComputingGraph:
|
|
296
292
|
async def execute_async(
|
297
293
|
self,
|
298
294
|
input: dict[ComputeFnId, tuple],
|
299
|
-
output: set[str],
|
295
|
+
output: Optional[set[str]] = None,
|
300
296
|
context: Optional[
|
301
297
|
dict[str, Callable | Any] | Callable[[], dict[str, Any]]
|
302
298
|
] = None,
|
@@ -321,6 +317,9 @@ class DirectedComputingGraph:
|
|
321
317
|
else:
|
322
318
|
context = {k: v() if callable(v) else v for k, v in context.items()}
|
323
319
|
|
320
|
+
if output is None:
|
321
|
+
output = set()
|
322
|
+
|
324
323
|
# This is a quick reactive algorithm, we may be able to do it better.
|
325
324
|
# The idea is when all inputs of a function is available, we can execute a function.
|
326
325
|
# We assume that the memory is large enough to hold all the functions and their inputs
|
@@ -330,24 +329,25 @@ class DirectedComputingGraph:
|
|
330
329
|
# when it's finished, we put the outgoing edges into a stack.
|
331
330
|
runtimes: dict[NodeId, NodeRuntime] = {}
|
332
331
|
|
333
|
-
for
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
332
|
+
for id in input.keys():
|
333
|
+
for u in self.node2descendants[id]:
|
334
|
+
if u.id in input:
|
335
|
+
# user provided input should supersede the context
|
336
|
+
n_provided_args = len(input[u.id])
|
337
|
+
n_consumed_context = n_provided_args - len(u.required_args)
|
338
|
+
else:
|
339
|
+
n_consumed_context = 0
|
340
340
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
341
|
+
node_context = tuple(
|
342
|
+
(
|
343
|
+
context[name]
|
344
|
+
if name in context
|
345
|
+
else u.required_context_default_args[name]
|
346
|
+
)
|
347
|
+
for name in u.required_context[n_consumed_context:]
|
346
348
|
)
|
347
|
-
|
348
|
-
)
|
349
|
+
runtimes[u.id] = NodeRuntime.from_node(self.graph, u, node_context)
|
349
350
|
|
350
|
-
runtimes[u.id] = NodeRuntime.from_node(self.graph, u, node_context)
|
351
351
|
stack: list[NodeId] = []
|
352
352
|
|
353
353
|
for id, args in input.items():
|
@@ -4,6 +4,7 @@ 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,
|
@@ -32,4 +33,5 @@ __all__ = [
|
|
32
33
|
"RelTableIndex",
|
33
34
|
"load_data_from_dir",
|
34
35
|
"replay_events",
|
36
|
+
"auto_import",
|
35
37
|
]
|
@@ -1,9 +1,9 @@
|
|
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
|
6
|
+
from importlib import import_module
|
7
7
|
from pathlib import Path
|
8
8
|
from typing import (
|
9
9
|
TYPE_CHECKING,
|
@@ -23,7 +23,8 @@ 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
|
|
@@ -77,7 +78,7 @@ reserved_keywords = {
|
|
77
78
|
def import_attr(attr_ident: str):
|
78
79
|
lst = attr_ident.rsplit(".", 1)
|
79
80
|
module, cls = lst
|
80
|
-
module =
|
81
|
+
module = import_module(module)
|
81
82
|
return getattr(module, cls)
|
82
83
|
|
83
84
|
|
@@ -147,7 +148,7 @@ def identity(x: T) -> T:
|
|
147
148
|
|
148
149
|
|
149
150
|
def get_classpath(type: Type | Callable) -> str:
|
150
|
-
if type.__module__ == "builtins":
|
151
|
+
if hasattr(type, "__module__") and type.__module__ == "builtins":
|
151
152
|
return type.__qualname__
|
152
153
|
|
153
154
|
if hasattr(type, "__qualname__"):
|
@@ -174,7 +175,7 @@ def get_dbclass_deser_func(type: type[T]) -> Callable[[dict], T]:
|
|
174
175
|
.replace(".models.db.", ".models.data.")
|
175
176
|
.rsplit(".", maxsplit=1)
|
176
177
|
)
|
177
|
-
StructType = getattr(
|
178
|
+
StructType = getattr(import_module(module), f"Create{clsname}")
|
178
179
|
|
179
180
|
def deser_func(obj: dict):
|
180
181
|
record = msgspec.json.decode(orjson.dumps(obj), type=StructType)
|
@@ -187,6 +188,29 @@ def get_dbclass_deser_func(type: type[T]) -> Callable[[dict], T]:
|
|
187
188
|
return deser_func
|
188
189
|
|
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
|
+
|
190
214
|
class LoadTableDataArgs(TypedDict, total=False):
|
191
215
|
table: type
|
192
216
|
tables: Sequence[type]
|
@@ -374,7 +398,10 @@ def load_data_from_dir(
|
|
374
398
|
|
375
399
|
|
376
400
|
async def replay_events(
|
377
|
-
engine: AsyncEngine,
|
401
|
+
engine: AsyncEngine,
|
402
|
+
dcg: DirectedComputingGraph,
|
403
|
+
tables: Sequence[type],
|
404
|
+
verbose: bool = False,
|
378
405
|
):
|
379
406
|
"""Replay the events in the DirectedComputingGraph. This is useful to re-run the workflows
|
380
407
|
that computes derived data after initial data loading.
|
@@ -383,7 +410,12 @@ async def replay_events(
|
|
383
410
|
for tbl in tables:
|
384
411
|
innode = f"{tbl.__tablename__}.create"
|
385
412
|
for record in tqdm(
|
386
|
-
session.execute(select(tbl)).scalars(),
|
413
|
+
(await session.execute(select(tbl))).scalars(),
|
387
414
|
desc=f"Replaying events for {tbl.__tablename__}",
|
415
|
+
disable=not verbose,
|
388
416
|
):
|
389
|
-
await dcg.execute_async(
|
417
|
+
await dcg.execute_async(
|
418
|
+
input={innode: (record,)}, context={"session": session}
|
419
|
+
)
|
420
|
+
|
421
|
+
await session.commit()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|