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
tests/test_secrets.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Tests for the moose_lib.secrets module.
|
|
2
|
+
|
|
3
|
+
This module tests the runtime environment variable marker functionality,
|
|
4
|
+
which allows users to defer secret resolution until runtime rather than
|
|
5
|
+
embedding secrets at build time.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import pytest
|
|
10
|
+
from moose_lib.secrets import moose_runtime_env, get, MOOSE_RUNTIME_ENV_PREFIX
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture(scope="module", autouse=True)
|
|
14
|
+
def set_infra_map_loading_for_secrets_tests():
|
|
15
|
+
"""Set IS_LOADING_INFRA_MAP=true for secrets tests so moose_runtime_env.get() returns markers."""
|
|
16
|
+
os.environ["IS_LOADING_INFRA_MAP"] = "true"
|
|
17
|
+
yield
|
|
18
|
+
# Clean up after all tests in this module
|
|
19
|
+
os.environ.pop("IS_LOADING_INFRA_MAP", None)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestMooseRuntimeEnvGet:
|
|
23
|
+
"""Tests for the moose_runtime_env.get() method."""
|
|
24
|
+
|
|
25
|
+
def test_creates_marker_with_correct_prefix(self):
|
|
26
|
+
"""Should create a marker string with the correct prefix."""
|
|
27
|
+
var_name = "AWS_ACCESS_KEY_ID"
|
|
28
|
+
result = moose_runtime_env.get(var_name)
|
|
29
|
+
|
|
30
|
+
assert result == f"{MOOSE_RUNTIME_ENV_PREFIX}{var_name}"
|
|
31
|
+
assert result == "__MOOSE_RUNTIME_ENV__:AWS_ACCESS_KEY_ID"
|
|
32
|
+
|
|
33
|
+
def test_handles_different_variable_names(self):
|
|
34
|
+
"""Should handle different environment variable names correctly."""
|
|
35
|
+
test_cases = [
|
|
36
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
37
|
+
"DATABASE_PASSWORD",
|
|
38
|
+
"API_KEY",
|
|
39
|
+
"MY_CUSTOM_SECRET",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
for var_name in test_cases:
|
|
43
|
+
result = moose_runtime_env.get(var_name)
|
|
44
|
+
assert result == f"{MOOSE_RUNTIME_ENV_PREFIX}{var_name}"
|
|
45
|
+
assert var_name in result
|
|
46
|
+
|
|
47
|
+
def test_raises_error_for_empty_string(self):
|
|
48
|
+
"""Should raise ValueError for empty string."""
|
|
49
|
+
with pytest.raises(
|
|
50
|
+
ValueError, match="Environment variable name cannot be empty"
|
|
51
|
+
):
|
|
52
|
+
moose_runtime_env.get("")
|
|
53
|
+
|
|
54
|
+
def test_raises_error_for_whitespace_only(self):
|
|
55
|
+
"""Should raise ValueError for whitespace-only string."""
|
|
56
|
+
with pytest.raises(
|
|
57
|
+
ValueError, match="Environment variable name cannot be empty"
|
|
58
|
+
):
|
|
59
|
+
moose_runtime_env.get(" ")
|
|
60
|
+
|
|
61
|
+
def test_raises_error_for_tabs_only(self):
|
|
62
|
+
"""Should raise ValueError for string with only tabs."""
|
|
63
|
+
with pytest.raises(
|
|
64
|
+
ValueError, match="Environment variable name cannot be empty"
|
|
65
|
+
):
|
|
66
|
+
moose_runtime_env.get("\t\t")
|
|
67
|
+
|
|
68
|
+
def test_allows_underscores_in_variable_names(self):
|
|
69
|
+
"""Should allow variable names with underscores."""
|
|
70
|
+
var_name = "MY_LONG_VAR_NAME"
|
|
71
|
+
result = moose_runtime_env.get(var_name)
|
|
72
|
+
|
|
73
|
+
assert result == f"{MOOSE_RUNTIME_ENV_PREFIX}{var_name}"
|
|
74
|
+
|
|
75
|
+
def test_allows_numbers_in_variable_names(self):
|
|
76
|
+
"""Should allow variable names with numbers."""
|
|
77
|
+
var_name = "API_KEY_123"
|
|
78
|
+
result = moose_runtime_env.get(var_name)
|
|
79
|
+
|
|
80
|
+
assert result == f"{MOOSE_RUNTIME_ENV_PREFIX}{var_name}"
|
|
81
|
+
|
|
82
|
+
def test_preserves_exact_casing(self):
|
|
83
|
+
"""Should preserve exact variable name casing."""
|
|
84
|
+
var_name = "MixedCase_VarName"
|
|
85
|
+
result = moose_runtime_env.get(var_name)
|
|
86
|
+
|
|
87
|
+
assert var_name in result
|
|
88
|
+
assert var_name.lower() not in result # Ensure casing wasn't changed
|
|
89
|
+
|
|
90
|
+
def test_can_be_used_in_s3queue_config(self):
|
|
91
|
+
"""Should create markers that can be used in S3Queue configuration."""
|
|
92
|
+
access_key_marker = moose_runtime_env.get("AWS_ACCESS_KEY_ID")
|
|
93
|
+
secret_key_marker = moose_runtime_env.get("AWS_SECRET_ACCESS_KEY")
|
|
94
|
+
|
|
95
|
+
config = {
|
|
96
|
+
"aws_access_key_id": access_key_marker,
|
|
97
|
+
"aws_secret_access_key": secret_key_marker,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
assert "AWS_ACCESS_KEY_ID" in config["aws_access_key_id"]
|
|
101
|
+
assert "AWS_SECRET_ACCESS_KEY" in config["aws_secret_access_key"]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TestModuleLevelGetFunction:
|
|
105
|
+
"""Tests for the module-level get() function."""
|
|
106
|
+
|
|
107
|
+
def test_module_level_get_creates_marker(self):
|
|
108
|
+
"""The module-level get function should create markers."""
|
|
109
|
+
var_name = "TEST_SECRET"
|
|
110
|
+
result = get(var_name)
|
|
111
|
+
|
|
112
|
+
assert result == f"{MOOSE_RUNTIME_ENV_PREFIX}{var_name}"
|
|
113
|
+
|
|
114
|
+
def test_module_level_get_matches_class_method(self):
|
|
115
|
+
"""Module-level get should produce same result as class method."""
|
|
116
|
+
var_name = "MY_SECRET"
|
|
117
|
+
|
|
118
|
+
result_module = get(var_name)
|
|
119
|
+
result_class = moose_runtime_env.get(var_name)
|
|
120
|
+
|
|
121
|
+
assert result_module == result_class
|
|
122
|
+
|
|
123
|
+
def test_module_level_get_raises_error_for_empty(self):
|
|
124
|
+
"""Module-level get should raise ValueError for empty string."""
|
|
125
|
+
with pytest.raises(
|
|
126
|
+
ValueError, match="Environment variable name cannot be empty"
|
|
127
|
+
):
|
|
128
|
+
get("")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TestMooseRuntimeEnvPrefix:
|
|
132
|
+
"""Tests for the MOOSE_RUNTIME_ENV_PREFIX constant."""
|
|
133
|
+
|
|
134
|
+
def test_has_expected_value(self):
|
|
135
|
+
"""Should have the expected prefix value."""
|
|
136
|
+
assert MOOSE_RUNTIME_ENV_PREFIX == "__MOOSE_RUNTIME_ENV__:"
|
|
137
|
+
|
|
138
|
+
def test_is_string(self):
|
|
139
|
+
"""Should be a string."""
|
|
140
|
+
assert isinstance(MOOSE_RUNTIME_ENV_PREFIX, str)
|
|
141
|
+
|
|
142
|
+
def test_is_not_empty(self):
|
|
143
|
+
"""Should not be empty."""
|
|
144
|
+
assert len(MOOSE_RUNTIME_ENV_PREFIX) > 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestMarkerFormatValidation:
|
|
148
|
+
"""Tests for marker format validation and parsing."""
|
|
149
|
+
|
|
150
|
+
def test_creates_easily_detectable_markers(self):
|
|
151
|
+
"""Should create markers that are easily detectable."""
|
|
152
|
+
marker = moose_runtime_env.get("TEST_VAR")
|
|
153
|
+
|
|
154
|
+
assert marker.startswith("__MOOSE_RUNTIME_ENV__:")
|
|
155
|
+
|
|
156
|
+
def test_markers_can_be_split_to_extract_variable_name(self):
|
|
157
|
+
"""Should create markers that can be split to extract variable name."""
|
|
158
|
+
var_name = "MY_SECRET"
|
|
159
|
+
marker = moose_runtime_env.get(var_name)
|
|
160
|
+
|
|
161
|
+
parts = marker.split(MOOSE_RUNTIME_ENV_PREFIX)
|
|
162
|
+
assert len(parts) == 2
|
|
163
|
+
assert parts[1] == var_name
|
|
164
|
+
|
|
165
|
+
def test_markers_are_json_serializable(self):
|
|
166
|
+
"""Should create markers that are JSON serializable."""
|
|
167
|
+
import json
|
|
168
|
+
|
|
169
|
+
marker = moose_runtime_env.get("TEST_VAR")
|
|
170
|
+
json_str = json.dumps({"secret": marker})
|
|
171
|
+
parsed = json.loads(json_str)
|
|
172
|
+
|
|
173
|
+
assert parsed["secret"] == marker
|
|
174
|
+
|
|
175
|
+
def test_markers_work_with_dict_serialization(self):
|
|
176
|
+
"""Should work correctly with dictionary serialization."""
|
|
177
|
+
marker = moose_runtime_env.get("DATABASE_PASSWORD")
|
|
178
|
+
|
|
179
|
+
config = {"password": marker, "other_field": "value"}
|
|
180
|
+
|
|
181
|
+
# Verify the marker is preserved in the dict
|
|
182
|
+
assert config["password"] == marker
|
|
183
|
+
assert MOOSE_RUNTIME_ENV_PREFIX in config["password"]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestIntegrationScenarios:
|
|
187
|
+
"""Integration tests for real-world usage scenarios."""
|
|
188
|
+
|
|
189
|
+
def test_s3queue_engine_with_secrets(self):
|
|
190
|
+
"""Should work correctly in S3Queue engine configuration."""
|
|
191
|
+
from moose_lib.blocks import S3QueueEngine
|
|
192
|
+
|
|
193
|
+
engine = S3QueueEngine(
|
|
194
|
+
s3_path="s3://my-bucket/data/*.json",
|
|
195
|
+
format="JSONEachRow",
|
|
196
|
+
aws_access_key_id=moose_runtime_env.get("AWS_ACCESS_KEY_ID"),
|
|
197
|
+
aws_secret_access_key=moose_runtime_env.get("AWS_SECRET_ACCESS_KEY"),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Verify markers were set correctly
|
|
201
|
+
assert engine.aws_access_key_id == "__MOOSE_RUNTIME_ENV__:AWS_ACCESS_KEY_ID"
|
|
202
|
+
assert (
|
|
203
|
+
engine.aws_secret_access_key
|
|
204
|
+
== "__MOOSE_RUNTIME_ENV__:AWS_SECRET_ACCESS_KEY"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def test_multiple_secrets_in_same_config(self):
|
|
208
|
+
"""Should handle multiple secrets in the same configuration."""
|
|
209
|
+
config = {
|
|
210
|
+
"username": moose_runtime_env.get("DB_USERNAME"),
|
|
211
|
+
"password": moose_runtime_env.get("DB_PASSWORD"),
|
|
212
|
+
"api_key": moose_runtime_env.get("API_KEY"),
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# All should have the correct prefix
|
|
216
|
+
for value in config.values():
|
|
217
|
+
assert value.startswith(MOOSE_RUNTIME_ENV_PREFIX)
|
|
218
|
+
|
|
219
|
+
# Each should have the correct variable name
|
|
220
|
+
assert "DB_USERNAME" in config["username"]
|
|
221
|
+
assert "DB_PASSWORD" in config["password"]
|
|
222
|
+
assert "API_KEY" in config["api_key"]
|
|
223
|
+
|
|
224
|
+
def test_mixed_secret_and_plain_values(self):
|
|
225
|
+
"""Should handle mix of secret markers and plain values."""
|
|
226
|
+
config = {
|
|
227
|
+
"region": "us-east-1", # Plain value
|
|
228
|
+
"access_key": moose_runtime_env.get("AWS_ACCESS_KEY_ID"), # Secret
|
|
229
|
+
"bucket": "my-bucket", # Plain value
|
|
230
|
+
"secret_key": moose_runtime_env.get("AWS_SECRET_ACCESS_KEY"), # Secret
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# Plain values should be unchanged
|
|
234
|
+
assert config["region"] == "us-east-1"
|
|
235
|
+
assert config["bucket"] == "my-bucket"
|
|
236
|
+
|
|
237
|
+
# Secrets should have markers
|
|
238
|
+
assert MOOSE_RUNTIME_ENV_PREFIX in config["access_key"]
|
|
239
|
+
assert MOOSE_RUNTIME_ENV_PREFIX in config["secret_key"]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from moose_lib import simple_aggregated, Key
|
|
5
|
+
from moose_lib.data_models import SimpleAggregateFunction, _to_columns
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_simple_aggregated_helper():
|
|
9
|
+
"""Test that simple_aggregated helper creates correct annotation"""
|
|
10
|
+
annotated_type = simple_aggregated("sum", int)
|
|
11
|
+
|
|
12
|
+
# Check that it's annotated
|
|
13
|
+
assert hasattr(annotated_type, "__metadata__")
|
|
14
|
+
metadata = annotated_type.__metadata__[0]
|
|
15
|
+
|
|
16
|
+
# Check metadata is SimpleAggregateFunction instance
|
|
17
|
+
assert isinstance(metadata, SimpleAggregateFunction)
|
|
18
|
+
assert metadata.agg_func == "sum"
|
|
19
|
+
assert metadata.arg_type == int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_simple_aggregate_function_to_dict():
|
|
23
|
+
"""Test that SimpleAggregateFunction.to_dict() creates correct structure"""
|
|
24
|
+
func = SimpleAggregateFunction(agg_func="sum", arg_type=int)
|
|
25
|
+
result = func.to_dict()
|
|
26
|
+
|
|
27
|
+
assert result["functionName"] == "sum"
|
|
28
|
+
assert "argumentType" in result
|
|
29
|
+
# unless Annotated, Python int becomes `Int64`
|
|
30
|
+
assert result["argumentType"] == "Int64"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_simple_aggregate_function_to_dict_with_different_types():
|
|
34
|
+
"""Test SimpleAggregateFunction.to_dict() with various types"""
|
|
35
|
+
# Test with float
|
|
36
|
+
func_float = SimpleAggregateFunction(agg_func="max", arg_type=float)
|
|
37
|
+
result_float = func_float.to_dict()
|
|
38
|
+
assert result_float["functionName"] == "max"
|
|
39
|
+
assert result_float["argumentType"] == "Float64"
|
|
40
|
+
|
|
41
|
+
# Test with str
|
|
42
|
+
func_str = SimpleAggregateFunction(agg_func="anyLast", arg_type=str)
|
|
43
|
+
result_str = func_str.to_dict()
|
|
44
|
+
assert result_str["functionName"] == "anyLast"
|
|
45
|
+
assert result_str["argumentType"] == "String"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_dataclass_with_simple_aggregated():
|
|
49
|
+
"""Test that BaseModel with simple_aggregated field converts correctly"""
|
|
50
|
+
|
|
51
|
+
class TestModel(BaseModel):
|
|
52
|
+
date_stamp: Key[datetime.datetime]
|
|
53
|
+
table_name: Key[str]
|
|
54
|
+
row_count: simple_aggregated("sum", int)
|
|
55
|
+
|
|
56
|
+
columns = _to_columns(TestModel)
|
|
57
|
+
|
|
58
|
+
# Find the row_count column
|
|
59
|
+
row_count_col = next(c for c in columns if c.name == "row_count")
|
|
60
|
+
|
|
61
|
+
# Check basic type - Python int maps to "Int64" by default
|
|
62
|
+
assert row_count_col.data_type == "Int64"
|
|
63
|
+
|
|
64
|
+
# Check annotation
|
|
65
|
+
simple_agg_annotation = next(
|
|
66
|
+
(a for a in row_count_col.annotations if a[0] == "simpleAggregationFunction"),
|
|
67
|
+
None,
|
|
68
|
+
)
|
|
69
|
+
assert simple_agg_annotation is not None
|
|
70
|
+
assert simple_agg_annotation[1]["functionName"] == "sum"
|
|
71
|
+
assert simple_agg_annotation[1]["argumentType"] == "Int64"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_multiple_simple_aggregated_fields():
|
|
75
|
+
"""Test BaseModel with multiple SimpleAggregateFunction fields"""
|
|
76
|
+
|
|
77
|
+
class StatsModel(BaseModel):
|
|
78
|
+
timestamp: Key[datetime.datetime]
|
|
79
|
+
total_count: simple_aggregated("sum", int)
|
|
80
|
+
max_value: simple_aggregated("max", int)
|
|
81
|
+
min_value: simple_aggregated("min", int)
|
|
82
|
+
last_seen: simple_aggregated("anyLast", datetime.datetime)
|
|
83
|
+
|
|
84
|
+
columns = _to_columns(StatsModel)
|
|
85
|
+
|
|
86
|
+
# Test sum
|
|
87
|
+
sum_col = next(c for c in columns if c.name == "total_count")
|
|
88
|
+
sum_annotation = next(
|
|
89
|
+
a for a in sum_col.annotations if a[0] == "simpleAggregationFunction"
|
|
90
|
+
)
|
|
91
|
+
assert sum_annotation[1]["functionName"] == "sum"
|
|
92
|
+
|
|
93
|
+
# Test max
|
|
94
|
+
max_col = next(c for c in columns if c.name == "max_value")
|
|
95
|
+
max_annotation = next(
|
|
96
|
+
a for a in max_col.annotations if a[0] == "simpleAggregationFunction"
|
|
97
|
+
)
|
|
98
|
+
assert max_annotation[1]["functionName"] == "max"
|
|
99
|
+
|
|
100
|
+
# Test min
|
|
101
|
+
min_col = next(c for c in columns if c.name == "min_value")
|
|
102
|
+
min_annotation = next(
|
|
103
|
+
a for a in min_col.annotations if a[0] == "simpleAggregationFunction"
|
|
104
|
+
)
|
|
105
|
+
assert min_annotation[1]["functionName"] == "min"
|
|
106
|
+
|
|
107
|
+
# Test anyLast with datetime
|
|
108
|
+
last_col = next(c for c in columns if c.name == "last_seen")
|
|
109
|
+
assert last_col.data_type == "DateTime"
|
|
110
|
+
last_annotation = next(
|
|
111
|
+
a for a in last_col.annotations if a[0] == "simpleAggregationFunction"
|
|
112
|
+
)
|
|
113
|
+
assert last_annotation[1]["functionName"] == "anyLast"
|
|
114
|
+
assert last_annotation[1]["argumentType"] == "DateTime"
|
tests/test_web_app.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for WebApp SDK functionality.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from moose_lib.dmv2 import WebApp, WebAppConfig, WebAppMetadata
|
|
7
|
+
from moose_lib.dmv2._registry import _web_apps
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Mock FastAPI app for testing
|
|
11
|
+
class MockFastAPIApp:
|
|
12
|
+
"""Mock FastAPI application for testing."""
|
|
13
|
+
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(autouse=True)
|
|
18
|
+
def clear_registry():
|
|
19
|
+
"""Clear the WebApp registry before each test."""
|
|
20
|
+
_web_apps.clear()
|
|
21
|
+
yield
|
|
22
|
+
_web_apps.clear()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_webapp_basic_creation():
|
|
26
|
+
"""Test basic WebApp creation with required mount_path."""
|
|
27
|
+
app = MockFastAPIApp()
|
|
28
|
+
config = WebAppConfig(mount_path="/test")
|
|
29
|
+
webapp = WebApp("test_app", app, config)
|
|
30
|
+
|
|
31
|
+
assert webapp.name == "test_app"
|
|
32
|
+
assert webapp.app is app
|
|
33
|
+
assert webapp.config.mount_path == "/test"
|
|
34
|
+
assert webapp.config.inject_moose_utils is True
|
|
35
|
+
assert "test_app" in _web_apps
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_webapp_with_custom_mount_path():
|
|
39
|
+
"""Test WebApp with custom mount path."""
|
|
40
|
+
app = MockFastAPIApp()
|
|
41
|
+
config = WebAppConfig(mount_path="/myapi")
|
|
42
|
+
webapp = WebApp("test_app", app, config)
|
|
43
|
+
|
|
44
|
+
assert webapp.config.mount_path == "/myapi"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_webapp_with_metadata():
|
|
48
|
+
"""Test WebApp with metadata."""
|
|
49
|
+
app = MockFastAPIApp()
|
|
50
|
+
config = WebAppConfig(
|
|
51
|
+
mount_path="/api",
|
|
52
|
+
metadata=WebAppMetadata(description="My API"),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
with pytest.raises(ValueError, match="cannot begin with a reserved path"):
|
|
56
|
+
WebApp("test_app", app, config)
|
|
57
|
+
|
|
58
|
+
# Now test with valid mount path
|
|
59
|
+
config.mount_path = "/myapi"
|
|
60
|
+
webapp = WebApp("test_app", app, config)
|
|
61
|
+
assert webapp.config.metadata.description == "My API"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_webapp_duplicate_name():
|
|
65
|
+
"""Test that duplicate WebApp names are rejected."""
|
|
66
|
+
app1 = MockFastAPIApp()
|
|
67
|
+
app2 = MockFastAPIApp()
|
|
68
|
+
|
|
69
|
+
WebApp("test_app", app1, WebAppConfig(mount_path="/test1"))
|
|
70
|
+
|
|
71
|
+
with pytest.raises(ValueError, match="WebApp with name 'test_app' already exists"):
|
|
72
|
+
WebApp("test_app", app2, WebAppConfig(mount_path="/test2"))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_webapp_trailing_slash_validation():
|
|
76
|
+
"""Test that trailing slashes are rejected."""
|
|
77
|
+
app = MockFastAPIApp()
|
|
78
|
+
config = WebAppConfig(mount_path="/myapi/")
|
|
79
|
+
|
|
80
|
+
with pytest.raises(ValueError, match="mountPath cannot end with a trailing slash"):
|
|
81
|
+
WebApp("test_app", app, config)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_webapp_root_path_rejected():
|
|
85
|
+
"""Test that root path '/' is rejected to prevent overlap with reserved paths."""
|
|
86
|
+
app = MockFastAPIApp()
|
|
87
|
+
config = WebAppConfig(mount_path="/")
|
|
88
|
+
|
|
89
|
+
with pytest.raises(
|
|
90
|
+
ValueError,
|
|
91
|
+
match='mountPath cannot be "/" as it would allow routes to overlap with reserved paths',
|
|
92
|
+
):
|
|
93
|
+
WebApp("test_app", app, config)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_webapp_reserved_paths():
|
|
97
|
+
"""Test that reserved paths are rejected."""
|
|
98
|
+
reserved_paths = [
|
|
99
|
+
"/admin",
|
|
100
|
+
"/api",
|
|
101
|
+
"/consumption",
|
|
102
|
+
"/health",
|
|
103
|
+
"/ingest",
|
|
104
|
+
"/moose",
|
|
105
|
+
"/ready",
|
|
106
|
+
"/workflows",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
for path in reserved_paths:
|
|
110
|
+
app = MockFastAPIApp()
|
|
111
|
+
config = WebAppConfig(mount_path=path)
|
|
112
|
+
|
|
113
|
+
with pytest.raises(ValueError, match="cannot begin with a reserved path"):
|
|
114
|
+
WebApp(f"test_{path}", app, config)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_webapp_reserved_path_prefix():
|
|
118
|
+
"""Test that paths starting with reserved prefixes are rejected."""
|
|
119
|
+
app = MockFastAPIApp()
|
|
120
|
+
config = WebAppConfig(mount_path="/api/v1")
|
|
121
|
+
|
|
122
|
+
with pytest.raises(ValueError, match="cannot begin with a reserved path"):
|
|
123
|
+
WebApp("test_app", app, config)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_webapp_duplicate_mount_path():
|
|
127
|
+
"""Test that duplicate mount paths are rejected."""
|
|
128
|
+
app1 = MockFastAPIApp()
|
|
129
|
+
app2 = MockFastAPIApp()
|
|
130
|
+
|
|
131
|
+
config1 = WebAppConfig(mount_path="/myapi")
|
|
132
|
+
WebApp("app1", app1, config1)
|
|
133
|
+
|
|
134
|
+
config2 = WebAppConfig(mount_path="/myapi")
|
|
135
|
+
with pytest.raises(
|
|
136
|
+
ValueError, match='WebApp with mountPath "/myapi" already exists'
|
|
137
|
+
):
|
|
138
|
+
WebApp("app2", app2, config2)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_webapp_different_mount_paths():
|
|
142
|
+
"""Test that WebApps with different mount paths can coexist."""
|
|
143
|
+
app1 = MockFastAPIApp()
|
|
144
|
+
app2 = MockFastAPIApp()
|
|
145
|
+
|
|
146
|
+
WebApp("app1", app1, WebAppConfig(mount_path="/api1"))
|
|
147
|
+
WebApp("app2", app2, WebAppConfig(mount_path="/api2"))
|
|
148
|
+
|
|
149
|
+
assert len(_web_apps) == 2
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_webapp_inject_moose_utils_false():
|
|
153
|
+
"""Test WebApp with inject_moose_utils disabled."""
|
|
154
|
+
app = MockFastAPIApp()
|
|
155
|
+
config = WebAppConfig(mount_path="/test", inject_moose_utils=False)
|
|
156
|
+
webapp = WebApp("test_app", app, config)
|
|
157
|
+
|
|
158
|
+
assert webapp.config.inject_moose_utils is False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_webapp_repr():
|
|
162
|
+
"""Test WebApp string representation."""
|
|
163
|
+
app = MockFastAPIApp()
|
|
164
|
+
webapp = WebApp("test_app", app, WebAppConfig(mount_path="/myapi"))
|
|
165
|
+
|
|
166
|
+
assert "test_app" in repr(webapp)
|
|
167
|
+
assert "/myapi" in repr(webapp)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_webapp_mount_path_required():
|
|
171
|
+
"""Test that mount_path is required."""
|
|
172
|
+
app = MockFastAPIApp()
|
|
173
|
+
|
|
174
|
+
with pytest.raises(ValueError, match="mountPath is required"):
|
|
175
|
+
WebApp("test_app", app, WebAppConfig(mount_path=""))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_webapp_serialization():
|
|
179
|
+
"""Test that WebApps can be serialized via internal.py."""
|
|
180
|
+
from moose_lib.internal import to_infra_map
|
|
181
|
+
from moose_lib.dmv2 import get_web_apps
|
|
182
|
+
|
|
183
|
+
app = MockFastAPIApp()
|
|
184
|
+
WebApp(
|
|
185
|
+
"test_app",
|
|
186
|
+
app,
|
|
187
|
+
WebAppConfig(
|
|
188
|
+
mount_path="/myapi", metadata=WebAppMetadata(description="Test API")
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Verify it's in the registry
|
|
193
|
+
web_apps = get_web_apps()
|
|
194
|
+
assert "test_app" in web_apps
|
|
195
|
+
|
|
196
|
+
# Serialize to infra map
|
|
197
|
+
infra_map = to_infra_map()
|
|
198
|
+
|
|
199
|
+
assert "webApps" in infra_map
|
|
200
|
+
assert "test_app" in infra_map["webApps"]
|
|
201
|
+
assert infra_map["webApps"]["test_app"]["name"] == "test_app"
|
|
202
|
+
assert infra_map["webApps"]["test_app"]["mountPath"] == "/myapi"
|
|
203
|
+
assert infra_map["webApps"]["test_app"]["metadata"]["description"] == "Test API"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_webapp_serialization_with_mount_path():
|
|
207
|
+
"""Test WebApp serialization with explicit mount path."""
|
|
208
|
+
from moose_lib.internal import to_infra_map
|
|
209
|
+
|
|
210
|
+
app = MockFastAPIApp()
|
|
211
|
+
WebApp("test_app", app, WebAppConfig(mount_path="/testpath"))
|
|
212
|
+
|
|
213
|
+
infra_map = to_infra_map()
|
|
214
|
+
|
|
215
|
+
assert infra_map["webApps"]["test_app"]["mountPath"] == "/testpath"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_webapp_serialization_no_metadata():
|
|
219
|
+
"""Test WebApp serialization without metadata."""
|
|
220
|
+
from moose_lib.internal import to_infra_map
|
|
221
|
+
|
|
222
|
+
app = MockFastAPIApp()
|
|
223
|
+
WebApp("test_app", app, WebAppConfig(mount_path="/myapi"))
|
|
224
|
+
|
|
225
|
+
infra_map = to_infra_map()
|
|
226
|
+
|
|
227
|
+
assert infra_map["webApps"]["test_app"]["metadata"] is None
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
moose_lib/__init__.py,sha256=RyZnXIB7-eo-pxTXvKE8tqK9IHJuI5CmBadFY0nI9n4,766
|
|
2
|
-
moose_lib/blocks.py,sha256=78u_ufWJmObijMMjLe7pDrZtrCVkeyNBe78mKgWVsMA,9789
|
|
3
|
-
moose_lib/commons.py,sha256=FUpRv8D3-LeGcjhcqtDyiimz5izwpCq53h50ydxC_uA,3711
|
|
4
|
-
moose_lib/data_models.py,sha256=TMa6t9wLBKhHk1-KjCHDzt2SFS9Eo-ALnKUHEQUSkU0,10910
|
|
5
|
-
moose_lib/dmv2_serializer.py,sha256=CL_Pvvg8tJOT8Qk6hywDNzY8MYGhMVdTOw8arZi3jng,49
|
|
6
|
-
moose_lib/internal.py,sha256=ngUsuczYrD7Vzr_WhO23vVwnJMJAegORi5uTw3x9p7U,21139
|
|
7
|
-
moose_lib/main.py,sha256=JWsgza52xEh25AyF61cO1ItJ8VXJHHz8j-4HG445Whg,20380
|
|
8
|
-
moose_lib/query_builder.py,sha256=-L5p2dArBx3SBA-WZPkcCJPemKXnqJw60NHy-wn5wa4,6619
|
|
9
|
-
moose_lib/query_param.py,sha256=kxcR09BMIsEg4o2qetjKrVu1YFRaLfMEzwzyGsKUpvA,6474
|
|
10
|
-
moose_lib/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
moose_lib/clients/redis_client.py,sha256=BDYjJ582V-rW92qVQ4neZ9Pu7JtDNt8x8jBWApt1XUg,11895
|
|
12
|
-
moose_lib/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
moose_lib/config/config_file.py,sha256=NyjY6YFraBel7vBk18lLkpGaPR1viKMAEv4ZldyfLIA,2585
|
|
14
|
-
moose_lib/config/runtime.py,sha256=h4SXRn4Mlbeh02lKN7HX2Mdp3QzySnZOeSvm8G-B3ko,3857
|
|
15
|
-
moose_lib/dmv2/__init__.py,sha256=3DVAtNMZUoP94CMJBFhuXfYEQXDbQUNKSgg9XnKqae0,2768
|
|
16
|
-
moose_lib/dmv2/_registry.py,sha256=gsLuWOvekgQRVvjVjPRHW2hN48LWyOWpLSpS2I-CWHc,708
|
|
17
|
-
moose_lib/dmv2/consumption.py,sha256=sWfGwgCBQIIrhFEbx1CULfoN3rFFoy8uCBUefu4ejiw,12928
|
|
18
|
-
moose_lib/dmv2/ingest_api.py,sha256=XhvHHgGPXp-BuRpAALth-FRhanwy-zJQ_83Cg_RLolM,2586
|
|
19
|
-
moose_lib/dmv2/ingest_pipeline.py,sha256=VOkHaCrmalBGJkWy4iI93WJ_tyaXmHRSSIHBflms6F0,7432
|
|
20
|
-
moose_lib/dmv2/life_cycle.py,sha256=wl0k6yzwU1MJ_fO_UkN29buoY5G6ChYZvfwigP9fVfM,1254
|
|
21
|
-
moose_lib/dmv2/materialized_view.py,sha256=wTam1V5CC2rExt7YrdK_Cz4rRTONm2keKOF951LlCP4,4875
|
|
22
|
-
moose_lib/dmv2/olap_table.py,sha256=YY5zgMrijPnRDaEkM5DbnqSzVRcP8s2bHCFFXISrUE0,34062
|
|
23
|
-
moose_lib/dmv2/registry.py,sha256=SZOXhSC3kizhPHKQN6ZWzoMZHl90AUG3H_WKVev5zIU,2680
|
|
24
|
-
moose_lib/dmv2/sql_resource.py,sha256=kUZoGqxhZMHMthtBZGYJBxTFjXkspXiWLXhJRYXgGUM,1864
|
|
25
|
-
moose_lib/dmv2/stream.py,sha256=IGrUdWBEHjZ9nYz3-TChq-x9P98V9dH6TbamQ2oz_TE,11725
|
|
26
|
-
moose_lib/dmv2/types.py,sha256=3YZ5Kbz2eOn94GWMTYT6Ix69Ekwe6aoUR4DiETQjv9E,4633
|
|
27
|
-
moose_lib/dmv2/view.py,sha256=fVbfbJgc2lvhjpGvpfKcFUqZqxKuLD4X59jdupxIe94,1350
|
|
28
|
-
moose_lib/dmv2/workflow.py,sha256=_FY4-VRo7uWxRtoipxGSo04qzBb4pbP30iQei1W0Ios,6287
|
|
29
|
-
moose_lib/streaming/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
-
moose_lib/streaming/streaming_function_runner.py,sha256=Qv3vTSowo5Y_SeGu2XxcK5NeL_ZW0Um0ZgHxHpZ9VGM,23693
|
|
31
|
-
moose_lib/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
-
moose_lib/utilities/sql.py,sha256=kbg1DT5GdsIwgTsMzbsd6SAVf9aWER8DqmT_eKS3XN4,904
|
|
33
|
-
tests/__init__.py,sha256=0Gh4yzPkkC3TzBGKhenpMIxJcRhyrrCfxLSfpTZnPMQ,53
|
|
34
|
-
tests/conftest.py,sha256=ZVJNbnr4DwbcqkTmePW6U01zAzE6QD0kNAEZjPG1f4s,169
|
|
35
|
-
tests/test_moose.py,sha256=mBsx_OYWmL8ppDzL_7Bd7xR6qf_i3-pCIO3wm2iQNaA,2136
|
|
36
|
-
tests/test_query_builder.py,sha256=O3imdFSaqU13kbK1jSQaHbBgynhVmJaApT8DlRqYwJU,1116
|
|
37
|
-
tests/test_redis_client.py,sha256=d9_MLYsJ4ecVil_jPB2gW3Q5aWnavxmmjZg2uYI3LVo,3256
|
|
38
|
-
tests/test_s3queue_config.py,sha256=F05cnD61S2wBKPabcpEJxf55-DJGF4nLqwBb6aFbprc,9741
|
|
39
|
-
moose_lib-0.6.90.dist-info/METADATA,sha256=HtKY83Dl-nuR8s2XqXt0jEo1e7K8bWSZmUYYshb4GLI,766
|
|
40
|
-
moose_lib-0.6.90.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
41
|
-
moose_lib-0.6.90.dist-info/top_level.txt,sha256=XEns2-4aCmGp2XjJAeEH9TAUcGONLnSLy6ycT9FSJh8,16
|
|
42
|
-
moose_lib-0.6.90.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|