moose-lib 0.6.136__tar.gz → 0.6.138__tar.gz
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-0.6.136 → moose_lib-0.6.138}/PKG-INFO +1 -1
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/__init__.py +24 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/_registry.py +2 -1
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/registry.py +10 -0
- moose_lib-0.6.138/moose_lib/dmv2/web_app.py +170 -0
- moose_lib-0.6.138/moose_lib/dmv2/web_app_helpers.py +92 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/internal.py +45 -1
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib.egg-info/PKG-INFO +1 -1
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib.egg-info/SOURCES.txt +4 -1
- moose_lib-0.6.138/tests/test_web_app.py +221 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/README.md +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/__init__.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/blocks.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/clients/__init__.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/clients/redis_client.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/commons.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/config/__init__.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/config/config_file.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/config/runtime.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/data_models.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/consumption.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/ingest_api.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/ingest_pipeline.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/life_cycle.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/materialized_view.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/olap_table.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/sql_resource.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/stream.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/types.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/view.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/workflow.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2_serializer.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/main.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/query_builder.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/query_param.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/streaming/__init__.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/streaming/streaming_function_runner.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/utilities/__init__.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/utilities/sql.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib.egg-info/dependency_links.txt +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib.egg-info/requires.txt +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib.egg-info/top_level.txt +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/setup.cfg +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/setup.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/__init__.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/conftest.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/test_moose.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/test_olap_table_versioning.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/test_query_builder.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/test_redis_client.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/test_s3queue_config.py +0 -0
- {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/test_simple_aggregate.py +0 -0
|
@@ -80,6 +80,18 @@ from .life_cycle import (
|
|
|
80
80
|
LifeCycle,
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
+
from .web_app import (
|
|
84
|
+
WebApp,
|
|
85
|
+
WebAppConfig,
|
|
86
|
+
WebAppMetadata,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
from .web_app_helpers import (
|
|
90
|
+
ApiUtil,
|
|
91
|
+
get_moose_utils,
|
|
92
|
+
get_moose_dependency,
|
|
93
|
+
)
|
|
94
|
+
|
|
83
95
|
from .registry import (
|
|
84
96
|
get_tables,
|
|
85
97
|
get_table,
|
|
@@ -93,6 +105,8 @@ from .registry import (
|
|
|
93
105
|
get_sql_resource,
|
|
94
106
|
get_workflows,
|
|
95
107
|
get_workflow,
|
|
108
|
+
get_web_apps,
|
|
109
|
+
get_web_app,
|
|
96
110
|
# Backward compatibility aliases
|
|
97
111
|
get_consumption_apis,
|
|
98
112
|
get_consumption_api,
|
|
@@ -158,6 +172,14 @@ __all__ = [
|
|
|
158
172
|
# Lifecycle
|
|
159
173
|
'LifeCycle',
|
|
160
174
|
|
|
175
|
+
# WebApp
|
|
176
|
+
'WebApp',
|
|
177
|
+
'WebAppConfig',
|
|
178
|
+
'WebAppMetadata',
|
|
179
|
+
'ApiUtil',
|
|
180
|
+
'get_moose_utils',
|
|
181
|
+
'get_moose_dependency',
|
|
182
|
+
|
|
161
183
|
# Registry
|
|
162
184
|
'get_tables',
|
|
163
185
|
'get_table',
|
|
@@ -171,6 +193,8 @@ __all__ = [
|
|
|
171
193
|
'get_sql_resource',
|
|
172
194
|
'get_workflows',
|
|
173
195
|
'get_workflow',
|
|
196
|
+
'get_web_apps',
|
|
197
|
+
'get_web_app',
|
|
174
198
|
# Backward compatibility aliases (deprecated)
|
|
175
199
|
'get_consumption_apis',
|
|
176
200
|
'get_consumption_api',
|
|
@@ -16,4 +16,5 @@ _api_name_aliases: Dict[str, Any] = {}
|
|
|
16
16
|
# Map from custom paths to API instances for path-based lookup
|
|
17
17
|
_api_path_map: Dict[str, Any] = {}
|
|
18
18
|
_sql_resources: Dict[str, Any] = {}
|
|
19
|
-
_workflows: Dict[str, Any] = {}
|
|
19
|
+
_workflows: Dict[str, Any] = {}
|
|
20
|
+
_web_apps: Dict[str, Any] = {}
|
|
@@ -11,6 +11,7 @@ from .ingest_api import IngestApi
|
|
|
11
11
|
from .consumption import Api
|
|
12
12
|
from .sql_resource import SqlResource
|
|
13
13
|
from .workflow import Workflow
|
|
14
|
+
from .web_app import WebApp
|
|
14
15
|
from ._registry import (
|
|
15
16
|
_tables,
|
|
16
17
|
_streams,
|
|
@@ -20,6 +21,7 @@ from ._registry import (
|
|
|
20
21
|
_workflows,
|
|
21
22
|
_api_name_aliases,
|
|
22
23
|
_api_path_map,
|
|
24
|
+
_web_apps,
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
def get_tables() -> Dict[str, OlapTable]:
|
|
@@ -87,6 +89,14 @@ def get_workflow(name: str) -> Optional[Workflow]:
|
|
|
87
89
|
"""Get a registered workflow by name."""
|
|
88
90
|
return _workflows.get(name)
|
|
89
91
|
|
|
92
|
+
def get_web_apps() -> Dict[str, WebApp]:
|
|
93
|
+
"""Get all registered WebApps."""
|
|
94
|
+
return _web_apps
|
|
95
|
+
|
|
96
|
+
def get_web_app(name: str) -> Optional[WebApp]:
|
|
97
|
+
"""Get a registered WebApp by name."""
|
|
98
|
+
return _web_apps.get(name)
|
|
99
|
+
|
|
90
100
|
|
|
91
101
|
# Backward compatibility aliases (deprecated)
|
|
92
102
|
get_consumption_apis = get_apis
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
from typing import Optional, Dict, Any
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Reserved mount paths that cannot be used by WebApps
|
|
13
|
+
RESERVED_MOUNT_PATHS = [
|
|
14
|
+
"/admin",
|
|
15
|
+
"/api",
|
|
16
|
+
"/consumption",
|
|
17
|
+
"/health",
|
|
18
|
+
"/ingest",
|
|
19
|
+
"/moose", # reserved for future use
|
|
20
|
+
"/ready",
|
|
21
|
+
"/workflows",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class WebAppMetadata:
|
|
27
|
+
"""Metadata for a WebApp.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
description: Optional description of the WebApp's purpose.
|
|
31
|
+
"""
|
|
32
|
+
description: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class WebAppConfig:
|
|
37
|
+
"""Configuration for a WebApp.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
mount_path: The URL path where the WebApp will be mounted (required).
|
|
41
|
+
Cannot be "/" (root path).
|
|
42
|
+
Cannot end with "/" (trailing slash).
|
|
43
|
+
Cannot start with reserved paths.
|
|
44
|
+
metadata: Optional metadata for documentation purposes.
|
|
45
|
+
inject_moose_utils: Whether to inject MooseClient utilities into requests.
|
|
46
|
+
Defaults to True.
|
|
47
|
+
"""
|
|
48
|
+
mount_path: str
|
|
49
|
+
metadata: Optional[WebAppMetadata] = None
|
|
50
|
+
inject_moose_utils: bool = True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class WebApp:
|
|
54
|
+
"""A WebApp resource that wraps a FastAPI application.
|
|
55
|
+
|
|
56
|
+
WebApps are managed by the Moose infrastructure and automatically
|
|
57
|
+
proxied through the Rust webserver, allowing them to coexist with
|
|
58
|
+
other Moose resources on the same port.
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
```python
|
|
62
|
+
from fastapi import FastAPI, Request
|
|
63
|
+
from moose_lib.dmv2 import WebApp, WebAppConfig, WebAppMetadata
|
|
64
|
+
from moose_lib.dmv2.web_app_helpers import get_moose_utils
|
|
65
|
+
|
|
66
|
+
app = FastAPI()
|
|
67
|
+
|
|
68
|
+
@app.get("/hello")
|
|
69
|
+
async def hello(request: Request):
|
|
70
|
+
moose = get_moose_utils(request)
|
|
71
|
+
# Use moose.client for queries
|
|
72
|
+
return {"message": "Hello World"}
|
|
73
|
+
|
|
74
|
+
# Register as a WebApp with custom mount path
|
|
75
|
+
my_webapp = WebApp(
|
|
76
|
+
"myApi",
|
|
77
|
+
app,
|
|
78
|
+
WebAppConfig(
|
|
79
|
+
mount_path="/myapi",
|
|
80
|
+
metadata=WebAppMetadata(description="My custom API"),
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
name: Unique name for this WebApp.
|
|
87
|
+
app: The FastAPI application instance.
|
|
88
|
+
config: Configuration for the WebApp (required, must include mount_path).
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If validation fails (duplicate name, invalid mount path, etc.)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
name: str,
|
|
97
|
+
app: Any, # FastAPI app, typed as Any to avoid import dependency
|
|
98
|
+
config: WebAppConfig,
|
|
99
|
+
):
|
|
100
|
+
self.name = name
|
|
101
|
+
self.app = app
|
|
102
|
+
self.config = config
|
|
103
|
+
|
|
104
|
+
# Import the registry here to avoid circular dependency
|
|
105
|
+
from ._registry import _web_apps
|
|
106
|
+
|
|
107
|
+
# Validate the configuration
|
|
108
|
+
self._validate(name, self.config, _web_apps)
|
|
109
|
+
|
|
110
|
+
# Register this WebApp
|
|
111
|
+
_web_apps[name] = self
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _validate(name: str, config: WebAppConfig, existing_web_apps: Dict[str, 'WebApp']) -> None:
|
|
115
|
+
"""Validate WebApp configuration.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
name: The name of the WebApp being validated.
|
|
119
|
+
config: The configuration to validate.
|
|
120
|
+
existing_web_apps: Dictionary of already registered WebApps.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
ValueError: If validation fails.
|
|
124
|
+
"""
|
|
125
|
+
# Check for duplicate name
|
|
126
|
+
if name in existing_web_apps:
|
|
127
|
+
raise ValueError(f"WebApp with name '{name}' already exists")
|
|
128
|
+
|
|
129
|
+
# Validate mountPath - it is required
|
|
130
|
+
if not config.mount_path:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"mountPath is required. Please specify a mount path for your WebApp (e.g., \"/myapi\")."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
mount_path = config.mount_path
|
|
136
|
+
|
|
137
|
+
# Check for root path - not allowed as it would overlap reserved paths
|
|
138
|
+
if mount_path == "/":
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"mountPath cannot be \"/\" as it would allow routes to overlap with reserved paths: "
|
|
141
|
+
f"{', '.join(RESERVED_MOUNT_PATHS)}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Validate mount path format
|
|
145
|
+
if mount_path.endswith("/"):
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"mountPath cannot end with a trailing slash. "
|
|
148
|
+
f"Remove the '/' from: \"{mount_path}\""
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Check for reserved path prefixes
|
|
152
|
+
for reserved in RESERVED_MOUNT_PATHS:
|
|
153
|
+
if mount_path == reserved or mount_path.startswith(f"{reserved}/"):
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"mountPath cannot begin with a reserved path: "
|
|
156
|
+
f"{', '.join(RESERVED_MOUNT_PATHS)}. "
|
|
157
|
+
f"Got: \"{mount_path}\""
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Check for duplicate mount path
|
|
161
|
+
for existing_name, existing_app in existing_web_apps.items():
|
|
162
|
+
existing_mount = existing_app.config.mount_path
|
|
163
|
+
if existing_mount == mount_path:
|
|
164
|
+
raise ValueError(
|
|
165
|
+
f"WebApp with mountPath \"{mount_path}\" already exists "
|
|
166
|
+
f"(used by WebApp \"{existing_name}\")"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
return f"WebApp(name='{self.name}', mount_path='{self.config.mount_path}')"
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
from typing import Optional, Any, Dict
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ApiUtil:
|
|
13
|
+
"""Utilities available to WebApp request handlers.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
client: MooseClient instance for executing queries and workflows.
|
|
17
|
+
sql: SQL template function for building safe queries.
|
|
18
|
+
jwt: JWT payload if authentication is enabled, None otherwise.
|
|
19
|
+
"""
|
|
20
|
+
client: Any # MooseClient, typed as Any to avoid circular import
|
|
21
|
+
sql: Any # sql function from moose_lib.main
|
|
22
|
+
jwt: Optional[Dict[str, Any]] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_moose_utils(request: Any) -> Optional[ApiUtil]:
|
|
26
|
+
"""Extract Moose utilities from a FastAPI request.
|
|
27
|
+
|
|
28
|
+
The Moose infrastructure automatically injects utilities into request.state
|
|
29
|
+
when inject_moose_utils is enabled (default).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
request: FastAPI Request object.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
ApiUtil instance if available, None otherwise.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
```python
|
|
39
|
+
from fastapi import FastAPI, Request
|
|
40
|
+
from moose_lib.dmv2.web_app_helpers import get_moose_utils
|
|
41
|
+
|
|
42
|
+
app = FastAPI()
|
|
43
|
+
|
|
44
|
+
@app.get("/data")
|
|
45
|
+
async def get_data(request: Request):
|
|
46
|
+
moose = get_moose_utils(request)
|
|
47
|
+
if not moose:
|
|
48
|
+
return {"error": "Moose utilities not available"}
|
|
49
|
+
|
|
50
|
+
# Execute a query
|
|
51
|
+
result = moose.client.query.execute(
|
|
52
|
+
moose.sql("SELECT * FROM my_table LIMIT {limit}", limit=10)
|
|
53
|
+
)
|
|
54
|
+
return result
|
|
55
|
+
```
|
|
56
|
+
"""
|
|
57
|
+
# FastAPI uses request.state for storing custom data
|
|
58
|
+
if hasattr(request, 'state') and hasattr(request.state, 'moose'):
|
|
59
|
+
return request.state.moose
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_moose_dependency():
|
|
64
|
+
"""FastAPI dependency for injecting Moose utilities.
|
|
65
|
+
|
|
66
|
+
Can be used with FastAPI's Depends() to automatically inject
|
|
67
|
+
Moose utilities into route handlers.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A dependency function that extracts ApiUtil from the request.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
```python
|
|
74
|
+
from fastapi import FastAPI, Depends, Request
|
|
75
|
+
from moose_lib.dmv2.web_app_helpers import get_moose_dependency, ApiUtil
|
|
76
|
+
|
|
77
|
+
app = FastAPI()
|
|
78
|
+
|
|
79
|
+
@app.get("/data")
|
|
80
|
+
async def get_data(moose: ApiUtil = Depends(get_moose_dependency())):
|
|
81
|
+
# moose is automatically injected
|
|
82
|
+
result = moose.client.query.execute(...)
|
|
83
|
+
return result
|
|
84
|
+
```
|
|
85
|
+
"""
|
|
86
|
+
def moose_dependency(request: Any) -> ApiUtil:
|
|
87
|
+
moose = get_moose_utils(request)
|
|
88
|
+
if moose is None:
|
|
89
|
+
# This should rarely happen if inject_moose_utils=True
|
|
90
|
+
raise RuntimeError("Moose utilities not available in request")
|
|
91
|
+
return moose
|
|
92
|
+
return moose_dependency
|
|
@@ -19,6 +19,7 @@ from moose_lib.dmv2 import (
|
|
|
19
19
|
get_apis,
|
|
20
20
|
get_sql_resources,
|
|
21
21
|
get_workflows,
|
|
22
|
+
get_web_apps,
|
|
22
23
|
OlapTable,
|
|
23
24
|
View,
|
|
24
25
|
MaterializedView,
|
|
@@ -265,6 +266,32 @@ class WorkflowJson(BaseModel):
|
|
|
265
266
|
schedule: Optional[str] = None
|
|
266
267
|
|
|
267
268
|
|
|
269
|
+
class WebAppMetadataJson(BaseModel):
|
|
270
|
+
"""Internal representation of WebApp metadata for serialization.
|
|
271
|
+
|
|
272
|
+
Attributes:
|
|
273
|
+
description: Optional description of the WebApp.
|
|
274
|
+
"""
|
|
275
|
+
model_config = model_config
|
|
276
|
+
|
|
277
|
+
description: Optional[str] = None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class WebAppJson(BaseModel):
|
|
281
|
+
"""Internal representation of a WebApp configuration for serialization.
|
|
282
|
+
|
|
283
|
+
Attributes:
|
|
284
|
+
name: Name of the WebApp.
|
|
285
|
+
mount_path: The URL path where the WebApp is mounted.
|
|
286
|
+
metadata: Optional metadata for documentation purposes.
|
|
287
|
+
"""
|
|
288
|
+
model_config = model_config
|
|
289
|
+
|
|
290
|
+
name: str
|
|
291
|
+
mount_path: str
|
|
292
|
+
metadata: Optional[WebAppMetadataJson] = None
|
|
293
|
+
|
|
294
|
+
|
|
268
295
|
class InfrastructureSignatureJson(BaseModel):
|
|
269
296
|
"""Represents the unique signature of an infrastructure component (Table, Topic, etc.).
|
|
270
297
|
|
|
@@ -311,6 +338,7 @@ class InfrastructureMap(BaseModel):
|
|
|
311
338
|
apis: Dictionary mapping API names to their configurations.
|
|
312
339
|
sql_resources: Dictionary mapping SQL resource names to their configurations.
|
|
313
340
|
workflows: Dictionary mapping workflow names to their configurations.
|
|
341
|
+
web_apps: Dictionary mapping WebApp names to their configurations.
|
|
314
342
|
"""
|
|
315
343
|
model_config = model_config
|
|
316
344
|
|
|
@@ -320,6 +348,7 @@ class InfrastructureMap(BaseModel):
|
|
|
320
348
|
apis: dict[str, InternalApiConfig]
|
|
321
349
|
sql_resources: dict[str, SqlResourceConfig]
|
|
322
350
|
workflows: dict[str, WorkflowJson]
|
|
351
|
+
web_apps: dict[str, WebAppJson]
|
|
323
352
|
|
|
324
353
|
|
|
325
354
|
def _map_sql_resource_ref(r: Any) -> InfrastructureSignatureJson:
|
|
@@ -538,6 +567,7 @@ def to_infra_map() -> dict:
|
|
|
538
567
|
apis = {}
|
|
539
568
|
sql_resources = {}
|
|
540
569
|
workflows = {}
|
|
570
|
+
web_apps = {}
|
|
541
571
|
|
|
542
572
|
for _registry_key, table in get_tables().items():
|
|
543
573
|
# Convert engine configuration to new format
|
|
@@ -657,13 +687,27 @@ def to_infra_map() -> dict:
|
|
|
657
687
|
schedule=workflow.config.schedule,
|
|
658
688
|
)
|
|
659
689
|
|
|
690
|
+
for name, web_app in get_web_apps().items():
|
|
691
|
+
mount_path = web_app.config.mount_path or "/"
|
|
692
|
+
metadata = None
|
|
693
|
+
if web_app.config.metadata:
|
|
694
|
+
metadata = WebAppMetadataJson(
|
|
695
|
+
description=web_app.config.metadata.description
|
|
696
|
+
)
|
|
697
|
+
web_apps[name] = WebAppJson(
|
|
698
|
+
name=web_app.name,
|
|
699
|
+
mount_path=mount_path,
|
|
700
|
+
metadata=metadata,
|
|
701
|
+
)
|
|
702
|
+
|
|
660
703
|
infra_map = InfrastructureMap(
|
|
661
704
|
tables=tables,
|
|
662
705
|
topics=topics,
|
|
663
706
|
ingest_apis=ingest_apis,
|
|
664
707
|
apis=apis,
|
|
665
708
|
sql_resources=sql_resources,
|
|
666
|
-
workflows=workflows
|
|
709
|
+
workflows=workflows,
|
|
710
|
+
web_apps=web_apps
|
|
667
711
|
)
|
|
668
712
|
|
|
669
713
|
return infra_map.model_dump(by_alias=True)
|
|
@@ -32,6 +32,8 @@ moose_lib/dmv2/sql_resource.py
|
|
|
32
32
|
moose_lib/dmv2/stream.py
|
|
33
33
|
moose_lib/dmv2/types.py
|
|
34
34
|
moose_lib/dmv2/view.py
|
|
35
|
+
moose_lib/dmv2/web_app.py
|
|
36
|
+
moose_lib/dmv2/web_app_helpers.py
|
|
35
37
|
moose_lib/dmv2/workflow.py
|
|
36
38
|
moose_lib/streaming/__init__.py
|
|
37
39
|
moose_lib/streaming/streaming_function_runner.py
|
|
@@ -44,4 +46,5 @@ tests/test_olap_table_versioning.py
|
|
|
44
46
|
tests/test_query_builder.py
|
|
45
47
|
tests/test_redis_client.py
|
|
46
48
|
tests/test_s3queue_config.py
|
|
47
|
-
tests/test_simple_aggregate.py
|
|
49
|
+
tests/test_simple_aggregate.py
|
|
50
|
+
tests/test_web_app.py
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for WebApp SDK functionality.
|
|
3
|
+
"""
|
|
4
|
+
import pytest
|
|
5
|
+
from moose_lib.dmv2 import WebApp, WebAppConfig, WebAppMetadata
|
|
6
|
+
from moose_lib.dmv2._registry import _web_apps
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Mock FastAPI app for testing
|
|
10
|
+
class MockFastAPIApp:
|
|
11
|
+
"""Mock FastAPI application for testing."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(autouse=True)
|
|
16
|
+
def clear_registry():
|
|
17
|
+
"""Clear the WebApp registry before each test."""
|
|
18
|
+
_web_apps.clear()
|
|
19
|
+
yield
|
|
20
|
+
_web_apps.clear()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_webapp_basic_creation():
|
|
24
|
+
"""Test basic WebApp creation with required mount_path."""
|
|
25
|
+
app = MockFastAPIApp()
|
|
26
|
+
config = WebAppConfig(mount_path="/test")
|
|
27
|
+
webapp = WebApp("test_app", app, config)
|
|
28
|
+
|
|
29
|
+
assert webapp.name == "test_app"
|
|
30
|
+
assert webapp.app is app
|
|
31
|
+
assert webapp.config.mount_path == "/test"
|
|
32
|
+
assert webapp.config.inject_moose_utils is True
|
|
33
|
+
assert "test_app" in _web_apps
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_webapp_with_custom_mount_path():
|
|
37
|
+
"""Test WebApp with custom mount path."""
|
|
38
|
+
app = MockFastAPIApp()
|
|
39
|
+
config = WebAppConfig(mount_path="/myapi")
|
|
40
|
+
webapp = WebApp("test_app", app, config)
|
|
41
|
+
|
|
42
|
+
assert webapp.config.mount_path == "/myapi"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_webapp_with_metadata():
|
|
46
|
+
"""Test WebApp with metadata."""
|
|
47
|
+
app = MockFastAPIApp()
|
|
48
|
+
config = WebAppConfig(
|
|
49
|
+
mount_path="/api",
|
|
50
|
+
metadata=WebAppMetadata(description="My API"),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
with pytest.raises(ValueError, match="cannot begin with a reserved path"):
|
|
54
|
+
WebApp("test_app", app, config)
|
|
55
|
+
|
|
56
|
+
# Now test with valid mount path
|
|
57
|
+
config.mount_path = "/myapi"
|
|
58
|
+
webapp = WebApp("test_app", app, config)
|
|
59
|
+
assert webapp.config.metadata.description == "My API"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_webapp_duplicate_name():
|
|
63
|
+
"""Test that duplicate WebApp names are rejected."""
|
|
64
|
+
app1 = MockFastAPIApp()
|
|
65
|
+
app2 = MockFastAPIApp()
|
|
66
|
+
|
|
67
|
+
WebApp("test_app", app1, WebAppConfig(mount_path="/test1"))
|
|
68
|
+
|
|
69
|
+
with pytest.raises(ValueError, match="WebApp with name 'test_app' already exists"):
|
|
70
|
+
WebApp("test_app", app2, WebAppConfig(mount_path="/test2"))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_webapp_trailing_slash_validation():
|
|
74
|
+
"""Test that trailing slashes are rejected."""
|
|
75
|
+
app = MockFastAPIApp()
|
|
76
|
+
config = WebAppConfig(mount_path="/myapi/")
|
|
77
|
+
|
|
78
|
+
with pytest.raises(ValueError, match="mountPath cannot end with a trailing slash"):
|
|
79
|
+
WebApp("test_app", app, config)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_webapp_root_path_rejected():
|
|
83
|
+
"""Test that root path '/' is rejected to prevent overlap with reserved paths."""
|
|
84
|
+
app = MockFastAPIApp()
|
|
85
|
+
config = WebAppConfig(mount_path="/")
|
|
86
|
+
|
|
87
|
+
with pytest.raises(ValueError, match='mountPath cannot be "/" as it would allow routes to overlap with reserved paths'):
|
|
88
|
+
WebApp("test_app", app, config)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_webapp_reserved_paths():
|
|
92
|
+
"""Test that reserved paths are rejected."""
|
|
93
|
+
reserved_paths = [
|
|
94
|
+
"/admin",
|
|
95
|
+
"/api",
|
|
96
|
+
"/consumption",
|
|
97
|
+
"/health",
|
|
98
|
+
"/ingest",
|
|
99
|
+
"/moose",
|
|
100
|
+
"/ready",
|
|
101
|
+
"/workflows",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
for path in reserved_paths:
|
|
105
|
+
app = MockFastAPIApp()
|
|
106
|
+
config = WebAppConfig(mount_path=path)
|
|
107
|
+
|
|
108
|
+
with pytest.raises(ValueError, match="cannot begin with a reserved path"):
|
|
109
|
+
WebApp(f"test_{path}", app, config)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_webapp_reserved_path_prefix():
|
|
113
|
+
"""Test that paths starting with reserved prefixes are rejected."""
|
|
114
|
+
app = MockFastAPIApp()
|
|
115
|
+
config = WebAppConfig(mount_path="/api/v1")
|
|
116
|
+
|
|
117
|
+
with pytest.raises(ValueError, match="cannot begin with a reserved path"):
|
|
118
|
+
WebApp("test_app", app, config)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_webapp_duplicate_mount_path():
|
|
122
|
+
"""Test that duplicate mount paths are rejected."""
|
|
123
|
+
app1 = MockFastAPIApp()
|
|
124
|
+
app2 = MockFastAPIApp()
|
|
125
|
+
|
|
126
|
+
config1 = WebAppConfig(mount_path="/myapi")
|
|
127
|
+
WebApp("app1", app1, config1)
|
|
128
|
+
|
|
129
|
+
config2 = WebAppConfig(mount_path="/myapi")
|
|
130
|
+
with pytest.raises(ValueError, match='WebApp with mountPath "/myapi" already exists'):
|
|
131
|
+
WebApp("app2", app2, config2)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_webapp_different_mount_paths():
|
|
135
|
+
"""Test that WebApps with different mount paths can coexist."""
|
|
136
|
+
app1 = MockFastAPIApp()
|
|
137
|
+
app2 = MockFastAPIApp()
|
|
138
|
+
|
|
139
|
+
WebApp("app1", app1, WebAppConfig(mount_path="/api1"))
|
|
140
|
+
WebApp("app2", app2, WebAppConfig(mount_path="/api2"))
|
|
141
|
+
|
|
142
|
+
assert len(_web_apps) == 2
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_webapp_inject_moose_utils_false():
|
|
146
|
+
"""Test WebApp with inject_moose_utils disabled."""
|
|
147
|
+
app = MockFastAPIApp()
|
|
148
|
+
config = WebAppConfig(mount_path="/test", inject_moose_utils=False)
|
|
149
|
+
webapp = WebApp("test_app", app, config)
|
|
150
|
+
|
|
151
|
+
assert webapp.config.inject_moose_utils is False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_webapp_repr():
|
|
155
|
+
"""Test WebApp string representation."""
|
|
156
|
+
app = MockFastAPIApp()
|
|
157
|
+
webapp = WebApp("test_app", app, WebAppConfig(mount_path="/myapi"))
|
|
158
|
+
|
|
159
|
+
assert "test_app" in repr(webapp)
|
|
160
|
+
assert "/myapi" in repr(webapp)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_webapp_mount_path_required():
|
|
164
|
+
"""Test that mount_path is required."""
|
|
165
|
+
app = MockFastAPIApp()
|
|
166
|
+
|
|
167
|
+
with pytest.raises(ValueError, match="mountPath is required"):
|
|
168
|
+
WebApp("test_app", app, WebAppConfig(mount_path=""))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_webapp_serialization():
|
|
172
|
+
"""Test that WebApps can be serialized via internal.py."""
|
|
173
|
+
from moose_lib.internal import to_infra_map
|
|
174
|
+
from moose_lib.dmv2 import get_web_apps
|
|
175
|
+
|
|
176
|
+
app = MockFastAPIApp()
|
|
177
|
+
WebApp(
|
|
178
|
+
"test_app",
|
|
179
|
+
app,
|
|
180
|
+
WebAppConfig(
|
|
181
|
+
mount_path="/myapi",
|
|
182
|
+
metadata=WebAppMetadata(description="Test API")
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Verify it's in the registry
|
|
187
|
+
web_apps = get_web_apps()
|
|
188
|
+
assert "test_app" in web_apps
|
|
189
|
+
|
|
190
|
+
# Serialize to infra map
|
|
191
|
+
infra_map = to_infra_map()
|
|
192
|
+
|
|
193
|
+
assert "webApps" in infra_map
|
|
194
|
+
assert "test_app" in infra_map["webApps"]
|
|
195
|
+
assert infra_map["webApps"]["test_app"]["name"] == "test_app"
|
|
196
|
+
assert infra_map["webApps"]["test_app"]["mountPath"] == "/myapi"
|
|
197
|
+
assert infra_map["webApps"]["test_app"]["metadata"]["description"] == "Test API"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_webapp_serialization_with_mount_path():
|
|
201
|
+
"""Test WebApp serialization with explicit mount path."""
|
|
202
|
+
from moose_lib.internal import to_infra_map
|
|
203
|
+
|
|
204
|
+
app = MockFastAPIApp()
|
|
205
|
+
WebApp("test_app", app, WebAppConfig(mount_path="/testpath"))
|
|
206
|
+
|
|
207
|
+
infra_map = to_infra_map()
|
|
208
|
+
|
|
209
|
+
assert infra_map["webApps"]["test_app"]["mountPath"] == "/testpath"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_webapp_serialization_no_metadata():
|
|
213
|
+
"""Test WebApp serialization without metadata."""
|
|
214
|
+
from moose_lib.internal import to_infra_map
|
|
215
|
+
|
|
216
|
+
app = MockFastAPIApp()
|
|
217
|
+
WebApp("test_app", app, WebAppConfig(mount_path="/myapi"))
|
|
218
|
+
|
|
219
|
+
infra_map = to_infra_map()
|
|
220
|
+
|
|
221
|
+
assert infra_map["webApps"]["test_app"]["metadata"] is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|