moose-lib 0.6.90__py3-none-any.whl → 0.6.283__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.
Files changed (59) hide show
  1. moose_lib/__init__.py +38 -3
  2. moose_lib/blocks.py +497 -37
  3. moose_lib/clients/redis_client.py +26 -14
  4. moose_lib/commons.py +94 -5
  5. moose_lib/config/config_file.py +44 -2
  6. moose_lib/config/runtime.py +137 -5
  7. moose_lib/data_models.py +451 -46
  8. moose_lib/dmv2/__init__.py +88 -60
  9. moose_lib/dmv2/_registry.py +3 -1
  10. moose_lib/dmv2/_source_capture.py +37 -0
  11. moose_lib/dmv2/consumption.py +55 -32
  12. moose_lib/dmv2/ingest_api.py +9 -2
  13. moose_lib/dmv2/ingest_pipeline.py +56 -13
  14. moose_lib/dmv2/life_cycle.py +3 -1
  15. moose_lib/dmv2/materialized_view.py +24 -14
  16. moose_lib/dmv2/moose_model.py +165 -0
  17. moose_lib/dmv2/olap_table.py +304 -119
  18. moose_lib/dmv2/registry.py +28 -3
  19. moose_lib/dmv2/sql_resource.py +16 -8
  20. moose_lib/dmv2/stream.py +241 -21
  21. moose_lib/dmv2/types.py +14 -8
  22. moose_lib/dmv2/view.py +13 -6
  23. moose_lib/dmv2/web_app.py +175 -0
  24. moose_lib/dmv2/web_app_helpers.py +96 -0
  25. moose_lib/dmv2/workflow.py +37 -9
  26. moose_lib/internal.py +537 -68
  27. moose_lib/main.py +87 -56
  28. moose_lib/query_builder.py +18 -5
  29. moose_lib/query_param.py +54 -20
  30. moose_lib/secrets.py +122 -0
  31. moose_lib/streaming/streaming_function_runner.py +266 -156
  32. moose_lib/utilities/sql.py +0 -1
  33. {moose_lib-0.6.90.dist-info → moose_lib-0.6.283.dist-info}/METADATA +19 -1
  34. moose_lib-0.6.283.dist-info/RECORD +63 -0
  35. tests/__init__.py +1 -1
  36. tests/conftest.py +38 -1
  37. tests/test_backward_compatibility.py +85 -0
  38. tests/test_cluster_validation.py +85 -0
  39. tests/test_codec.py +75 -0
  40. tests/test_column_formatting.py +80 -0
  41. tests/test_fixedstring.py +43 -0
  42. tests/test_iceberg_config.py +105 -0
  43. tests/test_int_types.py +211 -0
  44. tests/test_kafka_config.py +141 -0
  45. tests/test_materialized.py +74 -0
  46. tests/test_metadata.py +37 -0
  47. tests/test_moose.py +21 -30
  48. tests/test_moose_model.py +153 -0
  49. tests/test_olap_table_moosemodel.py +89 -0
  50. tests/test_olap_table_versioning.py +210 -0
  51. tests/test_query_builder.py +97 -9
  52. tests/test_redis_client.py +10 -3
  53. tests/test_s3queue_config.py +211 -110
  54. tests/test_secrets.py +239 -0
  55. tests/test_simple_aggregate.py +114 -0
  56. tests/test_web_app.py +227 -0
  57. moose_lib-0.6.90.dist-info/RECORD +0 -42
  58. {moose_lib-0.6.90.dist-info → moose_lib-0.6.283.dist-info}/WHEEL +0 -0
  59. {moose_lib-0.6.90.dist-info → moose_lib-0.6.283.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,175 @@
1
+ """
2
+ WebApp support for Python - bring your own FastAPI application.
3
+
4
+ This module allows developers to register FastAPI applications as WebApp resources
5
+ that are managed by the Moose infrastructure, similar to other resources like
6
+ OlapTables, Streams, and APIs.
7
+ """
8
+
9
+ from typing import Optional, Dict, Any
10
+ from dataclasses import dataclass
11
+
12
+
13
+ # Reserved mount paths that cannot be used by WebApps
14
+ RESERVED_MOUNT_PATHS = [
15
+ "/admin",
16
+ "/api",
17
+ "/consumption",
18
+ "/health",
19
+ "/ingest",
20
+ "/moose", # reserved for future use
21
+ "/ready",
22
+ "/workflows",
23
+ ]
24
+
25
+
26
+ @dataclass
27
+ class WebAppMetadata:
28
+ """Metadata for a WebApp.
29
+
30
+ Attributes:
31
+ description: Optional description of the WebApp's purpose.
32
+ """
33
+
34
+ description: Optional[str] = None
35
+
36
+
37
+ @dataclass
38
+ class WebAppConfig:
39
+ """Configuration for a WebApp.
40
+
41
+ Attributes:
42
+ mount_path: The URL path where the WebApp will be mounted (required).
43
+ Cannot be "/" (root path).
44
+ Cannot end with "/" (trailing slash).
45
+ Cannot start with reserved paths.
46
+ metadata: Optional metadata for documentation purposes.
47
+ inject_moose_utils: Whether to inject MooseClient utilities into requests.
48
+ Defaults to True.
49
+ """
50
+
51
+ mount_path: str
52
+ metadata: Optional[WebAppMetadata] = None
53
+ inject_moose_utils: bool = True
54
+
55
+
56
+ class WebApp:
57
+ """A WebApp resource that wraps a FastAPI application.
58
+
59
+ WebApps are managed by the Moose infrastructure and automatically
60
+ proxied through the Rust webserver, allowing them to coexist with
61
+ other Moose resources on the same port.
62
+
63
+ Example:
64
+ ```python
65
+ from fastapi import FastAPI, Request
66
+ from moose_lib.dmv2 import WebApp, WebAppConfig, WebAppMetadata
67
+ from moose_lib.dmv2.web_app_helpers import get_moose_utils
68
+
69
+ app = FastAPI()
70
+
71
+ @app.get("/hello")
72
+ async def hello(request: Request):
73
+ moose = get_moose_utils(request)
74
+ # Use moose.client for queries
75
+ return {"message": "Hello World"}
76
+
77
+ # Register as a WebApp with custom mount path
78
+ my_webapp = WebApp(
79
+ "myApi",
80
+ app,
81
+ WebAppConfig(
82
+ mount_path="/myapi",
83
+ metadata=WebAppMetadata(description="My custom API"),
84
+ )
85
+ )
86
+ ```
87
+
88
+ Args:
89
+ name: Unique name for this WebApp.
90
+ app: The FastAPI application instance.
91
+ config: Configuration for the WebApp (required, must include mount_path).
92
+
93
+ Raises:
94
+ ValueError: If validation fails (duplicate name, invalid mount path, etc.)
95
+ """
96
+
97
+ def __init__(
98
+ self,
99
+ name: str,
100
+ app: Any, # FastAPI app, typed as Any to avoid import dependency
101
+ config: WebAppConfig,
102
+ ):
103
+ self.name = name
104
+ self.app = app
105
+ self.config = config
106
+
107
+ # Import the registry here to avoid circular dependency
108
+ from ._registry import _web_apps
109
+
110
+ # Validate the configuration
111
+ self._validate(name, self.config, _web_apps)
112
+
113
+ # Register this WebApp
114
+ _web_apps[name] = self
115
+
116
+ @staticmethod
117
+ def _validate(
118
+ name: str, config: WebAppConfig, existing_web_apps: Dict[str, "WebApp"]
119
+ ) -> None:
120
+ """Validate WebApp configuration.
121
+
122
+ Args:
123
+ name: The name of the WebApp being validated.
124
+ config: The configuration to validate.
125
+ existing_web_apps: Dictionary of already registered WebApps.
126
+
127
+ Raises:
128
+ ValueError: If validation fails.
129
+ """
130
+ # Check for duplicate name
131
+ if name in existing_web_apps:
132
+ raise ValueError(f"WebApp with name '{name}' already exists")
133
+
134
+ # Validate mountPath - it is required
135
+ if not config.mount_path:
136
+ raise ValueError(
137
+ f'mountPath is required. Please specify a mount path for your WebApp (e.g., "/myapi").'
138
+ )
139
+
140
+ mount_path = config.mount_path
141
+
142
+ # Check for root path - not allowed as it would overlap reserved paths
143
+ if mount_path == "/":
144
+ raise ValueError(
145
+ f'mountPath cannot be "/" as it would allow routes to overlap with reserved paths: '
146
+ f"{', '.join(RESERVED_MOUNT_PATHS)}"
147
+ )
148
+
149
+ # Validate mount path format
150
+ if mount_path.endswith("/"):
151
+ raise ValueError(
152
+ f"mountPath cannot end with a trailing slash. "
153
+ f"Remove the '/' from: \"{mount_path}\""
154
+ )
155
+
156
+ # Check for reserved path prefixes
157
+ for reserved in RESERVED_MOUNT_PATHS:
158
+ if mount_path == reserved or mount_path.startswith(f"{reserved}/"):
159
+ raise ValueError(
160
+ f"mountPath cannot begin with a reserved path: "
161
+ f"{', '.join(RESERVED_MOUNT_PATHS)}. "
162
+ f'Got: "{mount_path}"'
163
+ )
164
+
165
+ # Check for duplicate mount path
166
+ for existing_name, existing_app in existing_web_apps.items():
167
+ existing_mount = existing_app.config.mount_path
168
+ if existing_mount == mount_path:
169
+ raise ValueError(
170
+ f'WebApp with mountPath "{mount_path}" already exists '
171
+ f'(used by WebApp "{existing_name}")'
172
+ )
173
+
174
+ def __repr__(self) -> str:
175
+ return f"WebApp(name='{self.name}', mount_path='{self.config.mount_path}')"
@@ -0,0 +1,96 @@
1
+ """
2
+ Helper utilities for WebApp integration with FastAPI.
3
+
4
+ This module provides utilities to access Moose services (ClickHouse, Temporal)
5
+ from within FastAPI request handlers.
6
+ """
7
+
8
+ from typing import Optional, Any, Dict
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class ApiUtil:
14
+ """Utilities available to WebApp request handlers.
15
+
16
+ Attributes:
17
+ client: MooseClient instance for executing queries and workflows.
18
+ sql: SQL template function for building safe queries.
19
+ jwt: JWT payload if authentication is enabled, None otherwise.
20
+ """
21
+
22
+ client: Any # MooseClient, typed as Any to avoid circular import
23
+ sql: Any # sql function from moose_lib.main
24
+ jwt: Optional[Dict[str, Any]] = None
25
+
26
+
27
+ def get_moose_utils(request: Any) -> Optional[ApiUtil]:
28
+ """Extract Moose utilities from a FastAPI request.
29
+
30
+ The Moose infrastructure automatically injects utilities into request.state
31
+ when inject_moose_utils is enabled (default).
32
+
33
+ Args:
34
+ request: FastAPI Request object.
35
+
36
+ Returns:
37
+ ApiUtil instance if available, None otherwise.
38
+
39
+ Example:
40
+ ```python
41
+ from fastapi import FastAPI, Request
42
+ from moose_lib.dmv2.web_app_helpers import get_moose_utils
43
+
44
+ app = FastAPI()
45
+
46
+ @app.get("/data")
47
+ async def get_data(request: Request):
48
+ moose = get_moose_utils(request)
49
+ if not moose:
50
+ return {"error": "Moose utilities not available"}
51
+
52
+ # Execute a query
53
+ result = moose.client.query.execute(
54
+ moose.sql("SELECT * FROM my_table LIMIT {limit}", limit=10)
55
+ )
56
+ return result
57
+ ```
58
+ """
59
+ # FastAPI uses request.state for storing custom data
60
+ if hasattr(request, "state") and hasattr(request.state, "moose"):
61
+ return request.state.moose
62
+ return None
63
+
64
+
65
+ def get_moose_dependency():
66
+ """FastAPI dependency for injecting Moose utilities.
67
+
68
+ Can be used with FastAPI's Depends() to automatically inject
69
+ Moose utilities into route handlers.
70
+
71
+ Returns:
72
+ A dependency function that extracts ApiUtil from the request.
73
+
74
+ Example:
75
+ ```python
76
+ from fastapi import FastAPI, Depends, Request
77
+ from moose_lib.dmv2.web_app_helpers import get_moose_dependency, ApiUtil
78
+
79
+ app = FastAPI()
80
+
81
+ @app.get("/data")
82
+ async def get_data(moose: ApiUtil = Depends(get_moose_dependency())):
83
+ # moose is automatically injected
84
+ result = moose.client.query.execute(...)
85
+ return result
86
+ ```
87
+ """
88
+
89
+ def moose_dependency(request: Any) -> ApiUtil:
90
+ moose = get_moose_utils(request)
91
+ if moose is None:
92
+ # This should rarely happen if inject_moose_utils=True
93
+ raise RuntimeError("Moose utilities not available in request")
94
+ return moose
95
+
96
+ return moose_dependency
@@ -4,6 +4,7 @@ Workflow definitions for Moose Data Model v2 (dmv2).
4
4
  This module provides classes for defining and configuring workflows composed of tasks,
5
5
  including task dependencies, configurations, and execution functions.
6
6
  """
7
+
7
8
  import dataclasses
8
9
  from typing import Any, Optional, Dict, List, Callable, Union, Awaitable, Generic
9
10
  from pydantic import BaseModel
@@ -11,6 +12,7 @@ from pydantic import BaseModel
11
12
  from .types import TypedMooseResource, T_none, U_none
12
13
  from ._registry import _workflows
13
14
 
15
+
14
16
  @dataclasses.dataclass
15
17
  class TaskContext(Generic[T_none]):
16
18
  """Context object passed to task handlers.
@@ -18,10 +20,15 @@ class TaskContext(Generic[T_none]):
18
20
  - When a task declares an input model `T`, `input` is of type `T` (not Optional).
19
21
  - For no-input tasks (`T` is `None`), `input` is exactly `None`.
20
22
  """
23
+
21
24
  state: Dict[str, Any]
22
25
  input: T_none
23
26
 
24
- type TaskRunFunc[T_none, U_none] = Callable[[TaskContext[T_none]], Union[U_none, Awaitable[U_none]]]
27
+
28
+ type TaskRunFunc[T_none, U_none] = Callable[
29
+ [TaskContext[T_none]], Union[U_none, Awaitable[U_none]]
30
+ ]
31
+
25
32
 
26
33
  @dataclasses.dataclass
27
34
  class TaskConfig(Generic[T_none, U_none]):
@@ -36,12 +43,16 @@ class TaskConfig(Generic[T_none, U_none]):
36
43
  timeout: Optional timeout string (e.g. "5m", "1h", "never").
37
44
  retries: Optional number of retry attempts.
38
45
  """
46
+
39
47
  run: TaskRunFunc[T_none, U_none]
40
48
  on_complete: Optional[list["Task[U_none, Any]"]] = None
41
- on_cancel: Optional[Callable[[TaskContext[T_none]], Union[None, Awaitable[None]]]] = None
49
+ on_cancel: Optional[
50
+ Callable[[TaskContext[T_none]], Union[None, Awaitable[None]]]
51
+ ] = None
42
52
  timeout: Optional[str] = None
43
53
  retries: Optional[int] = None
44
54
 
55
+
45
56
  class Task(TypedMooseResource, Generic[T_none, U_none]):
46
57
  """Represents a task that can be executed as part of a workflow.
47
58
 
@@ -61,6 +72,7 @@ class Task(TypedMooseResource, Generic[T_none, U_none]):
61
72
  name (str): The name of the task.
62
73
  model_type (type[T]): The Pydantic model associated with this task's input.
63
74
  """
75
+
64
76
  config: TaskConfig[T_none, U_none]
65
77
 
66
78
  def __init__(self, name: str, config: TaskConfig[T_none, U_none], **kwargs):
@@ -70,17 +82,27 @@ class Task(TypedMooseResource, Generic[T_none, U_none]):
70
82
 
71
83
  @classmethod
72
84
  def _get_type(cls, keyword_args: dict):
73
- t = keyword_args.get('t')
85
+ t = keyword_args.get("t")
74
86
  if t is None:
75
- raise ValueError(f"Use `{cls.__name__}[T, U](name='...')` to supply both input and output types")
87
+ raise ValueError(
88
+ f"Use `{cls.__name__}[T, U](name='...')` to supply both input and output types"
89
+ )
76
90
  if not isinstance(t, tuple) or len(t) != 2:
77
- raise ValueError(f"Use `{cls.__name__}[T, U](name='...')` to supply both input and output types")
91
+ raise ValueError(
92
+ f"Use `{cls.__name__}[T, U](name='...')` to supply both input and output types"
93
+ )
78
94
 
79
95
  input_type, output_type = t
80
- if input_type is not None and (not isinstance(input_type, type) or not issubclass(input_type, BaseModel)):
96
+ if input_type is not None and (
97
+ not isinstance(input_type, type) or not issubclass(input_type, BaseModel)
98
+ ):
81
99
  raise ValueError(f"Input type {input_type} is not a Pydantic model or None")
82
- if output_type is not None and (not isinstance(output_type, type) or not issubclass(output_type, BaseModel)):
83
- raise ValueError(f"Output type {output_type} is not a Pydantic model or None")
100
+ if output_type is not None and (
101
+ not isinstance(output_type, type) or not issubclass(output_type, BaseModel)
102
+ ):
103
+ raise ValueError(
104
+ f"Output type {output_type} is not a Pydantic model or None"
105
+ )
84
106
  return t
85
107
 
86
108
  def _set_type(self, name: str, t: tuple[type[T_none], type[U_none]]):
@@ -89,6 +111,7 @@ class Task(TypedMooseResource, Generic[T_none, U_none]):
89
111
  self._u = output_type
90
112
  self.name = name
91
113
 
114
+
92
115
  @dataclasses.dataclass
93
116
  class WorkflowConfig:
94
117
  """Configuration for a workflow.
@@ -99,11 +122,13 @@ class WorkflowConfig:
99
122
  timeout: Optional timeout string for the entire workflow.
100
123
  schedule: Optional cron-like schedule string for recurring execution.
101
124
  """
125
+
102
126
  starting_task: Task[Any, Any]
103
127
  retries: Optional[int] = None
104
128
  timeout: Optional[str] = None
105
129
  schedule: Optional[str] = None
106
130
 
131
+
107
132
  class Workflow:
108
133
  """Represents a workflow composed of one or more tasks.
109
134
 
@@ -118,6 +143,7 @@ class Workflow:
118
143
  name (str): The name of the workflow.
119
144
  config (WorkflowConfig): The configuration for this workflow.
120
145
  """
146
+
121
147
  def __init__(self, name: str, config: WorkflowConfig):
122
148
  self.name = name
123
149
  self.config = config
@@ -130,6 +156,7 @@ class Workflow:
130
156
  Returns:
131
157
  list[str]: List of task names in the workflow, including all child tasks
132
158
  """
159
+
133
160
  def collect_task_names(task: Task) -> list[str]:
134
161
  names = [task.name]
135
162
  if task.config.on_complete:
@@ -148,6 +175,7 @@ class Workflow:
148
175
  Returns:
149
176
  Optional[Task]: The task if found, None otherwise
150
177
  """
178
+
151
179
  def find_task(task: Task) -> Optional[Task]:
152
180
  if task.name == task_name:
153
181
  return task
@@ -158,4 +186,4 @@ class Workflow:
158
186
  return found
159
187
  return None
160
188
 
161
- return find_task(self.config.starting_task)
189
+ return find_task(self.config.starting_task)