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 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 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
 
@@ -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 executed.
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 executed.
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 dataclasses import dataclass
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 = importlib.import_module(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 identity(x: T) -> T:
328
- """Identity function that returns the input unchanged."""
329
- return x
330
-
331
-
332
- def get_classpath(type: Type | Callable) -> str:
333
- if type.__module__ == "builtins":
334
- return type.__qualname__
335
-
336
- if hasattr(type, "__qualname__"):
337
- return type.__module__ + "." + type.__qualname__
338
-
339
- # typically a class from the typing module
340
- if hasattr(type, "_name") and type._name is not None:
341
- path = type.__module__ + "." + type._name
342
- if path in TYPE_ALIASES:
343
- path = TYPE_ALIASES[path]
344
- elif hasattr(type, "__origin__") and hasattr(type.__origin__, "_name"):
345
- # found one case which is typing.Union
346
- path = type.__module__ + "." + type.__origin__._name
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
+ )
@@ -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 == "whole_number":
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
- "whole_number": Constraint("whole_number", ()),
61
+ "non_negative_number": Constraint("non_negative_number", ()),
62
62
  "positive_number": Constraint("positive_number", ()),
63
63
  "url": Constraint("url", ()),
64
64
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sera-2
3
- Version: 1.20.16
3
+ Version: 1.21.1
4
4
  Summary:
5
5
  Author: Binh Vu
6
6
  Author-email: bvu687@gmail.com
@@ -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=V6wug1QA5KD0FsQU9r1XQmLUTB85E6oIR2e12Fi1IE0,6457
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=AGTzKVSl-EsSOJlNKPOA1Io7pIxfq0SMXuumq1IExl0,14902
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=wj59TDqHMQUehdYChO179_NqjZmpqykfkoHaVOtNm_Y,651
29
+ sera/misc/__init__.py,sha256=L70kdXM5WmtMpgLlQISnGmrLgn241TQF0zFXFPi3Xzk,727
30
30
  sera/misc/_formatter.py,sha256=aCGYL08l8f3aLODHxSocxBBwkRYEo3K1QzCDEn3suj0,1685
31
- sera/misc/_utils.py,sha256=dYl1HqiqO4FWpef6jGKNjyjk03jfDvtQH0K6VJrk-8s,11093
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=RpWDU-TfCslXaMUaTG9utWbl5z8Z6nzvF_fhqlek6ew,1987
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.20.16.dist-info/METADATA,sha256=f8-UFa7OKC_NkgMMQSdiM-LaHM78f3m18r1ge3z3tlk,937
46
- sera_2-1.20.16.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
47
- sera_2-1.20.16.dist-info/RECORD,,
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,,