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.
Files changed (52) hide show
  1. {moose_lib-0.6.136 → moose_lib-0.6.138}/PKG-INFO +1 -1
  2. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/__init__.py +24 -0
  3. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/_registry.py +2 -1
  4. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/registry.py +10 -0
  5. moose_lib-0.6.138/moose_lib/dmv2/web_app.py +170 -0
  6. moose_lib-0.6.138/moose_lib/dmv2/web_app_helpers.py +92 -0
  7. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/internal.py +45 -1
  8. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib.egg-info/PKG-INFO +1 -1
  9. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib.egg-info/SOURCES.txt +4 -1
  10. moose_lib-0.6.138/tests/test_web_app.py +221 -0
  11. {moose_lib-0.6.136 → moose_lib-0.6.138}/README.md +0 -0
  12. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/__init__.py +0 -0
  13. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/blocks.py +0 -0
  14. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/clients/__init__.py +0 -0
  15. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/clients/redis_client.py +0 -0
  16. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/commons.py +0 -0
  17. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/config/__init__.py +0 -0
  18. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/config/config_file.py +0 -0
  19. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/config/runtime.py +0 -0
  20. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/data_models.py +0 -0
  21. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/consumption.py +0 -0
  22. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/ingest_api.py +0 -0
  23. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/ingest_pipeline.py +0 -0
  24. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/life_cycle.py +0 -0
  25. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/materialized_view.py +0 -0
  26. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/olap_table.py +0 -0
  27. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/sql_resource.py +0 -0
  28. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/stream.py +0 -0
  29. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/types.py +0 -0
  30. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/view.py +0 -0
  31. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2/workflow.py +0 -0
  32. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/dmv2_serializer.py +0 -0
  33. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/main.py +0 -0
  34. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/query_builder.py +0 -0
  35. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/query_param.py +0 -0
  36. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/streaming/__init__.py +0 -0
  37. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/streaming/streaming_function_runner.py +0 -0
  38. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/utilities/__init__.py +0 -0
  39. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib/utilities/sql.py +0 -0
  40. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib.egg-info/dependency_links.txt +0 -0
  41. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib.egg-info/requires.txt +0 -0
  42. {moose_lib-0.6.136 → moose_lib-0.6.138}/moose_lib.egg-info/top_level.txt +0 -0
  43. {moose_lib-0.6.136 → moose_lib-0.6.138}/setup.cfg +0 -0
  44. {moose_lib-0.6.136 → moose_lib-0.6.138}/setup.py +0 -0
  45. {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/__init__.py +0 -0
  46. {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/conftest.py +0 -0
  47. {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/test_moose.py +0 -0
  48. {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/test_olap_table_versioning.py +0 -0
  49. {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/test_query_builder.py +0 -0
  50. {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/test_redis_client.py +0 -0
  51. {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/test_s3queue_config.py +0 -0
  52. {moose_lib-0.6.136 → moose_lib-0.6.138}/tests/test_simple_aggregate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moose_lib
3
- Version: 0.6.136
3
+ Version: 0.6.138
4
4
  Home-page: https://www.fiveonefour.com/moose
5
5
  Author: Fiveonefour Labs Inc.
6
6
  Author-email: support@fiveonefour.com
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moose_lib
3
- Version: 0.6.136
3
+ Version: 0.6.138
4
4
  Home-page: https://www.fiveonefour.com/moose
5
5
  Author: Fiveonefour Labs Inc.
6
6
  Author-email: support@fiveonefour.com
@@ -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