moose-lib 0.4.212__tar.gz → 0.4.214__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.
- {moose_lib-0.4.212 → moose_lib-0.4.214}/PKG-INFO +2 -1
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/dmv2.py +178 -9
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/internal.py +32 -2
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib.egg-info/PKG-INFO +2 -1
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib.egg-info/requires.txt +1 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/setup.py +1 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/README.md +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/__init__.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/blocks.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/clients/__init__.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/clients/redis_client.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/commons.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/data_models.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/dmv2-serializer.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/main.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/query_param.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/streaming/__init__.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/streaming/streaming_function_runner.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib/tasks.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib.egg-info/SOURCES.txt +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib.egg-info/dependency_links.txt +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/moose_lib.egg-info/top_level.txt +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/setup.cfg +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/tests/__init__.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/tests/conftest.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/tests/test_moose.py +0 -0
- {moose_lib-0.4.212 → moose_lib-0.4.214}/tests/test_redis_client.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: moose_lib
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.214
|
4
4
|
Home-page: https://www.fiveonefour.com/moose
|
5
5
|
Author: Fiveonefour Labs Inc.
|
6
6
|
Author-email: support@fiveonefour.com
|
@@ -11,6 +11,7 @@ Requires-Dist: pydantic==2.10.6
|
|
11
11
|
Requires-Dist: temporalio==1.9.0
|
12
12
|
Requires-Dist: kafka-python-ng==2.2.2
|
13
13
|
Requires-Dist: redis==6.2.0
|
14
|
+
Requires-Dist: humanfriendly==10.0
|
14
15
|
Dynamic: author
|
15
16
|
Dynamic: author-email
|
16
17
|
Dynamic: description
|
@@ -10,7 +10,7 @@ of data infrastructure using Python and Pydantic models.
|
|
10
10
|
"""
|
11
11
|
import dataclasses
|
12
12
|
import datetime
|
13
|
-
from typing import Any, Generic, Optional, TypeVar, Callable, Union, Literal
|
13
|
+
from typing import Any, Generic, Optional, TypeVar, Callable, Union, Literal, Awaitable
|
14
14
|
from pydantic import BaseModel, ConfigDict, AliasGenerator
|
15
15
|
from pydantic.alias_generators import to_camel
|
16
16
|
from pydantic.fields import FieldInfo
|
@@ -23,11 +23,24 @@ _streams: dict[str, "Stream"] = {}
|
|
23
23
|
_ingest_apis: dict[str, "IngestApi"] = {}
|
24
24
|
_egress_apis: dict[str, "ConsumptionApi"] = {}
|
25
25
|
_sql_resources: dict[str, "SqlResource"] = {}
|
26
|
+
_workflows: dict[str, "Workflow"] = {}
|
26
27
|
|
27
28
|
T = TypeVar('T', bound=BaseModel)
|
28
29
|
U = TypeVar('U', bound=BaseModel)
|
30
|
+
T_none = TypeVar('T_none', bound=Union[BaseModel, None])
|
31
|
+
U_none = TypeVar('U_none', bound=Union[BaseModel, None])
|
29
32
|
type ZeroOrMany[T] = Union[T, list[T], None]
|
30
33
|
|
34
|
+
type TaskRunFunc[T_none, U_none] = Union[
|
35
|
+
# Case 1: No input, no output
|
36
|
+
Callable[[], None],
|
37
|
+
# Case 2: No input, with output
|
38
|
+
Callable[[], Union[U_none, Awaitable[U_none]]],
|
39
|
+
# Case 3: With input, no output
|
40
|
+
Callable[[T_none], None],
|
41
|
+
# Case 4: With input, with output
|
42
|
+
Callable[[T_none], Union[U_none, Awaitable[U_none]]]
|
43
|
+
]
|
31
44
|
|
32
45
|
class Columns(Generic[T]):
|
33
46
|
"""Provides runtime checked column name access for Moose resources.
|
@@ -422,6 +435,7 @@ class IngestConfigWithDestination[T: BaseModel]:
|
|
422
435
|
metadata: Optional metadata for the ingestion configuration.
|
423
436
|
"""
|
424
437
|
destination: Stream[T]
|
438
|
+
dead_letter_queue: Optional[DeadLetterQueue[T]] = None
|
425
439
|
version: Optional[str] = None
|
426
440
|
metadata: Optional[dict] = None
|
427
441
|
|
@@ -443,6 +457,7 @@ class IngestPipelineConfig(BaseModel):
|
|
443
457
|
table: bool | OlapConfig = True
|
444
458
|
stream: bool | StreamConfig = True
|
445
459
|
ingest: bool | IngestConfig = True
|
460
|
+
dead_letter_queue: bool | StreamConfig = True
|
446
461
|
version: Optional[str] = None
|
447
462
|
metadata: Optional[dict] = None
|
448
463
|
|
@@ -491,6 +506,7 @@ class IngestPipeline(TypedMooseResource, Generic[T]):
|
|
491
506
|
table: The created `OlapTable` instance, if configured.
|
492
507
|
stream: The created `Stream` instance, if configured.
|
493
508
|
ingest_api: The created `IngestApi` instance, if configured.
|
509
|
+
dead_letter_queue: The created `DeadLetterQueue` instance, if configured.
|
494
510
|
columns (Columns[T]): Helper for accessing data field names safely.
|
495
511
|
name (str): The base name of the pipeline.
|
496
512
|
model_type (type[T]): The Pydantic model associated with this pipeline.
|
@@ -498,6 +514,7 @@ class IngestPipeline(TypedMooseResource, Generic[T]):
|
|
498
514
|
table: Optional[OlapTable[T]] = None
|
499
515
|
stream: Optional[Stream[T]] = None
|
500
516
|
ingest_api: Optional[IngestApi[T]] = None
|
517
|
+
dead_letter_queue: Optional[DeadLetterQueue[T]] = None
|
501
518
|
metadata: Optional[dict] = None
|
502
519
|
|
503
520
|
def get_table(self) -> OlapTable[T]:
|
@@ -561,6 +578,12 @@ class IngestPipeline(TypedMooseResource, Generic[T]):
|
|
561
578
|
stream_config.version = config.version
|
562
579
|
stream_config.metadata = stream_metadata
|
563
580
|
self.stream = Stream(name, stream_config, t=self._t)
|
581
|
+
if config.dead_letter_queue:
|
582
|
+
stream_config = StreamConfig() if config.dead_letter_queue is True else config.dead_letter_queue
|
583
|
+
if config.version:
|
584
|
+
stream_config.version = config.version
|
585
|
+
stream_config.metadata = stream_metadata
|
586
|
+
self.dead_letter_queue = DeadLetterQueue(f"{name}DeadLetterQueue", stream_config, t=self._t)
|
564
587
|
if config.ingest:
|
565
588
|
if self.stream is None:
|
566
589
|
raise ValueError("Ingest API needs a stream to write to.")
|
@@ -570,6 +593,8 @@ class IngestPipeline(TypedMooseResource, Generic[T]):
|
|
570
593
|
ingest_config_dict["destination"] = self.stream
|
571
594
|
if config.version:
|
572
595
|
ingest_config_dict["version"] = config.version
|
596
|
+
if self.dead_letter_queue:
|
597
|
+
ingest_config_dict["dead_letter_queue"] = self.dead_letter_queue
|
573
598
|
ingest_config_dict["metadata"] = ingest_metadata
|
574
599
|
ingest_config = IngestConfigWithDestination(**ingest_config_dict)
|
575
600
|
self.ingest_api = IngestApi(name, ingest_config, t=self._t)
|
@@ -694,13 +719,13 @@ class SqlResource:
|
|
694
719
|
pushes_data_to: list[Union[OlapTable, "SqlResource"]]
|
695
720
|
|
696
721
|
def __init__(
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
722
|
+
self,
|
723
|
+
name: str,
|
724
|
+
setup: list[str],
|
725
|
+
teardown: list[str],
|
726
|
+
pulls_data_from: Optional[list[Union[OlapTable, "SqlResource"]]] = None,
|
727
|
+
pushes_data_to: Optional[list[Union[OlapTable, "SqlResource"]]] = None,
|
728
|
+
metadata: dict = None
|
704
729
|
):
|
705
730
|
self.name = name
|
706
731
|
self.setup = setup
|
@@ -721,7 +746,8 @@ class View(SqlResource):
|
|
721
746
|
that this view depends on.
|
722
747
|
"""
|
723
748
|
|
724
|
-
def __init__(self, name: str, select_statement: str, base_tables: list[Union[OlapTable, SqlResource]],
|
749
|
+
def __init__(self, name: str, select_statement: str, base_tables: list[Union[OlapTable, SqlResource]],
|
750
|
+
metadata: dict = None):
|
725
751
|
setup = [
|
726
752
|
f"CREATE VIEW IF NOT EXISTS {name} AS {select_statement}".strip()
|
727
753
|
]
|
@@ -810,3 +836,146 @@ class MaterializedView(SqlResource, BaseTypedResource, Generic[T]):
|
|
810
836
|
|
811
837
|
self.target_table = target_table
|
812
838
|
self.config = options
|
839
|
+
|
840
|
+
|
841
|
+
@dataclasses.dataclass
|
842
|
+
class TaskConfig(Generic[T_none, U_none]):
|
843
|
+
"""Configuration for a Task.
|
844
|
+
|
845
|
+
Attributes:
|
846
|
+
run: The handler function that executes the task logic.
|
847
|
+
on_complete: Optional list of tasks to run after this task completes.
|
848
|
+
timeout: Optional timeout string (e.g. "5m", "1h").
|
849
|
+
retries: Optional number of retry attempts.
|
850
|
+
"""
|
851
|
+
run: TaskRunFunc[T_none, U_none]
|
852
|
+
on_complete: Optional[list["Task[U_none, Any]"]] = None
|
853
|
+
timeout: Optional[str] = None
|
854
|
+
retries: Optional[int] = None
|
855
|
+
|
856
|
+
|
857
|
+
class Task(TypedMooseResource, Generic[T_none, U_none]):
|
858
|
+
"""Represents a task that can be executed as part of a workflow.
|
859
|
+
|
860
|
+
Tasks are the basic unit of work in a workflow, with typed input and output.
|
861
|
+
They can be chained together using the on_complete configuration.
|
862
|
+
|
863
|
+
Args:
|
864
|
+
name: The name of the task.
|
865
|
+
config: Configuration specifying the task's behavior.
|
866
|
+
t: The Pydantic model defining the task's input schema
|
867
|
+
(passed via `Task[InputModel, OutputModel](...)`).
|
868
|
+
OutputModel can be None for tasks that don't return a value.
|
869
|
+
|
870
|
+
Attributes:
|
871
|
+
config (TaskConfig[T, U]): The configuration for this task.
|
872
|
+
columns (Columns[T]): Helper for accessing input field names safely.
|
873
|
+
name (str): The name of the task.
|
874
|
+
model_type (type[T]): The Pydantic model associated with this task's input.
|
875
|
+
"""
|
876
|
+
config: TaskConfig[T_none, U_none]
|
877
|
+
|
878
|
+
def __init__(self, name: str, config: TaskConfig[T_none, U_none], **kwargs):
|
879
|
+
super().__init__()
|
880
|
+
self._set_type(name, self._get_type(kwargs))
|
881
|
+
self.config = config
|
882
|
+
|
883
|
+
@classmethod
|
884
|
+
def _get_type(cls, keyword_args: dict):
|
885
|
+
t = keyword_args.get('t')
|
886
|
+
if t is None:
|
887
|
+
raise ValueError(f"Use `{cls.__name__}[T, U](name='...')` to supply both input and output types")
|
888
|
+
if not isinstance(t, tuple) or len(t) != 2:
|
889
|
+
raise ValueError(f"Use `{cls.__name__}[T, U](name='...')` to supply both input and output types")
|
890
|
+
|
891
|
+
input_type, output_type = t
|
892
|
+
if input_type is not None and (not isinstance(input_type, type) or not issubclass(input_type, BaseModel)):
|
893
|
+
raise ValueError(f"Input type {input_type} is not a Pydantic model or None")
|
894
|
+
if output_type is not None and (not isinstance(output_type, type) or not issubclass(output_type, BaseModel)):
|
895
|
+
raise ValueError(f"Output type {output_type} is not a Pydantic model or None")
|
896
|
+
return t
|
897
|
+
|
898
|
+
def _set_type(self, name: str, t: tuple[type[T_none], type[U_none]]):
|
899
|
+
input_type, output_type = t
|
900
|
+
self._t = input_type
|
901
|
+
self._u = output_type
|
902
|
+
self.name = name
|
903
|
+
|
904
|
+
|
905
|
+
@dataclasses.dataclass
|
906
|
+
class WorkflowConfig:
|
907
|
+
"""Configuration for a workflow.
|
908
|
+
|
909
|
+
Attributes:
|
910
|
+
starting_task: The first task to execute in the workflow.
|
911
|
+
retries: Optional number of retry attempts for the entire workflow.
|
912
|
+
timeout: Optional timeout string for the entire workflow.
|
913
|
+
schedule: Optional cron-like schedule string for recurring execution.
|
914
|
+
"""
|
915
|
+
starting_task: Task[Any, Any]
|
916
|
+
retries: Optional[int] = None
|
917
|
+
timeout: Optional[str] = None
|
918
|
+
schedule: Optional[str] = None
|
919
|
+
|
920
|
+
|
921
|
+
class Workflow:
|
922
|
+
"""Represents a workflow composed of one or more tasks.
|
923
|
+
|
924
|
+
Workflows define a sequence of tasks to be executed, with optional
|
925
|
+
scheduling, retries, and timeouts at the workflow level.
|
926
|
+
|
927
|
+
Args:
|
928
|
+
name: The name of the workflow.
|
929
|
+
config: Configuration specifying the workflow's behavior.
|
930
|
+
|
931
|
+
Attributes:
|
932
|
+
name (str): The name of the workflow.
|
933
|
+
config (WorkflowConfig): The configuration for this workflow.
|
934
|
+
"""
|
935
|
+
def __init__(self, name: str, config: WorkflowConfig):
|
936
|
+
self.name = name
|
937
|
+
self.config = config
|
938
|
+
# Register the workflow in the internal registry
|
939
|
+
_workflows[name] = self
|
940
|
+
|
941
|
+
def get_task_names(self) -> list[str]:
|
942
|
+
"""Get a list of all task names in this workflow.
|
943
|
+
|
944
|
+
Returns:
|
945
|
+
list[str]: List of task names in the workflow, including all child tasks
|
946
|
+
"""
|
947
|
+
def collect_task_names(task: Task) -> list[str]:
|
948
|
+
names = [task.name]
|
949
|
+
if task.config.on_complete:
|
950
|
+
for child in task.config.on_complete:
|
951
|
+
names.extend(collect_task_names(child))
|
952
|
+
return names
|
953
|
+
|
954
|
+
return collect_task_names(self.config.starting_task)
|
955
|
+
|
956
|
+
def get_task(self, task_name: str) -> Optional[Task]:
|
957
|
+
"""Find a task in this workflow by name.
|
958
|
+
|
959
|
+
Args:
|
960
|
+
task_name: The name of the task to find
|
961
|
+
|
962
|
+
Returns:
|
963
|
+
Optional[Task]: The task if found, None otherwise
|
964
|
+
"""
|
965
|
+
def find_task(task: Task) -> Optional[Task]:
|
966
|
+
if task.name == task_name:
|
967
|
+
return task
|
968
|
+
if task.config.on_complete:
|
969
|
+
for child in task.config.on_complete:
|
970
|
+
found = find_task(child)
|
971
|
+
if found:
|
972
|
+
return found
|
973
|
+
return None
|
974
|
+
|
975
|
+
return find_task(self.config.starting_task)
|
976
|
+
|
977
|
+
def _get_workflows() -> dict[str, Workflow]:
|
978
|
+
return _workflows
|
979
|
+
|
980
|
+
def _get_workflow(name: str) -> Optional[Workflow]:
|
981
|
+
return _workflows.get(name)
|
@@ -11,7 +11,7 @@ from typing import Literal, Optional, List, Any
|
|
11
11
|
from pydantic import BaseModel, ConfigDict, AliasGenerator
|
12
12
|
import json
|
13
13
|
from .data_models import Column, _to_columns
|
14
|
-
from moose_lib.dmv2 import _tables, _streams, _ingest_apis, _egress_apis, SqlResource, _sql_resources
|
14
|
+
from moose_lib.dmv2 import _tables, _streams, _ingest_apis, _egress_apis, SqlResource, _sql_resources, _workflows
|
15
15
|
from moose_lib.dmv2 import OlapTable, View, MaterializedView
|
16
16
|
from pydantic.alias_generators import to_camel
|
17
17
|
from pydantic.json_schema import JsonSchemaValue
|
@@ -110,6 +110,7 @@ class IngestApiConfig(BaseModel):
|
|
110
110
|
name: str
|
111
111
|
columns: List[Column]
|
112
112
|
write_to: Target
|
113
|
+
dead_letter_queue: Optional[str] = None
|
113
114
|
version: Optional[str] = None
|
114
115
|
metadata: Optional[dict] = None
|
115
116
|
|
@@ -131,6 +132,22 @@ class EgressApiConfig(BaseModel):
|
|
131
132
|
version: Optional[str] = None
|
132
133
|
metadata: Optional[dict] = None
|
133
134
|
|
135
|
+
class WorkflowJson(BaseModel):
|
136
|
+
"""Internal representation of a workflow configuration for serialization.
|
137
|
+
|
138
|
+
Attributes:
|
139
|
+
name: Name of the workflow.
|
140
|
+
retries: Optional number of retry attempts for the entire workflow.
|
141
|
+
timeout: Optional timeout string for the entire workflow.
|
142
|
+
schedule: Optional cron-like schedule string for recurring execution.
|
143
|
+
"""
|
144
|
+
model_config = model_config
|
145
|
+
|
146
|
+
name: str
|
147
|
+
retries: Optional[int] = None
|
148
|
+
timeout: Optional[str] = None
|
149
|
+
schedule: Optional[str] = None
|
150
|
+
|
134
151
|
class InfrastructureSignatureJson(BaseModel):
|
135
152
|
"""Represents the unique signature of an infrastructure component (Table, Topic, etc.).
|
136
153
|
|
@@ -175,6 +192,7 @@ class InfrastructureMap(BaseModel):
|
|
175
192
|
ingest_apis: Dictionary mapping ingest API names to their configurations.
|
176
193
|
egress_apis: Dictionary mapping egress API names to their configurations.
|
177
194
|
sql_resources: Dictionary mapping SQL resource names to their configurations.
|
195
|
+
workflows: Dictionary mapping workflow names to their configurations.
|
178
196
|
"""
|
179
197
|
model_config = model_config
|
180
198
|
|
@@ -183,6 +201,7 @@ class InfrastructureMap(BaseModel):
|
|
183
201
|
ingest_apis: dict[str, IngestApiConfig]
|
184
202
|
egress_apis: dict[str, EgressApiConfig]
|
185
203
|
sql_resources: dict[str, SqlResourceConfig]
|
204
|
+
workflows: dict[str, WorkflowJson]
|
186
205
|
|
187
206
|
|
188
207
|
def _map_sql_resource_ref(r: Any) -> InfrastructureSignatureJson:
|
@@ -233,6 +252,7 @@ def to_infra_map() -> dict:
|
|
233
252
|
ingest_apis = {}
|
234
253
|
egress_apis = {}
|
235
254
|
sql_resources = {}
|
255
|
+
workflows = {}
|
236
256
|
|
237
257
|
for name, table in _tables.items():
|
238
258
|
engine = table.config.engine
|
@@ -287,6 +307,7 @@ def to_infra_map() -> dict:
|
|
287
307
|
name=api.config.destination.name
|
288
308
|
),
|
289
309
|
metadata=getattr(api, "metadata", None),
|
310
|
+
dead_letter_queue=api.config.dead_letter_queue.name
|
290
311
|
)
|
291
312
|
|
292
313
|
for name, api in _egress_apis.items():
|
@@ -308,12 +329,21 @@ def to_infra_map() -> dict:
|
|
308
329
|
metadata=getattr(resource, "metadata", None),
|
309
330
|
)
|
310
331
|
|
332
|
+
for name, workflow in _workflows.items():
|
333
|
+
workflows[name] = WorkflowJson(
|
334
|
+
name=workflow.name,
|
335
|
+
retries=workflow.config.retries,
|
336
|
+
timeout=workflow.config.timeout,
|
337
|
+
schedule=workflow.config.schedule,
|
338
|
+
)
|
339
|
+
|
311
340
|
infra_map = InfrastructureMap(
|
312
341
|
tables=tables,
|
313
342
|
topics=topics,
|
314
343
|
ingest_apis=ingest_apis,
|
315
344
|
egress_apis=egress_apis,
|
316
|
-
sql_resources=sql_resources
|
345
|
+
sql_resources=sql_resources,
|
346
|
+
workflows=workflows
|
317
347
|
)
|
318
348
|
|
319
349
|
return infra_map.model_dump(by_alias=True)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: moose_lib
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.214
|
4
4
|
Home-page: https://www.fiveonefour.com/moose
|
5
5
|
Author: Fiveonefour Labs Inc.
|
6
6
|
Author-email: support@fiveonefour.com
|
@@ -11,6 +11,7 @@ Requires-Dist: pydantic==2.10.6
|
|
11
11
|
Requires-Dist: temporalio==1.9.0
|
12
12
|
Requires-Dist: kafka-python-ng==2.2.2
|
13
13
|
Requires-Dist: redis==6.2.0
|
14
|
+
Requires-Dist: humanfriendly==10.0
|
14
15
|
Dynamic: author
|
15
16
|
Dynamic: author-email
|
16
17
|
Dynamic: description
|
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
|