moose-lib 0.4.212__py3-none-any.whl → 0.4.214__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.
moose_lib/dmv2.py CHANGED
@@ -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
- self,
698
- name: str,
699
- setup: list[str],
700
- teardown: list[str],
701
- pulls_data_from: Optional[list[Union[OlapTable, "SqlResource"]]] = None,
702
- pushes_data_to: Optional[list[Union[OlapTable, "SqlResource"]]] = None,
703
- metadata: dict = None
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]], metadata: dict = None):
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)
moose_lib/internal.py CHANGED
@@ -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.212
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
@@ -3,8 +3,8 @@ moose_lib/blocks.py,sha256=_wdvC2NC_Y3MMEnB71WTgWbeQ--zPNHk19xjToJW0C0,3185
3
3
  moose_lib/commons.py,sha256=BV5X78MuOWHiZV9bsWSN69JIvzTNWUi-gnuMiAtaO8A,2489
4
4
  moose_lib/data_models.py,sha256=R6do1eQqHK6AZ4GTP5tOPtSZaltjZurfx9_Asji7Dwc,8529
5
5
  moose_lib/dmv2-serializer.py,sha256=CL_Pvvg8tJOT8Qk6hywDNzY8MYGhMVdTOw8arZi3jng,49
6
- moose_lib/dmv2.py,sha256=jSqN9ST7xhYCqUP4kJLDxIHMCsRvbkiTJAd27kb6Nfk,31803
7
- moose_lib/internal.py,sha256=gREvC3XxBFN4i7JL5uMj0riCu_JUO2YyiMZvCokg1ME,13101
6
+ moose_lib/dmv2.py,sha256=JFNRjTWUcuTw865Wy8E_UgjLeLZPWAU3m7Ec2rd3CKM,38404
7
+ moose_lib/internal.py,sha256=8aEXQjB2DWz-dGUJ0ch8g5awHxELoZx1XYmlVXlhCWo,14193
8
8
  moose_lib/main.py,sha256=In-u7yA1FsLDeP_2bhIgBtHY_BkXaZqDwf7BxwyC21c,8471
9
9
  moose_lib/query_param.py,sha256=AB5BKu610Ji-h1iYGMBZKfnEFqt85rS94kzhDwhWJnc,6288
10
10
  moose_lib/tasks.py,sha256=6MXA0j7nhvQILAJVTQHCAsquwrSOi2zAevghAc_7kXs,1554
@@ -16,7 +16,7 @@ tests/__init__.py,sha256=0Gh4yzPkkC3TzBGKhenpMIxJcRhyrrCfxLSfpTZnPMQ,53
16
16
  tests/conftest.py,sha256=ZVJNbnr4DwbcqkTmePW6U01zAzE6QD0kNAEZjPG1f4s,169
17
17
  tests/test_moose.py,sha256=mBsx_OYWmL8ppDzL_7Bd7xR6qf_i3-pCIO3wm2iQNaA,2136
18
18
  tests/test_redis_client.py,sha256=d9_MLYsJ4ecVil_jPB2gW3Q5aWnavxmmjZg2uYI3LVo,3256
19
- moose_lib-0.4.212.dist-info/METADATA,sha256=v5sDuNe4eTw8OkY_RXFRlTE4Az-07umLE-Hyy-vJuZY,603
20
- moose_lib-0.4.212.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- moose_lib-0.4.212.dist-info/top_level.txt,sha256=XEns2-4aCmGp2XjJAeEH9TAUcGONLnSLy6ycT9FSJh8,16
22
- moose_lib-0.4.212.dist-info/RECORD,,
19
+ moose_lib-0.4.214.dist-info/METADATA,sha256=rpuUzKwIew2DM6hjSWiSfvyxcJXSMXdOg64XyzhA_IQ,638
20
+ moose_lib-0.4.214.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ moose_lib-0.4.214.dist-info/top_level.txt,sha256=XEns2-4aCmGp2XjJAeEH9TAUcGONLnSLy6ycT9FSJh8,16
22
+ moose_lib-0.4.214.dist-info/RECORD,,