moose-lib 0.6.148.dev3442438466__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 +34 -3
- moose_lib/blocks.py +416 -52
- moose_lib/clients/redis_client.py +26 -14
- moose_lib/commons.py +37 -30
- moose_lib/config/config_file.py +5 -1
- moose_lib/config/runtime.py +73 -34
- moose_lib/data_models.py +331 -61
- moose_lib/dmv2/__init__.py +69 -73
- moose_lib/dmv2/_registry.py +2 -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 +35 -16
- 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 +299 -151
- moose_lib/dmv2/registry.py +18 -3
- moose_lib/dmv2/sql_resource.py +16 -8
- moose_lib/dmv2/stream.py +75 -23
- moose_lib/dmv2/types.py +14 -8
- moose_lib/dmv2/view.py +13 -6
- moose_lib/dmv2/web_app.py +11 -6
- moose_lib/dmv2/web_app_helpers.py +5 -1
- moose_lib/dmv2/workflow.py +37 -9
- moose_lib/internal.py +340 -56
- 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 +233 -117
- moose_lib/utilities/sql.py +0 -1
- {moose_lib-0.6.148.dev3442438466.dist-info → moose_lib-0.6.283.dist-info}/METADATA +18 -1
- moose_lib-0.6.283.dist-info/RECORD +63 -0
- tests/__init__.py +1 -1
- tests/conftest.py +6 -5
- 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 +52 -58
- 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 +42 -40
- tests/test_web_app.py +11 -5
- moose_lib-0.6.148.dev3442438466.dist-info/RECORD +0 -47
- {moose_lib-0.6.148.dev3442438466.dist-info → moose_lib-0.6.283.dist-info}/WHEEL +0 -0
- {moose_lib-0.6.148.dev3442438466.dist-info → moose_lib-0.6.283.dist-info}/top_level.txt +0 -0
moose_lib/dmv2/registry.py
CHANGED
|
@@ -4,6 +4,7 @@ Global registries for Moose Data Model v2 (dmv2) resources.
|
|
|
4
4
|
This module provides functions to access the registered resources.
|
|
5
5
|
The actual registry dictionaries are maintained in _registry.py to avoid circular dependencies.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from typing import Optional, Dict
|
|
8
9
|
from .olap_table import OlapTable
|
|
9
10
|
from .stream import Stream
|
|
@@ -24,34 +25,42 @@ from ._registry import (
|
|
|
24
25
|
_web_apps,
|
|
25
26
|
)
|
|
26
27
|
|
|
28
|
+
|
|
27
29
|
def get_tables() -> Dict[str, OlapTable]:
|
|
28
30
|
"""Get all registered OLAP tables."""
|
|
29
31
|
return _tables
|
|
30
32
|
|
|
33
|
+
|
|
31
34
|
def get_table(name: str) -> Optional[OlapTable]:
|
|
32
35
|
"""Get a registered OLAP table by name."""
|
|
33
36
|
return _tables.get(name)
|
|
34
37
|
|
|
38
|
+
|
|
35
39
|
def get_streams() -> Dict[str, Stream]:
|
|
36
40
|
"""Get all registered streams."""
|
|
37
41
|
return _streams
|
|
38
42
|
|
|
43
|
+
|
|
39
44
|
def get_stream(name: str) -> Optional[Stream]:
|
|
40
45
|
"""Get a registered stream by name."""
|
|
41
46
|
return _streams.get(name)
|
|
42
47
|
|
|
48
|
+
|
|
43
49
|
def get_ingest_apis() -> Dict[str, IngestApi]:
|
|
44
50
|
"""Get all registered ingestion APIs."""
|
|
45
51
|
return _ingest_apis
|
|
46
52
|
|
|
53
|
+
|
|
47
54
|
def get_ingest_api(name: str) -> Optional[IngestApi]:
|
|
48
55
|
"""Get a registered ingestion API by name."""
|
|
49
56
|
return _ingest_apis.get(name)
|
|
50
57
|
|
|
58
|
+
|
|
51
59
|
def get_apis() -> Dict[str, Api]:
|
|
52
60
|
"""Get all registered APIs."""
|
|
53
61
|
return _apis
|
|
54
62
|
|
|
63
|
+
|
|
55
64
|
def get_api(name: str) -> Optional[Api]:
|
|
56
65
|
"""Get a registered API by name or path.
|
|
57
66
|
|
|
@@ -64,35 +73,41 @@ def get_api(name: str) -> Optional[Api]:
|
|
|
64
73
|
api = _apis.get(name)
|
|
65
74
|
if api:
|
|
66
75
|
return api
|
|
67
|
-
|
|
76
|
+
|
|
68
77
|
# Try alias lookup
|
|
69
78
|
api = _api_name_aliases.get(name)
|
|
70
79
|
if api:
|
|
71
80
|
return api
|
|
72
|
-
|
|
81
|
+
|
|
73
82
|
# Try path-based lookup
|
|
74
83
|
return _api_path_map.get(name)
|
|
75
84
|
|
|
85
|
+
|
|
76
86
|
def get_sql_resources() -> Dict[str, SqlResource]:
|
|
77
87
|
"""Get all registered SQL resources."""
|
|
78
88
|
return _sql_resources
|
|
79
89
|
|
|
90
|
+
|
|
80
91
|
def get_sql_resource(name: str) -> Optional[SqlResource]:
|
|
81
92
|
"""Get a registered SQL resource by name."""
|
|
82
93
|
return _sql_resources.get(name)
|
|
83
94
|
|
|
95
|
+
|
|
84
96
|
def get_workflows() -> Dict[str, Workflow]:
|
|
85
97
|
"""Get all registered workflows."""
|
|
86
98
|
return _workflows
|
|
87
99
|
|
|
100
|
+
|
|
88
101
|
def get_workflow(name: str) -> Optional[Workflow]:
|
|
89
102
|
"""Get a registered workflow by name."""
|
|
90
103
|
return _workflows.get(name)
|
|
91
104
|
|
|
105
|
+
|
|
92
106
|
def get_web_apps() -> Dict[str, WebApp]:
|
|
93
107
|
"""Get all registered WebApps."""
|
|
94
108
|
return _web_apps
|
|
95
109
|
|
|
110
|
+
|
|
96
111
|
def get_web_app(name: str) -> Optional[WebApp]:
|
|
97
112
|
"""Get a registered WebApp by name."""
|
|
98
113
|
return _web_apps.get(name)
|
|
@@ -103,4 +118,4 @@ get_consumption_apis = get_apis
|
|
|
103
118
|
"""@deprecated: Use get_apis instead of get_consumption_apis"""
|
|
104
119
|
|
|
105
120
|
get_consumption_api = get_api
|
|
106
|
-
"""@deprecated: Use get_api instead of get_consumption_api"""
|
|
121
|
+
"""@deprecated: Use get_api instead of get_consumption_api"""
|
moose_lib/dmv2/sql_resource.py
CHANGED
|
@@ -4,11 +4,14 @@ Base SQL resource definitions for Moose Data Model v2 (dmv2).
|
|
|
4
4
|
This module provides the base class for SQL resources like Views and Materialized Views,
|
|
5
5
|
handling common functionality like setup/teardown SQL commands and dependency tracking.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from typing import Any, Optional, Union, List
|
|
8
9
|
from pydantic import BaseModel
|
|
9
10
|
|
|
10
11
|
from .olap_table import OlapTable
|
|
11
12
|
from ._registry import _sql_resources
|
|
13
|
+
from ._source_capture import get_source_file_from_stack
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
class SqlResource:
|
|
14
17
|
"""Base class for SQL resources like Views and Materialized Views.
|
|
@@ -23,22 +26,25 @@ class SqlResource:
|
|
|
23
26
|
pulls_data_from (list[SqlObject]): List of tables/views this resource reads from.
|
|
24
27
|
pushes_data_to (list[SqlObject]): List of tables/views this resource writes to.
|
|
25
28
|
kind: The kind of the SQL resource (e.g., "SqlResource").
|
|
29
|
+
source_file: Optional path to the source file where this resource was defined.
|
|
26
30
|
"""
|
|
31
|
+
|
|
27
32
|
setup: list[str]
|
|
28
33
|
teardown: list[str]
|
|
29
34
|
name: str
|
|
30
35
|
kind: str = "SqlResource"
|
|
31
36
|
pulls_data_from: list[Union[OlapTable, "SqlResource"]]
|
|
32
37
|
pushes_data_to: list[Union[OlapTable, "SqlResource"]]
|
|
38
|
+
source_file: Optional[str]
|
|
33
39
|
|
|
34
40
|
def __init__(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
self,
|
|
42
|
+
name: str,
|
|
43
|
+
setup: list[str],
|
|
44
|
+
teardown: list[str],
|
|
45
|
+
pulls_data_from: Optional[list[Union[OlapTable, "SqlResource"]]] = None,
|
|
46
|
+
pushes_data_to: Optional[list[Union[OlapTable, "SqlResource"]]] = None,
|
|
47
|
+
metadata: dict = None,
|
|
42
48
|
):
|
|
43
49
|
self.name = name
|
|
44
50
|
self.setup = setup
|
|
@@ -46,4 +52,6 @@ class SqlResource:
|
|
|
46
52
|
self.pulls_data_from = pulls_data_from or []
|
|
47
53
|
self.pushes_data_to = pushes_data_to or []
|
|
48
54
|
self.metadata = metadata
|
|
49
|
-
|
|
55
|
+
# Capture source file from call stack
|
|
56
|
+
self.source_file = get_source_file_from_stack()
|
|
57
|
+
_sql_resources[name] = self
|
moose_lib/dmv2/stream.py
CHANGED
|
@@ -4,6 +4,7 @@ Stream definitions for Moose Data Model v2 (dmv2).
|
|
|
4
4
|
This module provides classes for defining and configuring data streams,
|
|
5
5
|
including stream transformations, consumers, and dead letter queues.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
import dataclasses
|
|
8
9
|
import datetime
|
|
9
10
|
import json
|
|
@@ -50,6 +51,7 @@ class StreamConfig(BaseModel):
|
|
|
50
51
|
life_cycle: Determines how changes in code will propagate to the resources.
|
|
51
52
|
default_dead_letter_queue: default dead letter queue used by transforms/consumers
|
|
52
53
|
"""
|
|
54
|
+
|
|
53
55
|
parallelism: int = 1
|
|
54
56
|
retention_period: int = 60 * 60 * 24 * 7 # 7 days
|
|
55
57
|
destination: Optional[OlapTable] = None
|
|
@@ -69,6 +71,7 @@ class TransformConfig(BaseModel):
|
|
|
69
71
|
version: Optional version string to identify a specific transformation.
|
|
70
72
|
Allows multiple transformations to the same destination if versions differ.
|
|
71
73
|
"""
|
|
74
|
+
|
|
72
75
|
version: Optional[str] = None
|
|
73
76
|
dead_letter_queue: "Optional[DeadLetterQueue]" = None
|
|
74
77
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
@@ -82,6 +85,7 @@ class ConsumerConfig(BaseModel):
|
|
|
82
85
|
version: Optional version string to identify a specific consumer.
|
|
83
86
|
Allows multiple consumers if versions differ.
|
|
84
87
|
"""
|
|
88
|
+
|
|
85
89
|
version: Optional[str] = None
|
|
86
90
|
dead_letter_queue: "Optional[DeadLetterQueue]" = None
|
|
87
91
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
@@ -90,6 +94,7 @@ class ConsumerConfig(BaseModel):
|
|
|
90
94
|
@dataclasses.dataclass
|
|
91
95
|
class _RoutedMessage:
|
|
92
96
|
"""Internal class representing a message routed to a specific stream."""
|
|
97
|
+
|
|
93
98
|
destination: "Stream[Any]"
|
|
94
99
|
values: ZeroOrMany[Any]
|
|
95
100
|
|
|
@@ -97,6 +102,7 @@ class _RoutedMessage:
|
|
|
97
102
|
@dataclasses.dataclass
|
|
98
103
|
class ConsumerEntry(Generic[T]):
|
|
99
104
|
"""Internal class representing a consumer with its configuration."""
|
|
105
|
+
|
|
100
106
|
consumer: Callable[[T], None]
|
|
101
107
|
config: ConsumerConfig
|
|
102
108
|
|
|
@@ -104,6 +110,7 @@ class ConsumerEntry(Generic[T]):
|
|
|
104
110
|
@dataclasses.dataclass
|
|
105
111
|
class TransformEntry(Generic[T]):
|
|
106
112
|
"""Internal class representing a transformation with its configuration."""
|
|
113
|
+
|
|
107
114
|
destination: "Stream[Any]"
|
|
108
115
|
transformation: Callable[[T], ZeroOrMany[Any]]
|
|
109
116
|
config: TransformConfig
|
|
@@ -128,6 +135,7 @@ class Stream(TypedMooseResource, Generic[T]):
|
|
|
128
135
|
name (str): The name of the stream.
|
|
129
136
|
model_type (type[T]): The Pydantic model associated with this stream.
|
|
130
137
|
"""
|
|
138
|
+
|
|
131
139
|
config: StreamConfig
|
|
132
140
|
transformations: dict[str, list[TransformEntry[T]]]
|
|
133
141
|
consumers: list[ConsumerEntry[T]]
|
|
@@ -146,8 +154,12 @@ class Stream(TypedMooseResource, Generic[T]):
|
|
|
146
154
|
self.default_dead_letter_queue = self.config.default_dead_letter_queue
|
|
147
155
|
_streams[name] = self
|
|
148
156
|
|
|
149
|
-
def add_transform(
|
|
150
|
-
|
|
157
|
+
def add_transform(
|
|
158
|
+
self,
|
|
159
|
+
destination: "Stream[U]",
|
|
160
|
+
transformation: Callable[[T], ZeroOrMany[U]],
|
|
161
|
+
config: TransformConfig = None,
|
|
162
|
+
):
|
|
151
163
|
"""Adds a transformation step from this stream to a destination stream.
|
|
152
164
|
|
|
153
165
|
The transformation function receives a record of type `T` and should return
|
|
@@ -160,23 +172,37 @@ class Stream(TypedMooseResource, Generic[T]):
|
|
|
160
172
|
"""
|
|
161
173
|
config = config or TransformConfig()
|
|
162
174
|
if (
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
self.default_dead_letter_queue is not None
|
|
176
|
+
and config.dead_letter_queue is None
|
|
165
177
|
):
|
|
166
178
|
config = config.model_copy()
|
|
167
179
|
config.dead_letter_queue = self.default_dead_letter_queue
|
|
168
180
|
if destination.name in self.transformations:
|
|
169
181
|
existing_transforms = self.transformations[destination.name]
|
|
170
182
|
# Check if a transform with this version already exists
|
|
171
|
-
has_version = any(
|
|
183
|
+
has_version = any(
|
|
184
|
+
t.config.version == config.version for t in existing_transforms
|
|
185
|
+
)
|
|
172
186
|
if not has_version:
|
|
173
187
|
existing_transforms.append(
|
|
174
|
-
TransformEntry(
|
|
188
|
+
TransformEntry(
|
|
189
|
+
destination=destination,
|
|
190
|
+
transformation=transformation,
|
|
191
|
+
config=config,
|
|
192
|
+
)
|
|
193
|
+
)
|
|
175
194
|
else:
|
|
176
195
|
self.transformations[destination.name] = [
|
|
177
|
-
TransformEntry(
|
|
196
|
+
TransformEntry(
|
|
197
|
+
destination=destination,
|
|
198
|
+
transformation=transformation,
|
|
199
|
+
config=config,
|
|
200
|
+
)
|
|
201
|
+
]
|
|
178
202
|
|
|
179
|
-
def add_consumer(
|
|
203
|
+
def add_consumer(
|
|
204
|
+
self, consumer: Callable[[T], None], config: ConsumerConfig = None
|
|
205
|
+
):
|
|
180
206
|
"""Adds a consumer function to be executed for each record in the stream.
|
|
181
207
|
|
|
182
208
|
Consumers are typically used for side effects like logging or triggering external actions.
|
|
@@ -187,8 +213,8 @@ class Stream(TypedMooseResource, Generic[T]):
|
|
|
187
213
|
"""
|
|
188
214
|
config = config or ConsumerConfig()
|
|
189
215
|
if (
|
|
190
|
-
|
|
191
|
-
|
|
216
|
+
self.default_dead_letter_queue is not None
|
|
217
|
+
and config.dead_letter_queue is None
|
|
192
218
|
):
|
|
193
219
|
config = config.model_copy()
|
|
194
220
|
config.dead_letter_queue = self.default_dead_letter_queue
|
|
@@ -243,12 +269,15 @@ class Stream(TypedMooseResource, Generic[T]):
|
|
|
243
269
|
|
|
244
270
|
def _build_full_topic_name(self, namespace: Optional[str]) -> str:
|
|
245
271
|
"""Build full topic name with optional namespace and version suffix."""
|
|
246
|
-
version_suffix =
|
|
272
|
+
version_suffix = (
|
|
273
|
+
f"_{self.config.version.replace('.', '_')}" if self.config.version else ""
|
|
274
|
+
)
|
|
247
275
|
base = f"{self.name}{version_suffix}"
|
|
248
276
|
return f"{namespace}.{base}" if namespace else base
|
|
249
277
|
|
|
250
278
|
def _create_kafka_config_hash(self, cfg: RuntimeKafkaConfig) -> str:
|
|
251
279
|
import hashlib
|
|
280
|
+
|
|
252
281
|
config_string = ":".join(
|
|
253
282
|
str(x)
|
|
254
283
|
for x in (
|
|
@@ -273,11 +302,17 @@ class Stream(TypedMooseResource, Generic[T]):
|
|
|
273
302
|
cfg: RuntimeKafkaConfig = config_registry.get_kafka_config()
|
|
274
303
|
current_hash = self._create_kafka_config_hash(cfg)
|
|
275
304
|
|
|
276
|
-
if
|
|
305
|
+
if (
|
|
306
|
+
self._memoized_producer is not None
|
|
307
|
+
and self._kafka_config_hash == current_hash
|
|
308
|
+
):
|
|
277
309
|
return self._memoized_producer, cfg
|
|
278
310
|
|
|
279
311
|
# Close previous producer if config changed
|
|
280
|
-
if
|
|
312
|
+
if (
|
|
313
|
+
self._memoized_producer is not None
|
|
314
|
+
and self._kafka_config_hash != current_hash
|
|
315
|
+
):
|
|
281
316
|
try:
|
|
282
317
|
self._memoized_producer.flush()
|
|
283
318
|
self._memoized_producer.close()
|
|
@@ -296,7 +331,7 @@ class Stream(TypedMooseResource, Generic[T]):
|
|
|
296
331
|
sasl_password=cfg.sasl_password,
|
|
297
332
|
sasl_mechanism=cfg.sasl_mechanism,
|
|
298
333
|
security_protocol=cfg.security_protocol,
|
|
299
|
-
value_serializer=lambda v: v.model_dump_json().encode(
|
|
334
|
+
value_serializer=lambda v: v.model_dump_json().encode("utf-8"),
|
|
300
335
|
acks="all",
|
|
301
336
|
)
|
|
302
337
|
|
|
@@ -371,7 +406,9 @@ class Stream(TypedMooseResource, Generic[T]):
|
|
|
371
406
|
elif isinstance(sr.reference, SubjectLatest):
|
|
372
407
|
schema = client.get_latest_version(sr.reference.name).schema
|
|
373
408
|
else:
|
|
374
|
-
schema = client.get_version(
|
|
409
|
+
schema = client.get_version(
|
|
410
|
+
sr.reference.subject, sr.reference.version
|
|
411
|
+
).schema
|
|
375
412
|
|
|
376
413
|
serializer = JSONSerializer(schema, client)
|
|
377
414
|
|
|
@@ -395,9 +432,12 @@ class DeadLetterModel(BaseModel, Generic[T]):
|
|
|
395
432
|
failed_at: Timestamp when the error occurred.
|
|
396
433
|
source: Source of the error ("api", "transform", or "table").
|
|
397
434
|
"""
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
435
|
+
|
|
436
|
+
model_config = ConfigDict(
|
|
437
|
+
alias_generator=AliasGenerator(
|
|
438
|
+
serialization_alias=to_camel,
|
|
439
|
+
)
|
|
440
|
+
)
|
|
401
441
|
original_record: Any
|
|
402
442
|
error_message: str
|
|
403
443
|
error_type: str
|
|
@@ -429,10 +469,16 @@ class DeadLetterQueue(Stream, Generic[T]):
|
|
|
429
469
|
"""
|
|
430
470
|
self._model_type = self._get_type(kwargs)
|
|
431
471
|
kwargs["t"] = DeadLetterModel[self._model_type]
|
|
432
|
-
super().__init__(
|
|
472
|
+
super().__init__(
|
|
473
|
+
name, config if config is not None else StreamConfig(), **kwargs
|
|
474
|
+
)
|
|
433
475
|
|
|
434
|
-
def add_transform(
|
|
435
|
-
|
|
476
|
+
def add_transform(
|
|
477
|
+
self,
|
|
478
|
+
destination: Stream[U],
|
|
479
|
+
transformation: Callable[[DeadLetterModel[T]], ZeroOrMany[U]],
|
|
480
|
+
config: TransformConfig = None,
|
|
481
|
+
):
|
|
436
482
|
def wrapped_transform(record: DeadLetterModel[T]):
|
|
437
483
|
record._t = self._model_type
|
|
438
484
|
return transformation(record)
|
|
@@ -440,7 +486,11 @@ class DeadLetterQueue(Stream, Generic[T]):
|
|
|
440
486
|
config = config or TransformConfig()
|
|
441
487
|
super().add_transform(destination, wrapped_transform, config)
|
|
442
488
|
|
|
443
|
-
def add_consumer(
|
|
489
|
+
def add_consumer(
|
|
490
|
+
self,
|
|
491
|
+
consumer: Callable[[DeadLetterModel[T]], None],
|
|
492
|
+
config: ConsumerConfig = None,
|
|
493
|
+
):
|
|
444
494
|
def wrapped_consumer(record: DeadLetterModel[T]):
|
|
445
495
|
record._t = self._model_type
|
|
446
496
|
return consumer(record)
|
|
@@ -448,7 +498,9 @@ class DeadLetterQueue(Stream, Generic[T]):
|
|
|
448
498
|
config = config or ConsumerConfig()
|
|
449
499
|
super().add_consumer(wrapped_consumer, config)
|
|
450
500
|
|
|
451
|
-
def set_multi_transform(
|
|
501
|
+
def set_multi_transform(
|
|
502
|
+
self, transformation: Callable[[DeadLetterModel[T]], list[_RoutedMessage]]
|
|
503
|
+
):
|
|
452
504
|
def wrapped_transform(record: DeadLetterModel[T]):
|
|
453
505
|
record._t = self._model_type
|
|
454
506
|
return transformation(record)
|
moose_lib/dmv2/types.py
CHANGED
|
@@ -5,6 +5,7 @@ This module provides the core type definitions and base classes used across
|
|
|
5
5
|
the dmv2 package, including generic type parameters, type aliases, and base
|
|
6
6
|
resource classes.
|
|
7
7
|
"""
|
|
8
|
+
|
|
8
9
|
from typing import Any, Generic, TypeVar, Union
|
|
9
10
|
|
|
10
11
|
import typing_extensions
|
|
@@ -12,10 +13,10 @@ from pydantic import BaseModel
|
|
|
12
13
|
from pydantic.fields import FieldInfo
|
|
13
14
|
from ..data_models import Column
|
|
14
15
|
|
|
15
|
-
T = TypeVar(
|
|
16
|
-
U = TypeVar(
|
|
17
|
-
T_none = TypeVar(
|
|
18
|
-
U_none = TypeVar(
|
|
16
|
+
T = TypeVar("T", bound=BaseModel)
|
|
17
|
+
U = TypeVar("U", bound=BaseModel)
|
|
18
|
+
T_none = TypeVar("T_none", bound=Union[BaseModel, None])
|
|
19
|
+
U_none = TypeVar("U_none", bound=Union[BaseModel, None])
|
|
19
20
|
type ZeroOrMany[T] = Union[T, list[T], None]
|
|
20
21
|
|
|
21
22
|
|
|
@@ -33,6 +34,7 @@ class Cols:
|
|
|
33
34
|
>>> print(table.cols.user_id) # Output: a column object
|
|
34
35
|
>>> print(table.cols.non_existent) # Raises AttributeError
|
|
35
36
|
"""
|
|
37
|
+
|
|
36
38
|
_columns: dict[str, Column]
|
|
37
39
|
|
|
38
40
|
def __init__(self, columns: list[Column]):
|
|
@@ -52,7 +54,7 @@ class Cols:
|
|
|
52
54
|
return self.__getattr__(item)
|
|
53
55
|
|
|
54
56
|
|
|
55
|
-
@typing_extensions.deprecated(
|
|
57
|
+
@typing_extensions.deprecated("use cols in OlapTable instead")
|
|
56
58
|
class Columns(Generic[T]):
|
|
57
59
|
"""Provides runtime checked column name access for Moose resources.
|
|
58
60
|
|
|
@@ -70,6 +72,7 @@ class Columns(Generic[T]):
|
|
|
70
72
|
Args:
|
|
71
73
|
model: The Pydantic model type whose fields represent the columns.
|
|
72
74
|
"""
|
|
75
|
+
|
|
73
76
|
_fields: dict[str, FieldInfo]
|
|
74
77
|
|
|
75
78
|
def __init__(self, model: type[T]):
|
|
@@ -90,14 +93,17 @@ class BaseTypedResource(Generic[T]):
|
|
|
90
93
|
Attributes:
|
|
91
94
|
name (str): The name of the Moose resource.
|
|
92
95
|
"""
|
|
96
|
+
|
|
93
97
|
_t: type[T]
|
|
94
98
|
name: str
|
|
95
99
|
|
|
96
100
|
@classmethod
|
|
97
101
|
def _get_type(cls, keyword_args: dict):
|
|
98
|
-
t = keyword_args.get(
|
|
102
|
+
t = keyword_args.get("t")
|
|
99
103
|
if t is None:
|
|
100
|
-
raise ValueError(
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"Use `{cls.__name__}[T](name='...')` to supply the Pydantic model type`"
|
|
106
|
+
)
|
|
101
107
|
if not isinstance(t, type) or not issubclass(t, BaseModel):
|
|
102
108
|
raise ValueError(f"{t} is not a Pydantic model")
|
|
103
109
|
return t
|
|
@@ -130,7 +136,7 @@ class TypedMooseResource(BaseTypedResource, Generic[T]):
|
|
|
130
136
|
"""
|
|
131
137
|
|
|
132
138
|
@property
|
|
133
|
-
@typing_extensions.deprecated(
|
|
139
|
+
@typing_extensions.deprecated("use cols in OlapTable instead", category=None)
|
|
134
140
|
def columns(self):
|
|
135
141
|
return Columns[T](self._t)
|
|
136
142
|
|
moose_lib/dmv2/view.py
CHANGED
|
@@ -4,12 +4,14 @@ View definitions for Moose Data Model v2 (dmv2).
|
|
|
4
4
|
This module provides classes for defining standard SQL Views,
|
|
5
5
|
including their SQL statements and dependencies.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from typing import Union, List, Optional
|
|
8
9
|
from pydantic import BaseModel
|
|
9
10
|
|
|
10
11
|
from .sql_resource import SqlResource
|
|
11
12
|
from .olap_table import OlapTable
|
|
12
13
|
|
|
14
|
+
|
|
13
15
|
class View(SqlResource):
|
|
14
16
|
"""Represents a standard SQL database View.
|
|
15
17
|
|
|
@@ -27,10 +29,15 @@ class View(SqlResource):
|
|
|
27
29
|
pulls_data_from (list[SqlObject]): Source tables/views.
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
|
-
def __init__(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
]
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
name: str,
|
|
35
|
+
select_statement: str,
|
|
36
|
+
base_tables: list[Union[OlapTable, SqlResource]],
|
|
37
|
+
metadata: dict = None,
|
|
38
|
+
):
|
|
39
|
+
setup = [f"CREATE VIEW IF NOT EXISTS {name} AS {select_statement}".strip()]
|
|
35
40
|
teardown = [f"DROP VIEW IF EXISTS {name}"]
|
|
36
|
-
super().__init__(
|
|
41
|
+
super().__init__(
|
|
42
|
+
name, setup, teardown, pulls_data_from=base_tables, metadata=metadata
|
|
43
|
+
)
|
moose_lib/dmv2/web_app.py
CHANGED
|
@@ -5,6 +5,7 @@ This module allows developers to register FastAPI applications as WebApp resourc
|
|
|
5
5
|
that are managed by the Moose infrastructure, similar to other resources like
|
|
6
6
|
OlapTables, Streams, and APIs.
|
|
7
7
|
"""
|
|
8
|
+
|
|
8
9
|
from typing import Optional, Dict, Any
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
|
|
@@ -29,6 +30,7 @@ class WebAppMetadata:
|
|
|
29
30
|
Attributes:
|
|
30
31
|
description: Optional description of the WebApp's purpose.
|
|
31
32
|
"""
|
|
33
|
+
|
|
32
34
|
description: Optional[str] = None
|
|
33
35
|
|
|
34
36
|
|
|
@@ -45,6 +47,7 @@ class WebAppConfig:
|
|
|
45
47
|
inject_moose_utils: Whether to inject MooseClient utilities into requests.
|
|
46
48
|
Defaults to True.
|
|
47
49
|
"""
|
|
50
|
+
|
|
48
51
|
mount_path: str
|
|
49
52
|
metadata: Optional[WebAppMetadata] = None
|
|
50
53
|
inject_moose_utils: bool = True
|
|
@@ -111,7 +114,9 @@ class WebApp:
|
|
|
111
114
|
_web_apps[name] = self
|
|
112
115
|
|
|
113
116
|
@staticmethod
|
|
114
|
-
def _validate(
|
|
117
|
+
def _validate(
|
|
118
|
+
name: str, config: WebAppConfig, existing_web_apps: Dict[str, "WebApp"]
|
|
119
|
+
) -> None:
|
|
115
120
|
"""Validate WebApp configuration.
|
|
116
121
|
|
|
117
122
|
Args:
|
|
@@ -129,7 +134,7 @@ class WebApp:
|
|
|
129
134
|
# Validate mountPath - it is required
|
|
130
135
|
if not config.mount_path:
|
|
131
136
|
raise ValueError(
|
|
132
|
-
f
|
|
137
|
+
f'mountPath is required. Please specify a mount path for your WebApp (e.g., "/myapi").'
|
|
133
138
|
)
|
|
134
139
|
|
|
135
140
|
mount_path = config.mount_path
|
|
@@ -137,7 +142,7 @@ class WebApp:
|
|
|
137
142
|
# Check for root path - not allowed as it would overlap reserved paths
|
|
138
143
|
if mount_path == "/":
|
|
139
144
|
raise ValueError(
|
|
140
|
-
f
|
|
145
|
+
f'mountPath cannot be "/" as it would allow routes to overlap with reserved paths: '
|
|
141
146
|
f"{', '.join(RESERVED_MOUNT_PATHS)}"
|
|
142
147
|
)
|
|
143
148
|
|
|
@@ -154,7 +159,7 @@ class WebApp:
|
|
|
154
159
|
raise ValueError(
|
|
155
160
|
f"mountPath cannot begin with a reserved path: "
|
|
156
161
|
f"{', '.join(RESERVED_MOUNT_PATHS)}. "
|
|
157
|
-
f
|
|
162
|
+
f'Got: "{mount_path}"'
|
|
158
163
|
)
|
|
159
164
|
|
|
160
165
|
# Check for duplicate mount path
|
|
@@ -162,8 +167,8 @@ class WebApp:
|
|
|
162
167
|
existing_mount = existing_app.config.mount_path
|
|
163
168
|
if existing_mount == mount_path:
|
|
164
169
|
raise ValueError(
|
|
165
|
-
f
|
|
166
|
-
f
|
|
170
|
+
f'WebApp with mountPath "{mount_path}" already exists '
|
|
171
|
+
f'(used by WebApp "{existing_name}")'
|
|
167
172
|
)
|
|
168
173
|
|
|
169
174
|
def __repr__(self) -> str:
|
|
@@ -4,6 +4,7 @@ Helper utilities for WebApp integration with FastAPI.
|
|
|
4
4
|
This module provides utilities to access Moose services (ClickHouse, Temporal)
|
|
5
5
|
from within FastAPI request handlers.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from typing import Optional, Any, Dict
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
|
|
@@ -17,6 +18,7 @@ class ApiUtil:
|
|
|
17
18
|
sql: SQL template function for building safe queries.
|
|
18
19
|
jwt: JWT payload if authentication is enabled, None otherwise.
|
|
19
20
|
"""
|
|
21
|
+
|
|
20
22
|
client: Any # MooseClient, typed as Any to avoid circular import
|
|
21
23
|
sql: Any # sql function from moose_lib.main
|
|
22
24
|
jwt: Optional[Dict[str, Any]] = None
|
|
@@ -55,7 +57,7 @@ def get_moose_utils(request: Any) -> Optional[ApiUtil]:
|
|
|
55
57
|
```
|
|
56
58
|
"""
|
|
57
59
|
# FastAPI uses request.state for storing custom data
|
|
58
|
-
if hasattr(request,
|
|
60
|
+
if hasattr(request, "state") and hasattr(request.state, "moose"):
|
|
59
61
|
return request.state.moose
|
|
60
62
|
return None
|
|
61
63
|
|
|
@@ -83,10 +85,12 @@ def get_moose_dependency():
|
|
|
83
85
|
return result
|
|
84
86
|
```
|
|
85
87
|
"""
|
|
88
|
+
|
|
86
89
|
def moose_dependency(request: Any) -> ApiUtil:
|
|
87
90
|
moose = get_moose_utils(request)
|
|
88
91
|
if moose is None:
|
|
89
92
|
# This should rarely happen if inject_moose_utils=True
|
|
90
93
|
raise RuntimeError("Moose utilities not available in request")
|
|
91
94
|
return moose
|
|
95
|
+
|
|
92
96
|
return moose_dependency
|