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.
Files changed (47) hide show
  1. {sera_2-1.21.0 → sera_2-1.21.2}/PKG-INFO +1 -1
  2. {sera_2-1.21.0 → sera_2-1.21.2}/pyproject.toml +1 -1
  3. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_dcg.py +46 -46
  4. {sera_2-1.21.0 → sera_2-1.21.2}/sera/misc/__init__.py +2 -0
  5. {sera_2-1.21.0 → sera_2-1.21.2}/sera/misc/_utils.py +40 -8
  6. {sera_2-1.21.0 → sera_2-1.21.2}/README.md +0 -0
  7. {sera_2-1.21.0 → sera_2-1.21.2}/sera/__init__.py +0 -0
  8. {sera_2-1.21.0 → sera_2-1.21.2}/sera/constants.py +0 -0
  9. {sera_2-1.21.0 → sera_2-1.21.2}/sera/exports/__init__.py +0 -0
  10. {sera_2-1.21.0 → sera_2-1.21.2}/sera/exports/schema.py +0 -0
  11. {sera_2-1.21.0 → sera_2-1.21.2}/sera/exports/test.py +0 -0
  12. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/__init__.py +0 -0
  13. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/api_helper.py +0 -0
  14. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/api_test_helper.py +0 -0
  15. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/base_orm.py +0 -0
  16. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/base_service.py +0 -0
  17. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/__init__.py +0 -0
  18. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_edge.py +0 -0
  19. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_flow.py +0 -0
  20. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_fn_signature.py +0 -0
  21. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_node.py +0 -0
  22. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_runtime.py +0 -0
  23. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/directed_computing_graph/_type_conversion.py +0 -0
  24. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/middlewares/__init__.py +0 -0
  25. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/middlewares/auth.py +0 -0
  26. {sera_2-1.21.0 → sera_2-1.21.2}/sera/libs/middlewares/uscp.py +0 -0
  27. {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/__init__.py +0 -0
  28. {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/__main__.py +0 -0
  29. {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/make_app.py +0 -0
  30. {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/make_python_api.py +0 -0
  31. {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/make_python_model.py +0 -0
  32. {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/make_python_services.py +0 -0
  33. {sera_2-1.21.0 → sera_2-1.21.2}/sera/make/make_typescript_model.py +0 -0
  34. {sera_2-1.21.0 → sera_2-1.21.2}/sera/misc/_formatter.py +0 -0
  35. {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/__init__.py +0 -0
  36. {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_class.py +0 -0
  37. {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_collection.py +0 -0
  38. {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_constraints.py +0 -0
  39. {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_datatype.py +0 -0
  40. {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_default.py +0 -0
  41. {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_enum.py +0 -0
  42. {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_module.py +0 -0
  43. {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_multi_lingual_string.py +0 -0
  44. {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_parse.py +0 -0
  45. {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_property.py +0 -0
  46. {sera_2-1.21.0 → sera_2-1.21.2}/sera/models/_schema.py +0 -0
  47. {sera_2-1.21.0 → sera_2-1.21.2}/sera/typing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sera-2
3
- Version: 1.21.0
3
+ Version: 1.21.2
4
4
  Summary:
5
5
  Author: Binh Vu
6
6
  Author-email: bvu687@gmail.com
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "sera-2"
3
- version = "1.21.0"
3
+ version = "1.21.2"
4
4
  description = ""
5
5
  authors = ["Binh Vu <bvu687@gmail.com>"]
6
6
  readme = "README.md"
@@ -1,18 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
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
- for u in self.graph.iter_nodes():
228
- if u.id in input:
229
- # user provided input should supersede the context
230
- n_provided_args = len(input[u.id])
231
- n_consumed_context = n_provided_args - len(u.required_args)
232
- else:
233
- n_consumed_context = 0
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
- runtimes[u.id] = NodeRuntime.from_node(self.graph, u, node_context)
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 u in self.graph.iter_nodes():
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
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
- node_context = tuple(
342
- (
343
- context[name]
344
- if name in context
345
- else u.required_context_default_args[name]
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
- for name in u.required_context[n_consumed_context:]
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 = importlib.import_module(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(importlib.import_module(module), f"Create{clsname}")
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, dcg: DirectedComputingGraph, tables: Sequence[type]
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(input={innode: record})
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