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.
- moose_lib/__init__.py +38 -3
- moose_lib/blocks.py +497 -37
- moose_lib/clients/redis_client.py +26 -14
- moose_lib/commons.py +94 -5
- moose_lib/config/config_file.py +44 -2
- moose_lib/config/runtime.py +137 -5
- moose_lib/data_models.py +451 -46
- moose_lib/dmv2/__init__.py +88 -60
- moose_lib/dmv2/_registry.py +3 -1
- moose_lib/dmv2/_source_capture.py +37 -0
- moose_lib/dmv2/consumption.py +55 -32
- moose_lib/dmv2/ingest_api.py +9 -2
- moose_lib/dmv2/ingest_pipeline.py +56 -13
- moose_lib/dmv2/life_cycle.py +3 -1
- moose_lib/dmv2/materialized_view.py +24 -14
- moose_lib/dmv2/moose_model.py +165 -0
- moose_lib/dmv2/olap_table.py +304 -119
- moose_lib/dmv2/registry.py +28 -3
- moose_lib/dmv2/sql_resource.py +16 -8
- moose_lib/dmv2/stream.py +241 -21
- moose_lib/dmv2/types.py +14 -8
- moose_lib/dmv2/view.py +13 -6
- moose_lib/dmv2/web_app.py +175 -0
- moose_lib/dmv2/web_app_helpers.py +96 -0
- moose_lib/dmv2/workflow.py +37 -9
- moose_lib/internal.py +537 -68
- moose_lib/main.py +87 -56
- moose_lib/query_builder.py +18 -5
- moose_lib/query_param.py +54 -20
- moose_lib/secrets.py +122 -0
- moose_lib/streaming/streaming_function_runner.py +266 -156
- moose_lib/utilities/sql.py +0 -1
- {moose_lib-0.6.90.dist-info → moose_lib-0.6.283.dist-info}/METADATA +19 -1
- moose_lib-0.6.283.dist-info/RECORD +63 -0
- tests/__init__.py +1 -1
- tests/conftest.py +38 -1
- tests/test_backward_compatibility.py +85 -0
- tests/test_cluster_validation.py +85 -0
- tests/test_codec.py +75 -0
- tests/test_column_formatting.py +80 -0
- tests/test_fixedstring.py +43 -0
- tests/test_iceberg_config.py +105 -0
- tests/test_int_types.py +211 -0
- tests/test_kafka_config.py +141 -0
- tests/test_materialized.py +74 -0
- tests/test_metadata.py +37 -0
- tests/test_moose.py +21 -30
- tests/test_moose_model.py +153 -0
- tests/test_olap_table_moosemodel.py +89 -0
- tests/test_olap_table_versioning.py +210 -0
- tests/test_query_builder.py +97 -9
- tests/test_redis_client.py +10 -3
- tests/test_s3queue_config.py +211 -110
- tests/test_secrets.py +239 -0
- tests/test_simple_aggregate.py +114 -0
- tests/test_web_app.py +227 -0
- moose_lib-0.6.90.dist-info/RECORD +0 -42
- {moose_lib-0.6.90.dist-info → moose_lib-0.6.283.dist-info}/WHEEL +0 -0
- {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
|
moose_lib/dmv2/workflow.py
CHANGED
|
@@ -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
|
-
|
|
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[
|
|
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(
|
|
85
|
+
t = keyword_args.get("t")
|
|
74
86
|
if t is None:
|
|
75
|
-
raise ValueError(
|
|
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(
|
|
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 (
|
|
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 (
|
|
83
|
-
|
|
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)
|