mdb-engine 0.1.6__py3-none-any.whl → 0.4.12__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.
- mdb_engine/__init__.py +116 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +654 -11
- mdb_engine/auth/__init__.py +136 -29
- mdb_engine/auth/audit.py +592 -0
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +265 -70
- mdb_engine/auth/config_defaults.py +5 -5
- mdb_engine/auth/config_helpers.py +19 -18
- mdb_engine/auth/cookie_utils.py +12 -16
- mdb_engine/auth/csrf.py +483 -0
- mdb_engine/auth/decorators.py +10 -16
- mdb_engine/auth/dependencies.py +69 -71
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +61 -88
- mdb_engine/auth/jwt.py +11 -15
- mdb_engine/auth/middleware.py +79 -35
- mdb_engine/auth/oso_factory.py +21 -41
- mdb_engine/auth/provider.py +270 -171
- mdb_engine/auth/rate_limiter.py +505 -0
- mdb_engine/auth/restrictions.py +21 -36
- mdb_engine/auth/session_manager.py +24 -41
- mdb_engine/auth/shared_middleware.py +977 -0
- mdb_engine/auth/shared_users.py +775 -0
- mdb_engine/auth/token_lifecycle.py +10 -12
- mdb_engine/auth/token_store.py +17 -32
- mdb_engine/auth/users.py +99 -159
- mdb_engine/auth/utils.py +236 -42
- mdb_engine/cli/commands/generate.py +546 -10
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +7 -7
- mdb_engine/config.py +13 -28
- mdb_engine/constants.py +65 -0
- mdb_engine/core/README.md +117 -6
- mdb_engine/core/__init__.py +39 -7
- mdb_engine/core/app_registration.py +31 -50
- mdb_engine/core/app_secrets.py +289 -0
- mdb_engine/core/connection.py +20 -12
- mdb_engine/core/encryption.py +222 -0
- mdb_engine/core/engine.py +2862 -115
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +628 -204
- mdb_engine/core/ray_integration.py +436 -0
- mdb_engine/core/seeding.py +13 -21
- mdb_engine/core/service_initialization.py +20 -30
- mdb_engine/core/types.py +40 -43
- mdb_engine/database/README.md +140 -17
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +37 -50
- mdb_engine/database/connection.py +51 -30
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +747 -237
- mdb_engine/dependencies.py +427 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +247 -0
- mdb_engine/di/providers.py +206 -0
- mdb_engine/di/scopes.py +139 -0
- mdb_engine/embeddings/README.md +54 -24
- mdb_engine/embeddings/__init__.py +31 -24
- mdb_engine/embeddings/dependencies.py +38 -155
- mdb_engine/embeddings/service.py +78 -75
- mdb_engine/exceptions.py +104 -12
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +11 -11
- mdb_engine/indexes/manager.py +59 -123
- mdb_engine/memory/README.md +95 -4
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +363 -1168
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +17 -17
- mdb_engine/observability/logging.py +10 -10
- mdb_engine/observability/metrics.py +40 -19
- mdb_engine/repositories/__init__.py +34 -0
- mdb_engine/repositories/base.py +325 -0
- mdb_engine/repositories/mongo.py +233 -0
- mdb_engine/repositories/unit_of_work.py +166 -0
- mdb_engine/routing/README.md +1 -1
- mdb_engine/routing/__init__.py +1 -3
- mdb_engine/routing/websockets.py +41 -75
- mdb_engine/utils/__init__.py +3 -1
- mdb_engine/utils/mongo.py +117 -0
- mdb_engine-0.4.12.dist-info/METADATA +492 -0
- mdb_engine-0.4.12.dist-info/RECORD +97 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/WHEEL +1 -1
- mdb_engine-0.1.6.dist-info/METADATA +0 -213
- mdb_engine-0.1.6.dist-info/RECORD +0 -75
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/top_level.txt +0 -0
mdb_engine/core/manifest.py
CHANGED
|
@@ -32,21 +32,27 @@ For Scale:
|
|
|
32
32
|
import asyncio
|
|
33
33
|
import hashlib
|
|
34
34
|
import logging
|
|
35
|
-
from
|
|
35
|
+
from collections.abc import Awaitable, Callable
|
|
36
|
+
from typing import Any
|
|
36
37
|
|
|
37
38
|
from jsonschema import SchemaError, ValidationError, validate
|
|
38
39
|
|
|
39
|
-
from ..constants import (
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
from ..constants import (
|
|
41
|
+
CURRENT_SCHEMA_VERSION,
|
|
42
|
+
DEFAULT_SCHEMA_VERSION,
|
|
43
|
+
MAX_TTL_SECONDS,
|
|
44
|
+
MAX_VECTOR_DIMENSIONS,
|
|
45
|
+
MIN_TTL_SECONDS,
|
|
46
|
+
MIN_VECTOR_DIMENSIONS,
|
|
47
|
+
)
|
|
42
48
|
|
|
43
49
|
logger = logging.getLogger(__name__)
|
|
44
50
|
|
|
45
51
|
# Schema registry: maps version -> schema definition
|
|
46
|
-
SCHEMA_REGISTRY:
|
|
52
|
+
SCHEMA_REGISTRY: dict[str, dict[str, Any]] = {}
|
|
47
53
|
|
|
48
54
|
# Validation cache: maps (manifest_hash, version) -> validation_result
|
|
49
|
-
_validation_cache:
|
|
55
|
+
_validation_cache: dict[str, tuple[bool, str | None, list[str] | None]] = {}
|
|
50
56
|
_cache_lock = asyncio.Lock()
|
|
51
57
|
|
|
52
58
|
|
|
@@ -81,15 +87,13 @@ def _convert_tuples_to_lists(obj: Any) -> Any:
|
|
|
81
87
|
return obj
|
|
82
88
|
|
|
83
89
|
|
|
84
|
-
def _get_manifest_hash(manifest_data:
|
|
90
|
+
def _get_manifest_hash(manifest_data: dict[str, Any]) -> str:
|
|
85
91
|
"""Generate a hash for manifest caching."""
|
|
86
92
|
import json
|
|
87
93
|
|
|
88
94
|
# Normalize manifest by removing metadata fields that don't affect validation
|
|
89
95
|
normalized = {
|
|
90
|
-
k: v
|
|
91
|
-
for k, v in manifest_data.items()
|
|
92
|
-
if k not in ["_id", "_updated", "_created", "url"]
|
|
96
|
+
k: v for k, v in manifest_data.items() if k not in ["_id", "_updated", "_created", "url"]
|
|
93
97
|
}
|
|
94
98
|
normalized_str = json.dumps(normalized, sort_keys=True)
|
|
95
99
|
return hashlib.sha256(normalized_str.encode()).hexdigest()[:16]
|
|
@@ -137,6 +141,70 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
137
141
|
"auth": {
|
|
138
142
|
"type": "object",
|
|
139
143
|
"properties": {
|
|
144
|
+
"mode": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"enum": ["app", "shared"],
|
|
147
|
+
"default": "app",
|
|
148
|
+
"description": (
|
|
149
|
+
"Authentication mode: 'app' for per-app tokens "
|
|
150
|
+
"(isolated auth per app, default), 'shared' for "
|
|
151
|
+
"shared user pool with SSO across all apps. "
|
|
152
|
+
"Modes are mutually exclusive."
|
|
153
|
+
),
|
|
154
|
+
},
|
|
155
|
+
"roles": {
|
|
156
|
+
"type": "array",
|
|
157
|
+
"items": {"type": "string"},
|
|
158
|
+
"description": (
|
|
159
|
+
"Available roles for this app (shared mode only). "
|
|
160
|
+
"Example: ['viewer', 'editor', 'admin']"
|
|
161
|
+
),
|
|
162
|
+
},
|
|
163
|
+
"default_role": {
|
|
164
|
+
"type": "string",
|
|
165
|
+
"description": (
|
|
166
|
+
"Default role assigned to new users for this app "
|
|
167
|
+
"(shared mode only). Must be one of the defined roles."
|
|
168
|
+
),
|
|
169
|
+
},
|
|
170
|
+
"require_role": {
|
|
171
|
+
"type": "string",
|
|
172
|
+
"description": (
|
|
173
|
+
"Minimum role required to access this app "
|
|
174
|
+
"(shared mode only). Users without this role "
|
|
175
|
+
"will be denied access."
|
|
176
|
+
),
|
|
177
|
+
},
|
|
178
|
+
"public_routes": {
|
|
179
|
+
"type": "array",
|
|
180
|
+
"items": {"type": "string"},
|
|
181
|
+
"description": (
|
|
182
|
+
"Routes that don't require authentication. "
|
|
183
|
+
"Supports wildcards, e.g., ['/health', '/api/public/*']. "
|
|
184
|
+
"Works in both auth modes."
|
|
185
|
+
),
|
|
186
|
+
},
|
|
187
|
+
"auth_hub_url": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"format": "uri",
|
|
190
|
+
"description": (
|
|
191
|
+
"URL of the authentication hub for SSO apps (shared mode only). "
|
|
192
|
+
"Used for redirecting unauthenticated users to login. "
|
|
193
|
+
"Example: 'http://localhost:8000' or 'https://auth.example.com'. "
|
|
194
|
+
"Can be overridden via AUTH_HUB_URL environment variable."
|
|
195
|
+
),
|
|
196
|
+
},
|
|
197
|
+
"related_apps": {
|
|
198
|
+
"type": "object",
|
|
199
|
+
"additionalProperties": {"type": "string", "format": "uri"},
|
|
200
|
+
"description": (
|
|
201
|
+
"Map of related app slugs to their URLs for cross-app navigation "
|
|
202
|
+
"(useful in shared auth mode). Keys are app slugs, values are URLs. "
|
|
203
|
+
"Example: {'dashboard': 'http://localhost:8001', "
|
|
204
|
+
"'click_tracker': 'http://localhost:8000'}. "
|
|
205
|
+
"Can be overridden via {APP_SLUG_UPPER}_URL environment variables."
|
|
206
|
+
),
|
|
207
|
+
},
|
|
140
208
|
"policy": {
|
|
141
209
|
"type": "object",
|
|
142
210
|
"properties": {
|
|
@@ -254,24 +322,44 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
254
322
|
"initial_policies": {
|
|
255
323
|
"type": "array",
|
|
256
324
|
"items": {
|
|
257
|
-
"
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"
|
|
262
|
-
"
|
|
325
|
+
"oneOf": [
|
|
326
|
+
{
|
|
327
|
+
"type": "array",
|
|
328
|
+
"items": {"type": "string"},
|
|
329
|
+
"minItems": 3,
|
|
330
|
+
"maxItems": 3,
|
|
331
|
+
"description": (
|
|
332
|
+
"Casbin policy as array: "
|
|
333
|
+
'["role", "resource", "action"]'
|
|
334
|
+
),
|
|
263
335
|
},
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
336
|
+
{
|
|
337
|
+
"type": "object",
|
|
338
|
+
"properties": {
|
|
339
|
+
"role": {"type": "string"},
|
|
340
|
+
"resource": {
|
|
341
|
+
"type": "string",
|
|
342
|
+
"default": "documents",
|
|
343
|
+
},
|
|
344
|
+
"action": {"type": "string"},
|
|
345
|
+
},
|
|
346
|
+
"required": ["role", "action"],
|
|
347
|
+
"additionalProperties": False,
|
|
348
|
+
"description": (
|
|
349
|
+
"OSO policy as object: "
|
|
350
|
+
'{"role": "admin", "resource": "documents", '
|
|
351
|
+
'"action": "read"}'
|
|
352
|
+
),
|
|
353
|
+
},
|
|
354
|
+
],
|
|
268
355
|
},
|
|
269
356
|
"description": (
|
|
270
|
-
"Initial permission policies to set up "
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
|
|
274
|
-
'"
|
|
357
|
+
"Initial permission policies to set up on startup. "
|
|
358
|
+
"For Casbin provider: use arrays like "
|
|
359
|
+
'["admin", "clicks", "read"]. '
|
|
360
|
+
"For OSO Cloud provider: use objects like "
|
|
361
|
+
'{"role": "admin", "resource": "documents", '
|
|
362
|
+
'"action": "read"}.'
|
|
275
363
|
),
|
|
276
364
|
},
|
|
277
365
|
},
|
|
@@ -484,8 +572,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
484
572
|
"type": "string",
|
|
485
573
|
"default": "user",
|
|
486
574
|
"description": (
|
|
487
|
-
"Role for demo user in app "
|
|
488
|
-
"(default: 'user')"
|
|
575
|
+
"Role for demo user in app " "(default: 'user')"
|
|
489
576
|
),
|
|
490
577
|
},
|
|
491
578
|
"auto_create": {
|
|
@@ -567,6 +654,292 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
567
654
|
"independent of platform authentication."
|
|
568
655
|
),
|
|
569
656
|
},
|
|
657
|
+
"rate_limits": {
|
|
658
|
+
"type": "object",
|
|
659
|
+
"additionalProperties": {
|
|
660
|
+
"type": "object",
|
|
661
|
+
"properties": {
|
|
662
|
+
"max_attempts": {
|
|
663
|
+
"type": "integer",
|
|
664
|
+
"minimum": 1,
|
|
665
|
+
"default": 5,
|
|
666
|
+
"description": (
|
|
667
|
+
"Maximum attempts allowed in the time window " "(default: 5)."
|
|
668
|
+
),
|
|
669
|
+
},
|
|
670
|
+
"window_seconds": {
|
|
671
|
+
"type": "integer",
|
|
672
|
+
"minimum": 1,
|
|
673
|
+
"default": 300,
|
|
674
|
+
"description": (
|
|
675
|
+
"Time window in seconds for rate limiting "
|
|
676
|
+
"(default: 300 = 5 minutes)."
|
|
677
|
+
),
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
"additionalProperties": False,
|
|
681
|
+
},
|
|
682
|
+
"description": (
|
|
683
|
+
"Rate limiting configuration for auth endpoints. "
|
|
684
|
+
"Keys are endpoint paths (e.g., '/login', '/register'). "
|
|
685
|
+
"Example: {'/login': {'max_attempts': 5, 'window_seconds': 300}}"
|
|
686
|
+
),
|
|
687
|
+
},
|
|
688
|
+
"audit": {
|
|
689
|
+
"type": "object",
|
|
690
|
+
"properties": {
|
|
691
|
+
"enabled": {
|
|
692
|
+
"type": "boolean",
|
|
693
|
+
"default": True,
|
|
694
|
+
"description": (
|
|
695
|
+
"Enable audit logging for authentication events "
|
|
696
|
+
"(default: true for shared auth mode)."
|
|
697
|
+
),
|
|
698
|
+
},
|
|
699
|
+
"retention_days": {
|
|
700
|
+
"type": "integer",
|
|
701
|
+
"minimum": 1,
|
|
702
|
+
"default": 90,
|
|
703
|
+
"description": (
|
|
704
|
+
"Number of days to retain audit logs " "(default: 90 days)."
|
|
705
|
+
),
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
"additionalProperties": False,
|
|
709
|
+
"description": (
|
|
710
|
+
"Audit logging configuration for authentication events. "
|
|
711
|
+
"Logs are stored in MongoDB with automatic TTL cleanup."
|
|
712
|
+
),
|
|
713
|
+
},
|
|
714
|
+
"csrf_protection": {
|
|
715
|
+
"oneOf": [
|
|
716
|
+
{"type": "boolean"},
|
|
717
|
+
{
|
|
718
|
+
"type": "object",
|
|
719
|
+
"properties": {
|
|
720
|
+
"enabled": {
|
|
721
|
+
"type": "boolean",
|
|
722
|
+
"default": True,
|
|
723
|
+
"description": (
|
|
724
|
+
"Enable CSRF protection "
|
|
725
|
+
"(default: true for shared auth mode)."
|
|
726
|
+
),
|
|
727
|
+
},
|
|
728
|
+
"exempt_routes": {
|
|
729
|
+
"type": "array",
|
|
730
|
+
"items": {"type": "string"},
|
|
731
|
+
"description": (
|
|
732
|
+
"Routes exempt from CSRF validation. "
|
|
733
|
+
"Supports wildcards (e.g., '/api/*'). "
|
|
734
|
+
"Defaults to public_routes if not specified."
|
|
735
|
+
),
|
|
736
|
+
},
|
|
737
|
+
"rotate_tokens": {
|
|
738
|
+
"type": "boolean",
|
|
739
|
+
"default": False,
|
|
740
|
+
"description": (
|
|
741
|
+
"Rotate CSRF token on each request "
|
|
742
|
+
"(more secure, less convenient). Default: false."
|
|
743
|
+
),
|
|
744
|
+
},
|
|
745
|
+
"token_ttl": {
|
|
746
|
+
"type": "integer",
|
|
747
|
+
"minimum": 60,
|
|
748
|
+
"default": 3600,
|
|
749
|
+
"description": (
|
|
750
|
+
"CSRF token TTL in seconds " "(default: 3600 = 1 hour)."
|
|
751
|
+
),
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
"additionalProperties": False,
|
|
755
|
+
},
|
|
756
|
+
],
|
|
757
|
+
"default": True,
|
|
758
|
+
"description": (
|
|
759
|
+
"CSRF protection configuration. Auto-enabled for shared "
|
|
760
|
+
"auth mode. Set to false to disable, or provide object "
|
|
761
|
+
"for detailed configuration. Uses double-submit cookie "
|
|
762
|
+
"pattern with SameSite=Lax cookies."
|
|
763
|
+
),
|
|
764
|
+
},
|
|
765
|
+
"security": {
|
|
766
|
+
"type": "object",
|
|
767
|
+
"properties": {
|
|
768
|
+
"hsts": {
|
|
769
|
+
"type": "object",
|
|
770
|
+
"properties": {
|
|
771
|
+
"enabled": {
|
|
772
|
+
"type": "boolean",
|
|
773
|
+
"default": True,
|
|
774
|
+
"description": (
|
|
775
|
+
"Enable HSTS header in production " "(default: true)."
|
|
776
|
+
),
|
|
777
|
+
},
|
|
778
|
+
"max_age": {
|
|
779
|
+
"type": "integer",
|
|
780
|
+
"minimum": 0,
|
|
781
|
+
"default": 31536000,
|
|
782
|
+
"description": (
|
|
783
|
+
"HSTS max-age in seconds " "(default: 31536000 = 1 year)."
|
|
784
|
+
),
|
|
785
|
+
},
|
|
786
|
+
"include_subdomains": {
|
|
787
|
+
"type": "boolean",
|
|
788
|
+
"default": True,
|
|
789
|
+
"description": (
|
|
790
|
+
"Include subdomains in HSTS policy " "(default: true)."
|
|
791
|
+
),
|
|
792
|
+
},
|
|
793
|
+
"preload": {
|
|
794
|
+
"type": "boolean",
|
|
795
|
+
"default": False,
|
|
796
|
+
"description": (
|
|
797
|
+
"Add preload directive for HSTS preload "
|
|
798
|
+
"list submission (default: false). Only "
|
|
799
|
+
"enable if you're ready for permanent HTTPS."
|
|
800
|
+
),
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
"additionalProperties": False,
|
|
804
|
+
"description": (
|
|
805
|
+
"HTTP Strict Transport Security configuration. "
|
|
806
|
+
"Forces HTTPS connections in production."
|
|
807
|
+
),
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
"additionalProperties": False,
|
|
811
|
+
"description": (
|
|
812
|
+
"Security settings including HSTS, headers, and other " "security controls."
|
|
813
|
+
),
|
|
814
|
+
},
|
|
815
|
+
"jwt": {
|
|
816
|
+
"type": "object",
|
|
817
|
+
"properties": {
|
|
818
|
+
"algorithm": {
|
|
819
|
+
"type": "string",
|
|
820
|
+
"enum": ["HS256", "RS256", "ES256"],
|
|
821
|
+
"default": "HS256",
|
|
822
|
+
"description": (
|
|
823
|
+
"JWT signing algorithm. HS256 (HMAC, default) "
|
|
824
|
+
"uses symmetric secret. RS256 (RSA) and ES256 "
|
|
825
|
+
"(ECDSA) use asymmetric key pairs for better "
|
|
826
|
+
"security in distributed systems."
|
|
827
|
+
),
|
|
828
|
+
},
|
|
829
|
+
"token_expiry_hours": {
|
|
830
|
+
"type": "integer",
|
|
831
|
+
"minimum": 1,
|
|
832
|
+
"default": 24,
|
|
833
|
+
"description": ("JWT token expiry in hours (default: 24)."),
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
"additionalProperties": False,
|
|
837
|
+
"description": (
|
|
838
|
+
"JWT configuration for shared auth mode. "
|
|
839
|
+
"Controls algorithm and token lifetime."
|
|
840
|
+
),
|
|
841
|
+
},
|
|
842
|
+
"password_policy": {
|
|
843
|
+
"type": "object",
|
|
844
|
+
"properties": {
|
|
845
|
+
"min_length": {
|
|
846
|
+
"type": "integer",
|
|
847
|
+
"minimum": 6,
|
|
848
|
+
"default": 12,
|
|
849
|
+
"description": ("Minimum password length (default: 12)."),
|
|
850
|
+
},
|
|
851
|
+
"min_entropy_bits": {
|
|
852
|
+
"type": "integer",
|
|
853
|
+
"minimum": 0,
|
|
854
|
+
"default": 50,
|
|
855
|
+
"description": (
|
|
856
|
+
"Minimum password entropy in bits "
|
|
857
|
+
"(default: 50). Set to 0 to disable."
|
|
858
|
+
),
|
|
859
|
+
},
|
|
860
|
+
"require_uppercase": {
|
|
861
|
+
"type": "boolean",
|
|
862
|
+
"default": True,
|
|
863
|
+
"description": (
|
|
864
|
+
"Require at least one uppercase letter " "(default: true)."
|
|
865
|
+
),
|
|
866
|
+
},
|
|
867
|
+
"require_lowercase": {
|
|
868
|
+
"type": "boolean",
|
|
869
|
+
"default": True,
|
|
870
|
+
"description": (
|
|
871
|
+
"Require at least one lowercase letter " "(default: true)."
|
|
872
|
+
),
|
|
873
|
+
},
|
|
874
|
+
"require_numbers": {
|
|
875
|
+
"type": "boolean",
|
|
876
|
+
"default": True,
|
|
877
|
+
"description": ("Require at least one number " "(default: true)."),
|
|
878
|
+
},
|
|
879
|
+
"require_special": {
|
|
880
|
+
"type": "boolean",
|
|
881
|
+
"default": False,
|
|
882
|
+
"description": (
|
|
883
|
+
"Require at least one special character " "(default: false)."
|
|
884
|
+
),
|
|
885
|
+
},
|
|
886
|
+
"check_common_passwords": {
|
|
887
|
+
"type": "boolean",
|
|
888
|
+
"default": True,
|
|
889
|
+
"description": (
|
|
890
|
+
"Check against common password list " "(default: true)."
|
|
891
|
+
),
|
|
892
|
+
},
|
|
893
|
+
"check_breaches": {
|
|
894
|
+
"type": "boolean",
|
|
895
|
+
"default": False,
|
|
896
|
+
"description": (
|
|
897
|
+
"Check against HaveIBeenPwned breach database "
|
|
898
|
+
"(default: false). Requires network access."
|
|
899
|
+
),
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
"additionalProperties": False,
|
|
903
|
+
"description": (
|
|
904
|
+
"Password policy configuration for shared auth mode. "
|
|
905
|
+
"Enforces password strength requirements."
|
|
906
|
+
),
|
|
907
|
+
},
|
|
908
|
+
"session_binding": {
|
|
909
|
+
"type": "object",
|
|
910
|
+
"properties": {
|
|
911
|
+
"bind_ip": {
|
|
912
|
+
"type": "boolean",
|
|
913
|
+
"default": False,
|
|
914
|
+
"description": (
|
|
915
|
+
"Bind sessions to IP address. Strict mode: "
|
|
916
|
+
"reject if IP changes (default: false)."
|
|
917
|
+
),
|
|
918
|
+
},
|
|
919
|
+
"bind_fingerprint": {
|
|
920
|
+
"type": "boolean",
|
|
921
|
+
"default": True,
|
|
922
|
+
"description": (
|
|
923
|
+
"Bind sessions to device fingerprint. Soft mode: "
|
|
924
|
+
"log warning if fingerprint changes (default: true)."
|
|
925
|
+
),
|
|
926
|
+
},
|
|
927
|
+
"allow_ip_change_with_reauth": {
|
|
928
|
+
"type": "boolean",
|
|
929
|
+
"default": True,
|
|
930
|
+
"description": (
|
|
931
|
+
"Allow IP change if user re-authenticates "
|
|
932
|
+
"(default: true). Only applies when bind_ip=true."
|
|
933
|
+
),
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
"additionalProperties": False,
|
|
937
|
+
"description": (
|
|
938
|
+
"Session binding configuration. Ties sessions to client "
|
|
939
|
+
"characteristics for additional security against session "
|
|
940
|
+
"hijacking."
|
|
941
|
+
),
|
|
942
|
+
},
|
|
570
943
|
},
|
|
571
944
|
"additionalProperties": False,
|
|
572
945
|
"description": (
|
|
@@ -590,17 +963,13 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
590
963
|
"type": "integer",
|
|
591
964
|
"minimum": 60,
|
|
592
965
|
"default": 900,
|
|
593
|
-
"description": (
|
|
594
|
-
"Access token TTL in seconds " "(default: 900 = 15 minutes)."
|
|
595
|
-
),
|
|
966
|
+
"description": ("Access token TTL in seconds " "(default: 900 = 15 minutes)."),
|
|
596
967
|
},
|
|
597
968
|
"refresh_token_ttl": {
|
|
598
969
|
"type": "integer",
|
|
599
970
|
"minimum": 3600,
|
|
600
971
|
"default": 604800,
|
|
601
|
-
"description": (
|
|
602
|
-
"Refresh token TTL in seconds " "(default: 604800 = 7 days)."
|
|
603
|
-
),
|
|
972
|
+
"description": ("Refresh token TTL in seconds " "(default: 604800 = 7 days)."),
|
|
604
973
|
},
|
|
605
974
|
"token_rotation": {
|
|
606
975
|
"type": "boolean",
|
|
@@ -615,8 +984,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
615
984
|
"minimum": 1,
|
|
616
985
|
"default": 10,
|
|
617
986
|
"description": (
|
|
618
|
-
"Maximum number of concurrent sessions per user "
|
|
619
|
-
"(default: 10)."
|
|
987
|
+
"Maximum number of concurrent sessions per user " "(default: 10)."
|
|
620
988
|
),
|
|
621
989
|
},
|
|
622
990
|
"session_inactivity_timeout": {
|
|
@@ -635,8 +1003,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
635
1003
|
"type": "boolean",
|
|
636
1004
|
"default": False,
|
|
637
1005
|
"description": (
|
|
638
|
-
"Require HTTPS in production "
|
|
639
|
-
"(default: false, auto-detected)."
|
|
1006
|
+
"Require HTTPS in production " "(default: false, auto-detected)."
|
|
640
1007
|
),
|
|
641
1008
|
},
|
|
642
1009
|
"cookie_secure": {
|
|
@@ -718,9 +1085,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
718
1085
|
},
|
|
719
1086
|
},
|
|
720
1087
|
"additionalProperties": False,
|
|
721
|
-
"description": (
|
|
722
|
-
"Rate limiting configuration per endpoint type."
|
|
723
|
-
),
|
|
1088
|
+
"description": ("Rate limiting configuration per endpoint type."),
|
|
724
1089
|
},
|
|
725
1090
|
"password_policy": {
|
|
726
1091
|
"type": "object",
|
|
@@ -747,9 +1112,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
747
1112
|
"require_lowercase": {
|
|
748
1113
|
"type": "boolean",
|
|
749
1114
|
"default": True,
|
|
750
|
-
"description": (
|
|
751
|
-
"Require lowercase letters " "(default: true)"
|
|
752
|
-
),
|
|
1115
|
+
"description": ("Require lowercase letters " "(default: true)"),
|
|
753
1116
|
},
|
|
754
1117
|
"require_numbers": {
|
|
755
1118
|
"type": "boolean",
|
|
@@ -774,24 +1137,21 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
774
1137
|
"type": "boolean",
|
|
775
1138
|
"default": True,
|
|
776
1139
|
"description": (
|
|
777
|
-
"Enable session fingerprinting "
|
|
778
|
-
"(default: true)"
|
|
1140
|
+
"Enable session fingerprinting " "(default: true)"
|
|
779
1141
|
),
|
|
780
1142
|
},
|
|
781
1143
|
"validate_on_login": {
|
|
782
1144
|
"type": "boolean",
|
|
783
1145
|
"default": True,
|
|
784
1146
|
"description": (
|
|
785
|
-
"Validate fingerprint on login "
|
|
786
|
-
"(default: true)"
|
|
1147
|
+
"Validate fingerprint on login " "(default: true)"
|
|
787
1148
|
),
|
|
788
1149
|
},
|
|
789
1150
|
"validate_on_refresh": {
|
|
790
1151
|
"type": "boolean",
|
|
791
1152
|
"default": True,
|
|
792
1153
|
"description": (
|
|
793
|
-
"Validate fingerprint on token refresh "
|
|
794
|
-
"(default: true)"
|
|
1154
|
+
"Validate fingerprint on token refresh " "(default: true)"
|
|
795
1155
|
),
|
|
796
1156
|
},
|
|
797
1157
|
"validate_on_request": {
|
|
@@ -837,8 +1197,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
837
1197
|
"minimum": 1,
|
|
838
1198
|
"default": 900,
|
|
839
1199
|
"description": (
|
|
840
|
-
"Lockout duration in seconds "
|
|
841
|
-
"(default: 900 = 15 minutes)"
|
|
1200
|
+
"Lockout duration in seconds " "(default: 900 = 15 minutes)"
|
|
842
1201
|
),
|
|
843
1202
|
},
|
|
844
1203
|
"reset_on_success": {
|
|
@@ -860,8 +1219,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
860
1219
|
"type": "boolean",
|
|
861
1220
|
"default": False,
|
|
862
1221
|
"description": (
|
|
863
|
-
"Enable IP address validation "
|
|
864
|
-
"(default: false)"
|
|
1222
|
+
"Enable IP address validation " "(default: false)"
|
|
865
1223
|
),
|
|
866
1224
|
},
|
|
867
1225
|
"strict": {
|
|
@@ -876,8 +1234,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
876
1234
|
"type": "boolean",
|
|
877
1235
|
"default": True,
|
|
878
1236
|
"description": (
|
|
879
|
-
"Allow IP address changes during session "
|
|
880
|
-
"(default: true)"
|
|
1237
|
+
"Allow IP address changes during session " "(default: true)"
|
|
881
1238
|
),
|
|
882
1239
|
},
|
|
883
1240
|
},
|
|
@@ -897,9 +1254,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
897
1254
|
"bind_to_device": {
|
|
898
1255
|
"type": "boolean",
|
|
899
1256
|
"default": True,
|
|
900
|
-
"description": (
|
|
901
|
-
"Bind tokens to device ID " "(default: true)"
|
|
902
|
-
),
|
|
1257
|
+
"description": ("Bind tokens to device ID " "(default: true)"),
|
|
903
1258
|
},
|
|
904
1259
|
},
|
|
905
1260
|
"additionalProperties": False,
|
|
@@ -913,8 +1268,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
913
1268
|
"type": "boolean",
|
|
914
1269
|
"default": True,
|
|
915
1270
|
"description": (
|
|
916
|
-
"Automatically set up token management on app startup "
|
|
917
|
-
"(default: true)."
|
|
1271
|
+
"Automatically set up token management on app startup " "(default: true)."
|
|
918
1272
|
),
|
|
919
1273
|
},
|
|
920
1274
|
},
|
|
@@ -946,9 +1300,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
946
1300
|
},
|
|
947
1301
|
"collection_settings": {
|
|
948
1302
|
"type": "object",
|
|
949
|
-
"patternProperties": {
|
|
950
|
-
"^[a-zA-Z0-9_]+$": {"$ref": "#/definitions/collectionSettings"}
|
|
951
|
-
},
|
|
1303
|
+
"patternProperties": {"^[a-zA-Z0-9_]+$": {"$ref": "#/definitions/collectionSettings"}},
|
|
952
1304
|
"description": "Collection name -> collection settings",
|
|
953
1305
|
},
|
|
954
1306
|
"websockets": {
|
|
@@ -996,8 +1348,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
996
1348
|
"description": {
|
|
997
1349
|
"type": "string",
|
|
998
1350
|
"description": (
|
|
999
|
-
"Description of what this WebSocket endpoint "
|
|
1000
|
-
"is used for"
|
|
1351
|
+
"Description of what this WebSocket endpoint " "is used for"
|
|
1001
1352
|
),
|
|
1002
1353
|
},
|
|
1003
1354
|
"ping_interval": {
|
|
@@ -1210,8 +1561,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
1210
1561
|
"type": "boolean",
|
|
1211
1562
|
"default": False,
|
|
1212
1563
|
"description": (
|
|
1213
|
-
"Allow credentials (cookies, authorization headers) "
|
|
1214
|
-
"in CORS requests"
|
|
1564
|
+
"Allow credentials (cookies, authorization headers) " "in CORS requests"
|
|
1215
1565
|
),
|
|
1216
1566
|
},
|
|
1217
1567
|
"allow_methods": {
|
|
@@ -1256,6 +1606,40 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
1256
1606
|
"additionalProperties": False,
|
|
1257
1607
|
"description": "CORS (Cross-Origin Resource Sharing) configuration for web apps",
|
|
1258
1608
|
},
|
|
1609
|
+
"data_access": {
|
|
1610
|
+
"type": "object",
|
|
1611
|
+
"properties": {
|
|
1612
|
+
"read_scopes": {
|
|
1613
|
+
"type": "array",
|
|
1614
|
+
"items": {"type": "string"},
|
|
1615
|
+
"description": (
|
|
1616
|
+
"List of app slugs this app can read from. "
|
|
1617
|
+
"Defaults to [app_slug] if not specified."
|
|
1618
|
+
),
|
|
1619
|
+
},
|
|
1620
|
+
"write_scope": {
|
|
1621
|
+
"type": "string",
|
|
1622
|
+
"description": (
|
|
1623
|
+
"App slug this app writes to. " "Defaults to app_slug if not specified."
|
|
1624
|
+
),
|
|
1625
|
+
},
|
|
1626
|
+
"cross_app_policy": {
|
|
1627
|
+
"type": "string",
|
|
1628
|
+
"enum": ["explicit", "deny_all"],
|
|
1629
|
+
"default": "explicit",
|
|
1630
|
+
"description": (
|
|
1631
|
+
"Policy for cross-app access. 'explicit' allows access "
|
|
1632
|
+
"to apps listed in read_scopes. 'deny_all' blocks all "
|
|
1633
|
+
"cross-app access regardless of read_scopes."
|
|
1634
|
+
),
|
|
1635
|
+
},
|
|
1636
|
+
},
|
|
1637
|
+
"additionalProperties": False,
|
|
1638
|
+
"description": (
|
|
1639
|
+
"Data access configuration defining which apps this app can "
|
|
1640
|
+
"read from and write to. Used for cross-app data access control."
|
|
1641
|
+
),
|
|
1642
|
+
},
|
|
1259
1643
|
"observability": {
|
|
1260
1644
|
"type": "object",
|
|
1261
1645
|
"properties": {
|
|
@@ -1295,8 +1679,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
1295
1679
|
"type": "boolean",
|
|
1296
1680
|
"default": True,
|
|
1297
1681
|
"description": (
|
|
1298
|
-
"Collect operation-level metrics "
|
|
1299
|
-
"(duration, errors, etc.)"
|
|
1682
|
+
"Collect operation-level metrics " "(duration, errors, etc.)"
|
|
1300
1683
|
),
|
|
1301
1684
|
},
|
|
1302
1685
|
"collect_performance_metrics": {
|
|
@@ -1349,6 +1732,99 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
1349
1732
|
"additionalProperties": False,
|
|
1350
1733
|
"description": "Observability configuration (health checks, metrics, logging)",
|
|
1351
1734
|
},
|
|
1735
|
+
"multi_app": {
|
|
1736
|
+
"type": "object",
|
|
1737
|
+
"properties": {
|
|
1738
|
+
"enabled": {
|
|
1739
|
+
"type": "boolean",
|
|
1740
|
+
"default": False,
|
|
1741
|
+
"description": "Enable multi-app mounting mode",
|
|
1742
|
+
},
|
|
1743
|
+
"apps": {
|
|
1744
|
+
"type": "array",
|
|
1745
|
+
"items": {
|
|
1746
|
+
"type": "object",
|
|
1747
|
+
"properties": {
|
|
1748
|
+
"slug": {
|
|
1749
|
+
"type": "string",
|
|
1750
|
+
"pattern": "^[a-z0-9_-]+$",
|
|
1751
|
+
"description": (
|
|
1752
|
+
"App slug (lowercase alphanumeric, underscores, hyphens)"
|
|
1753
|
+
),
|
|
1754
|
+
},
|
|
1755
|
+
"manifest": {
|
|
1756
|
+
"type": "string",
|
|
1757
|
+
"description": (
|
|
1758
|
+
"Path to manifest.json file "
|
|
1759
|
+
"(relative to multi_app manifest or absolute)"
|
|
1760
|
+
),
|
|
1761
|
+
},
|
|
1762
|
+
"path_prefix": {
|
|
1763
|
+
"type": "string",
|
|
1764
|
+
"pattern": "^/.*",
|
|
1765
|
+
"default": "/{slug}",
|
|
1766
|
+
"description": (
|
|
1767
|
+
"Path prefix for mounting (defaults to /{slug}). "
|
|
1768
|
+
"Must start with '/' and be unique across all apps."
|
|
1769
|
+
),
|
|
1770
|
+
},
|
|
1771
|
+
"on_startup": {
|
|
1772
|
+
"type": "string",
|
|
1773
|
+
"description": (
|
|
1774
|
+
"Optional: Python function path for startup callback "
|
|
1775
|
+
"(e.g., 'module.function_name'). "
|
|
1776
|
+
"Not yet supported in manifest-based config."
|
|
1777
|
+
),
|
|
1778
|
+
},
|
|
1779
|
+
"on_shutdown": {
|
|
1780
|
+
"type": "string",
|
|
1781
|
+
"description": (
|
|
1782
|
+
"Optional: Python function path for shutdown callback "
|
|
1783
|
+
"(e.g., 'module.function_name'). "
|
|
1784
|
+
"Not yet supported in manifest-based config."
|
|
1785
|
+
),
|
|
1786
|
+
},
|
|
1787
|
+
},
|
|
1788
|
+
"required": ["slug", "manifest"],
|
|
1789
|
+
"additionalProperties": False,
|
|
1790
|
+
},
|
|
1791
|
+
"minItems": 1,
|
|
1792
|
+
"description": "List of apps to mount in multi-app mode",
|
|
1793
|
+
},
|
|
1794
|
+
"shared_middleware": {
|
|
1795
|
+
"type": "object",
|
|
1796
|
+
"properties": {
|
|
1797
|
+
"cors": {
|
|
1798
|
+
"type": "boolean",
|
|
1799
|
+
"default": True,
|
|
1800
|
+
"description": "Enable CORS middleware at parent level (default: true)",
|
|
1801
|
+
},
|
|
1802
|
+
"rate_limiting": {
|
|
1803
|
+
"type": "boolean",
|
|
1804
|
+
"default": True,
|
|
1805
|
+
"description": (
|
|
1806
|
+
"Enable rate limiting middleware at parent level (default: true)"
|
|
1807
|
+
),
|
|
1808
|
+
},
|
|
1809
|
+
"health_checks": {
|
|
1810
|
+
"type": "boolean",
|
|
1811
|
+
"default": True,
|
|
1812
|
+
"description": (
|
|
1813
|
+
"Enable unified health check endpoint at /health (default: true)"
|
|
1814
|
+
),
|
|
1815
|
+
},
|
|
1816
|
+
},
|
|
1817
|
+
"additionalProperties": False,
|
|
1818
|
+
"description": "Shared middleware configuration for parent app",
|
|
1819
|
+
},
|
|
1820
|
+
},
|
|
1821
|
+
"additionalProperties": False,
|
|
1822
|
+
"description": (
|
|
1823
|
+
"Multi-app mounting configuration. When enabled, allows mounting "
|
|
1824
|
+
"multiple FastAPI apps under a single parent app with path prefixes. "
|
|
1825
|
+
"Useful for deploying multiple apps (e.g., SSO apps) on a single service."
|
|
1826
|
+
),
|
|
1827
|
+
},
|
|
1352
1828
|
"initial_data": {
|
|
1353
1829
|
"type": "object",
|
|
1354
1830
|
"patternProperties": {
|
|
@@ -1456,8 +1932,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
1456
1932
|
"definition": {
|
|
1457
1933
|
"type": "object",
|
|
1458
1934
|
"description": (
|
|
1459
|
-
"Index definition (required for vectorSearch and "
|
|
1460
|
-
"search indexes)"
|
|
1935
|
+
"Index definition (required for vectorSearch and " "search indexes)"
|
|
1461
1936
|
),
|
|
1462
1937
|
},
|
|
1463
1938
|
"hybrid": {
|
|
@@ -1470,8 +1945,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
1470
1945
|
"type": "string",
|
|
1471
1946
|
"pattern": "^[a-zA-Z0-9_]+$",
|
|
1472
1947
|
"description": (
|
|
1473
|
-
"Name for the vector index "
|
|
1474
|
-
"(defaults to '{name}_vector')"
|
|
1948
|
+
"Name for the vector index " "(defaults to '{name}_vector')"
|
|
1475
1949
|
),
|
|
1476
1950
|
},
|
|
1477
1951
|
"definition": {
|
|
@@ -1493,8 +1967,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
1493
1967
|
"type": "string",
|
|
1494
1968
|
"pattern": "^[a-zA-Z0-9_]+$",
|
|
1495
1969
|
"description": (
|
|
1496
|
-
"Name for the text index "
|
|
1497
|
-
"(defaults to '{name}_text')"
|
|
1970
|
+
"Name for the text index " "(defaults to '{name}_text')"
|
|
1498
1971
|
),
|
|
1499
1972
|
},
|
|
1500
1973
|
"definition": {
|
|
@@ -1575,9 +2048,7 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
1575
2048
|
"then": {"required": ["keys", "options"]},
|
|
1576
2049
|
"else": {
|
|
1577
2050
|
"properties": {
|
|
1578
|
-
"options": {
|
|
1579
|
-
"not": {"required": ["partialFilterExpression"]}
|
|
1580
|
-
}
|
|
2051
|
+
"options": {"not": {"required": ["partialFilterExpression"]}}
|
|
1581
2052
|
}
|
|
1582
2053
|
},
|
|
1583
2054
|
},
|
|
@@ -1727,7 +2198,7 @@ SCHEMA_REGISTRY["default"] = MANIFEST_SCHEMA_V2
|
|
|
1727
2198
|
MANIFEST_SCHEMA = MANIFEST_SCHEMA_V2 # Backward compatibility
|
|
1728
2199
|
|
|
1729
2200
|
|
|
1730
|
-
def get_schema_version(manifest_data:
|
|
2201
|
+
def get_schema_version(manifest_data: dict[str, Any]) -> str:
|
|
1731
2202
|
"""
|
|
1732
2203
|
Detect schema version from manifest.
|
|
1733
2204
|
|
|
@@ -1740,7 +2211,7 @@ def get_schema_version(manifest_data: Dict[str, Any]) -> str:
|
|
|
1740
2211
|
Raises:
|
|
1741
2212
|
ValueError: If schema version format is invalid
|
|
1742
2213
|
"""
|
|
1743
|
-
version:
|
|
2214
|
+
version: str | None = manifest_data.get("schema_version")
|
|
1744
2215
|
if version:
|
|
1745
2216
|
# Validate version format
|
|
1746
2217
|
if not isinstance(version, str) or not version.replace(".", "").isdigit():
|
|
@@ -1762,8 +2233,8 @@ def get_schema_version(manifest_data: Dict[str, Any]) -> str:
|
|
|
1762
2233
|
|
|
1763
2234
|
|
|
1764
2235
|
def migrate_manifest(
|
|
1765
|
-
manifest_data:
|
|
1766
|
-
) ->
|
|
2236
|
+
manifest_data: dict[str, Any], target_version: str = CURRENT_SCHEMA_VERSION
|
|
2237
|
+
) -> dict[str, Any]:
|
|
1767
2238
|
"""
|
|
1768
2239
|
Migrate manifest from one schema version to another.
|
|
1769
2240
|
|
|
@@ -1803,9 +2274,7 @@ def migrate_manifest(
|
|
|
1803
2274
|
|
|
1804
2275
|
# No data transformation needed - V2.0 is backward compatible
|
|
1805
2276
|
# New fields (auth, etc.) are optional
|
|
1806
|
-
logger.debug(
|
|
1807
|
-
f"Migrated manifest from 1.0 to 2.0: {migrated.get('slug', 'unknown')}"
|
|
1808
|
-
)
|
|
2277
|
+
logger.debug(f"Migrated manifest from 1.0 to 2.0: {migrated.get('slug', 'unknown')}")
|
|
1809
2278
|
|
|
1810
2279
|
# Future: Add more migration paths as needed
|
|
1811
2280
|
# Example: 2.0 -> 3.0, etc.
|
|
@@ -1814,7 +2283,7 @@ def migrate_manifest(
|
|
|
1814
2283
|
return migrated
|
|
1815
2284
|
|
|
1816
2285
|
|
|
1817
|
-
def get_schema_for_version(version: str) ->
|
|
2286
|
+
def get_schema_for_version(version: str) -> dict[str, Any]:
|
|
1818
2287
|
"""
|
|
1819
2288
|
Get schema definition for a specific version.
|
|
1820
2289
|
|
|
@@ -1841,15 +2310,14 @@ def get_schema_for_version(version: str) -> Dict[str, Any]:
|
|
|
1841
2310
|
|
|
1842
2311
|
# Fallback to current
|
|
1843
2312
|
logger.warning(
|
|
1844
|
-
f"Schema version {version} not found, using current version "
|
|
1845
|
-
f"{CURRENT_SCHEMA_VERSION}"
|
|
2313
|
+
f"Schema version {version} not found, using current version " f"{CURRENT_SCHEMA_VERSION}"
|
|
1846
2314
|
)
|
|
1847
2315
|
return SCHEMA_REGISTRY[CURRENT_SCHEMA_VERSION]
|
|
1848
2316
|
|
|
1849
2317
|
|
|
1850
2318
|
async def _validate_manifest_async(
|
|
1851
|
-
manifest_data:
|
|
1852
|
-
) ->
|
|
2319
|
+
manifest_data: dict[str, Any], use_cache: bool = True
|
|
2320
|
+
) -> tuple[bool, str | None, list[str] | None]:
|
|
1853
2321
|
"""
|
|
1854
2322
|
Validate a manifest against the JSON Schema with versioning and caching support.
|
|
1855
2323
|
|
|
@@ -1873,12 +2341,11 @@ async def _validate_manifest_async(
|
|
|
1873
2341
|
Use validate_manifest_with_db() for database validation.
|
|
1874
2342
|
"""
|
|
1875
2343
|
# Check cache first
|
|
2344
|
+
cache_key = None
|
|
1876
2345
|
if use_cache:
|
|
1877
|
-
cache_key = (
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
if cache_key in _validation_cache:
|
|
1881
|
-
return _validation_cache[cache_key]
|
|
2346
|
+
cache_key = _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
|
|
2347
|
+
if cache_key in _validation_cache:
|
|
2348
|
+
return _validation_cache[cache_key]
|
|
1882
2349
|
|
|
1883
2350
|
try:
|
|
1884
2351
|
# Get schema version
|
|
@@ -1935,11 +2402,7 @@ async def _validate_manifest_async(
|
|
|
1935
2402
|
error_message = f"Invalid schema definition: {e.message}"
|
|
1936
2403
|
result = (False, error_message, ["schema"])
|
|
1937
2404
|
if use_cache:
|
|
1938
|
-
cache_key = (
|
|
1939
|
-
_get_manifest_hash(manifest_data)
|
|
1940
|
-
+ "_"
|
|
1941
|
-
+ get_schema_version(manifest_data)
|
|
1942
|
-
)
|
|
2405
|
+
cache_key = _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
|
|
1943
2406
|
_validation_cache[cache_key] = result
|
|
1944
2407
|
|
|
1945
2408
|
return result
|
|
@@ -1949,9 +2412,7 @@ async def _validate_manifest_async(
|
|
|
1949
2412
|
error_paths = []
|
|
1950
2413
|
error_messages = []
|
|
1951
2414
|
if isinstance(e, ValidationError):
|
|
1952
|
-
error_paths = [
|
|
1953
|
-
f".{'.'.join(str(p) for p in error.path)}" for error in e.context or [e]
|
|
1954
|
-
]
|
|
2415
|
+
error_paths = [f".{'.'.join(str(p) for p in error.path)}" for error in e.context or [e]]
|
|
1955
2416
|
error_messages = [error.message for error in e.context or [e]]
|
|
1956
2417
|
else:
|
|
1957
2418
|
error_messages = [str(e)]
|
|
@@ -1959,11 +2420,7 @@ async def _validate_manifest_async(
|
|
|
1959
2420
|
error_message = "; ".join(error_messages) if error_messages else str(e)
|
|
1960
2421
|
result = (False, error_message, error_paths if error_paths else None)
|
|
1961
2422
|
if use_cache:
|
|
1962
|
-
cache_key = (
|
|
1963
|
-
_get_manifest_hash(manifest_data)
|
|
1964
|
-
+ "_"
|
|
1965
|
-
+ get_schema_version(manifest_data)
|
|
1966
|
-
)
|
|
2423
|
+
cache_key = _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
|
|
1967
2424
|
_validation_cache[cache_key] = result
|
|
1968
2425
|
|
|
1969
2426
|
return result
|
|
@@ -1973,11 +2430,7 @@ async def _validate_manifest_async(
|
|
|
1973
2430
|
logger.exception("Unexpected error during manifest validation")
|
|
1974
2431
|
result = (False, error_message, None)
|
|
1975
2432
|
if use_cache:
|
|
1976
|
-
cache_key = (
|
|
1977
|
-
_get_manifest_hash(manifest_data)
|
|
1978
|
-
+ "_"
|
|
1979
|
-
+ get_schema_version(manifest_data)
|
|
1980
|
-
)
|
|
2433
|
+
cache_key = _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
|
|
1981
2434
|
_validation_cache[cache_key] = result
|
|
1982
2435
|
|
|
1983
2436
|
return result
|
|
@@ -1991,8 +2444,8 @@ def clear_validation_cache():
|
|
|
1991
2444
|
|
|
1992
2445
|
|
|
1993
2446
|
async def validate_manifests_parallel(
|
|
1994
|
-
manifests:
|
|
1995
|
-
) ->
|
|
2447
|
+
manifests: list[dict[str, Any]], use_cache: bool = True
|
|
2448
|
+
) -> list[tuple[bool, str | None, list[str] | None, str | None]]:
|
|
1996
2449
|
"""
|
|
1997
2450
|
Validate multiple manifests in parallel for scale.
|
|
1998
2451
|
|
|
@@ -2006,27 +2459,21 @@ async def validate_manifests_parallel(
|
|
|
2006
2459
|
"""
|
|
2007
2460
|
|
|
2008
2461
|
async def validate_one(
|
|
2009
|
-
manifest:
|
|
2010
|
-
) ->
|
|
2462
|
+
manifest: dict[str, Any],
|
|
2463
|
+
) -> tuple[bool, str | None, list[str] | None, str | None]:
|
|
2011
2464
|
slug = manifest.get("slug", "unknown")
|
|
2012
|
-
is_valid, error, paths = await _validate_manifest_async(
|
|
2013
|
-
manifest, use_cache=use_cache
|
|
2014
|
-
)
|
|
2465
|
+
is_valid, error, paths = await _validate_manifest_async(manifest, use_cache=use_cache)
|
|
2015
2466
|
return (is_valid, error, paths, slug)
|
|
2016
2467
|
|
|
2017
2468
|
# Run validations in parallel
|
|
2018
|
-
results = await asyncio.gather(
|
|
2019
|
-
*[validate_one(m) for m in manifests], return_exceptions=True
|
|
2020
|
-
)
|
|
2469
|
+
results = await asyncio.gather(*[validate_one(m) for m in manifests], return_exceptions=True)
|
|
2021
2470
|
|
|
2022
2471
|
# Handle exceptions
|
|
2023
2472
|
validated_results = []
|
|
2024
2473
|
for i, result in enumerate(results):
|
|
2025
2474
|
if isinstance(result, Exception):
|
|
2026
2475
|
slug = manifests[i].get("slug", "unknown")
|
|
2027
|
-
validated_results.append(
|
|
2028
|
-
(False, f"Validation error: {str(result)}", None, slug)
|
|
2029
|
-
)
|
|
2476
|
+
validated_results.append((False, f"Validation error: {str(result)}", None, slug))
|
|
2030
2477
|
else:
|
|
2031
2478
|
validated_results.append(result)
|
|
2032
2479
|
|
|
@@ -2034,8 +2481,8 @@ async def validate_manifests_parallel(
|
|
|
2034
2481
|
|
|
2035
2482
|
|
|
2036
2483
|
async def validate_developer_id(
|
|
2037
|
-
developer_id: str, db_validator:
|
|
2038
|
-
) ->
|
|
2484
|
+
developer_id: str, db_validator: Callable[[str], Awaitable[bool]] | None = None
|
|
2485
|
+
) -> tuple[bool, str | None]:
|
|
2039
2486
|
"""
|
|
2040
2487
|
Validate that a developer_id exists in the system and has developer role.
|
|
2041
2488
|
|
|
@@ -2072,19 +2519,17 @@ async def validate_developer_id(
|
|
|
2072
2519
|
f"developer_id '{developer_id}' does not exist or does not have developer role",
|
|
2073
2520
|
)
|
|
2074
2521
|
except (ValueError, TypeError, AttributeError) as e:
|
|
2075
|
-
logger.exception(
|
|
2076
|
-
f"Validation error validating developer_id '{developer_id}'"
|
|
2077
|
-
)
|
|
2522
|
+
logger.exception(f"Validation error validating developer_id '{developer_id}'")
|
|
2078
2523
|
return False, f"Error validating developer_id: {e}"
|
|
2079
2524
|
|
|
2080
2525
|
return True, None
|
|
2081
2526
|
|
|
2082
2527
|
|
|
2083
2528
|
async def validate_manifest_with_db(
|
|
2084
|
-
manifest_data:
|
|
2529
|
+
manifest_data: dict[str, Any],
|
|
2085
2530
|
db_validator: Callable[[str], Awaitable[bool]],
|
|
2086
2531
|
use_cache: bool = True,
|
|
2087
|
-
) ->
|
|
2532
|
+
) -> tuple[bool, str | None, list[str] | None]:
|
|
2088
2533
|
"""
|
|
2089
2534
|
Validate a manifest against the JSON Schema (with versioning) and check
|
|
2090
2535
|
developer_id exists in system.
|
|
@@ -2125,8 +2570,8 @@ async def validate_manifest_with_db(
|
|
|
2125
2570
|
# Public API: Synchronous wrapper for backward compatibility
|
|
2126
2571
|
# Most callers use this synchronously, so we provide a sync wrapper
|
|
2127
2572
|
def validate_manifest(
|
|
2128
|
-
manifest_data:
|
|
2129
|
-
) ->
|
|
2573
|
+
manifest_data: dict[str, Any], use_cache: bool = True
|
|
2574
|
+
) -> tuple[bool, str | None, list[str] | None]:
|
|
2130
2575
|
"""
|
|
2131
2576
|
Validate a manifest against the JSON Schema with versioning and caching
|
|
2132
2577
|
support (synchronous wrapper).
|
|
@@ -2154,23 +2599,19 @@ def validate_manifest(
|
|
|
2154
2599
|
|
|
2155
2600
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
2156
2601
|
future = executor.submit(
|
|
2157
|
-
lambda: asyncio.run(
|
|
2158
|
-
_validate_manifest_async(manifest_data, use_cache)
|
|
2159
|
-
)
|
|
2602
|
+
lambda: asyncio.run(_validate_manifest_async(manifest_data, use_cache))
|
|
2160
2603
|
)
|
|
2161
2604
|
return future.result()
|
|
2162
2605
|
else:
|
|
2163
|
-
return loop.run_until_complete(
|
|
2164
|
-
_validate_manifest_async(manifest_data, use_cache)
|
|
2165
|
-
)
|
|
2606
|
+
return loop.run_until_complete(_validate_manifest_async(manifest_data, use_cache))
|
|
2166
2607
|
except RuntimeError:
|
|
2167
2608
|
# No event loop, create one
|
|
2168
2609
|
return asyncio.run(_validate_manifest_async(manifest_data, use_cache))
|
|
2169
2610
|
|
|
2170
2611
|
|
|
2171
2612
|
def _validate_regular_index(
|
|
2172
|
-
index_def:
|
|
2173
|
-
) ->
|
|
2613
|
+
index_def: dict[str, Any], collection_name: str, index_name: str
|
|
2614
|
+
) -> tuple[bool, str | None]:
|
|
2174
2615
|
"""Validate a regular index definition."""
|
|
2175
2616
|
if "keys" not in index_def:
|
|
2176
2617
|
return (
|
|
@@ -2186,8 +2627,7 @@ def _validate_regular_index(
|
|
|
2186
2627
|
):
|
|
2187
2628
|
return (
|
|
2188
2629
|
False,
|
|
2189
|
-
f"Regular index '{index_name}' in collection "
|
|
2190
|
-
f"'{collection_name}' has empty 'keys'",
|
|
2630
|
+
f"Regular index '{index_name}' in collection " f"'{collection_name}' has empty 'keys'",
|
|
2191
2631
|
)
|
|
2192
2632
|
|
|
2193
2633
|
# Check for _id index
|
|
@@ -2208,14 +2648,13 @@ def _validate_regular_index(
|
|
|
2208
2648
|
|
|
2209
2649
|
|
|
2210
2650
|
def _validate_ttl_index(
|
|
2211
|
-
index_def:
|
|
2212
|
-
) ->
|
|
2651
|
+
index_def: dict[str, Any], collection_name: str, index_name: str
|
|
2652
|
+
) -> tuple[bool, str | None]:
|
|
2213
2653
|
"""Validate a TTL index definition."""
|
|
2214
2654
|
if "keys" not in index_def:
|
|
2215
2655
|
return (
|
|
2216
2656
|
False,
|
|
2217
|
-
f"TTL index '{index_name}' in collection '{collection_name}' "
|
|
2218
|
-
f"requires 'keys' field",
|
|
2657
|
+
f"TTL index '{index_name}' in collection '{collection_name}' " f"requires 'keys' field",
|
|
2219
2658
|
)
|
|
2220
2659
|
options = index_def.get("options", {})
|
|
2221
2660
|
if "expireAfterSeconds" not in options:
|
|
@@ -2243,8 +2682,8 @@ def _validate_ttl_index(
|
|
|
2243
2682
|
|
|
2244
2683
|
|
|
2245
2684
|
def _validate_partial_index(
|
|
2246
|
-
index_def:
|
|
2247
|
-
) ->
|
|
2685
|
+
index_def: dict[str, Any], collection_name: str, index_name: str
|
|
2686
|
+
) -> tuple[bool, str | None]:
|
|
2248
2687
|
"""Validate a partial index definition."""
|
|
2249
2688
|
if "keys" not in index_def:
|
|
2250
2689
|
return (
|
|
@@ -2264,8 +2703,8 @@ def _validate_partial_index(
|
|
|
2264
2703
|
|
|
2265
2704
|
|
|
2266
2705
|
def _validate_text_index(
|
|
2267
|
-
index_def:
|
|
2268
|
-
) ->
|
|
2706
|
+
index_def: dict[str, Any], collection_name: str, index_name: str
|
|
2707
|
+
) -> tuple[bool, str | None]:
|
|
2269
2708
|
"""Validate a text index definition."""
|
|
2270
2709
|
if "keys" not in index_def:
|
|
2271
2710
|
return (
|
|
@@ -2290,8 +2729,8 @@ def _validate_text_index(
|
|
|
2290
2729
|
|
|
2291
2730
|
|
|
2292
2731
|
def _validate_geospatial_index(
|
|
2293
|
-
index_def:
|
|
2294
|
-
) ->
|
|
2732
|
+
index_def: dict[str, Any], collection_name: str, index_name: str
|
|
2733
|
+
) -> tuple[bool, str | None]:
|
|
2295
2734
|
"""Validate a geospatial index definition."""
|
|
2296
2735
|
if "keys" not in index_def:
|
|
2297
2736
|
return (
|
|
@@ -2305,9 +2744,7 @@ def _validate_geospatial_index(
|
|
|
2305
2744
|
if isinstance(keys, dict):
|
|
2306
2745
|
has_geo = any(v in ["2dsphere", "2d", "geoHaystack"] for v in keys.values())
|
|
2307
2746
|
elif isinstance(keys, list):
|
|
2308
|
-
has_geo = any(
|
|
2309
|
-
len(k) >= 2 and k[1] in ["2dsphere", "2d", "geoHaystack"] for k in keys
|
|
2310
|
-
)
|
|
2747
|
+
has_geo = any(len(k) >= 2 and k[1] in ["2dsphere", "2d", "geoHaystack"] for k in keys)
|
|
2311
2748
|
if not has_geo:
|
|
2312
2749
|
return (
|
|
2313
2750
|
False,
|
|
@@ -2319,8 +2756,8 @@ def _validate_geospatial_index(
|
|
|
2319
2756
|
|
|
2320
2757
|
|
|
2321
2758
|
def _validate_vector_search_index(
|
|
2322
|
-
index_def:
|
|
2323
|
-
) ->
|
|
2759
|
+
index_def: dict[str, Any], collection_name: str, index_name: str, index_type: str
|
|
2760
|
+
) -> tuple[bool, str | None]:
|
|
2324
2761
|
"""Validate a vectorSearch or search index definition."""
|
|
2325
2762
|
if "definition" not in index_def:
|
|
2326
2763
|
return (
|
|
@@ -2367,8 +2804,8 @@ def _validate_vector_search_index(
|
|
|
2367
2804
|
|
|
2368
2805
|
|
|
2369
2806
|
def _validate_hybrid_index(
|
|
2370
|
-
index_def:
|
|
2371
|
-
) ->
|
|
2807
|
+
index_def: dict[str, Any], collection_name: str, index_name: str
|
|
2808
|
+
) -> tuple[bool, str | None]:
|
|
2372
2809
|
"""Validate a hybrid index definition."""
|
|
2373
2810
|
if "hybrid" not in index_def:
|
|
2374
2811
|
return (
|
|
@@ -2431,8 +2868,8 @@ def _validate_hybrid_index(
|
|
|
2431
2868
|
|
|
2432
2869
|
|
|
2433
2870
|
def validate_index_definition(
|
|
2434
|
-
index_def:
|
|
2435
|
-
) ->
|
|
2871
|
+
index_def: dict[str, Any], collection_name: str, index_name: str
|
|
2872
|
+
) -> tuple[bool, str | None]:
|
|
2436
2873
|
"""
|
|
2437
2874
|
Validate a single index definition with context-specific checks.
|
|
2438
2875
|
|
|
@@ -2448,8 +2885,7 @@ def validate_index_definition(
|
|
|
2448
2885
|
if not index_type:
|
|
2449
2886
|
return (
|
|
2450
2887
|
False,
|
|
2451
|
-
f"Index '{index_name}' in collection '{collection_name}' "
|
|
2452
|
-
f"is missing 'type' field",
|
|
2888
|
+
f"Index '{index_name}' in collection '{collection_name}' " f"is missing 'type' field",
|
|
2453
2889
|
)
|
|
2454
2890
|
|
|
2455
2891
|
# Type-specific validation
|
|
@@ -2464,9 +2900,7 @@ def validate_index_definition(
|
|
|
2464
2900
|
elif index_type == "geospatial":
|
|
2465
2901
|
return _validate_geospatial_index(index_def, collection_name, index_name)
|
|
2466
2902
|
elif index_type in ("vectorSearch", "search"):
|
|
2467
|
-
return _validate_vector_search_index(
|
|
2468
|
-
index_def, collection_name, index_name, index_type
|
|
2469
|
-
)
|
|
2903
|
+
return _validate_vector_search_index(index_def, collection_name, index_name, index_type)
|
|
2470
2904
|
elif index_type == "hybrid":
|
|
2471
2905
|
return _validate_hybrid_index(index_def, collection_name, index_name)
|
|
2472
2906
|
else:
|
|
@@ -2478,8 +2912,8 @@ def validate_index_definition(
|
|
|
2478
2912
|
|
|
2479
2913
|
|
|
2480
2914
|
def validate_managed_indexes(
|
|
2481
|
-
managed_indexes:
|
|
2482
|
-
) ->
|
|
2915
|
+
managed_indexes: dict[str, list[dict[str, Any]]],
|
|
2916
|
+
) -> tuple[bool, str | None]:
|
|
2483
2917
|
"""
|
|
2484
2918
|
Validate all managed indexes with collection and index context.
|
|
2485
2919
|
|
|
@@ -2516,9 +2950,7 @@ def validate_managed_indexes(
|
|
|
2516
2950
|
)
|
|
2517
2951
|
|
|
2518
2952
|
index_name = index_def.get("name", f"index_{idx}")
|
|
2519
|
-
is_valid, error_msg = validate_index_definition(
|
|
2520
|
-
index_def, collection_name, index_name
|
|
2521
|
-
)
|
|
2953
|
+
is_valid, error_msg = validate_index_definition(index_def, collection_name, index_name)
|
|
2522
2954
|
if not is_valid:
|
|
2523
2955
|
return False, error_msg
|
|
2524
2956
|
|
|
@@ -2549,8 +2981,8 @@ class ManifestValidator:
|
|
|
2549
2981
|
|
|
2550
2982
|
@staticmethod
|
|
2551
2983
|
def validate(
|
|
2552
|
-
manifest:
|
|
2553
|
-
) ->
|
|
2984
|
+
manifest: dict[str, Any], use_cache: bool = True
|
|
2985
|
+
) -> tuple[bool, str | None, list[str] | None]:
|
|
2554
2986
|
"""
|
|
2555
2987
|
Validate manifest against schema.
|
|
2556
2988
|
|
|
@@ -2565,8 +2997,8 @@ class ManifestValidator:
|
|
|
2565
2997
|
|
|
2566
2998
|
@staticmethod
|
|
2567
2999
|
async def validate_async(
|
|
2568
|
-
manifest:
|
|
2569
|
-
) ->
|
|
3000
|
+
manifest: dict[str, Any], use_cache: bool = True
|
|
3001
|
+
) -> tuple[bool, str | None, list[str] | None]:
|
|
2570
3002
|
"""
|
|
2571
3003
|
Validate manifest asynchronously.
|
|
2572
3004
|
|
|
@@ -2585,10 +3017,10 @@ class ManifestValidator:
|
|
|
2585
3017
|
|
|
2586
3018
|
@staticmethod
|
|
2587
3019
|
async def validate_with_db(
|
|
2588
|
-
manifest:
|
|
3020
|
+
manifest: dict[str, Any],
|
|
2589
3021
|
db_validator: Callable[[str], Awaitable[bool]],
|
|
2590
3022
|
use_cache: bool = True,
|
|
2591
|
-
) ->
|
|
3023
|
+
) -> tuple[bool, str | None, list[str] | None]:
|
|
2592
3024
|
"""
|
|
2593
3025
|
Validate manifest and check developer_id exists in database.
|
|
2594
3026
|
|
|
@@ -2600,14 +3032,12 @@ class ManifestValidator:
|
|
|
2600
3032
|
Returns:
|
|
2601
3033
|
Tuple of (is_valid, error_message, error_paths)
|
|
2602
3034
|
"""
|
|
2603
|
-
return await validate_manifest_with_db(
|
|
2604
|
-
manifest, db_validator, use_cache=use_cache
|
|
2605
|
-
)
|
|
3035
|
+
return await validate_manifest_with_db(manifest, db_validator, use_cache=use_cache)
|
|
2606
3036
|
|
|
2607
3037
|
@staticmethod
|
|
2608
3038
|
def validate_managed_indexes(
|
|
2609
|
-
managed_indexes:
|
|
2610
|
-
) ->
|
|
3039
|
+
managed_indexes: dict[str, list[dict[str, Any]]],
|
|
3040
|
+
) -> tuple[bool, str | None]:
|
|
2611
3041
|
"""
|
|
2612
3042
|
Validate managed indexes configuration.
|
|
2613
3043
|
|
|
@@ -2621,8 +3051,8 @@ class ManifestValidator:
|
|
|
2621
3051
|
|
|
2622
3052
|
@staticmethod
|
|
2623
3053
|
def validate_index_definition(
|
|
2624
|
-
index_def:
|
|
2625
|
-
) ->
|
|
3054
|
+
index_def: dict[str, Any], collection_name: str, index_name: str
|
|
3055
|
+
) -> tuple[bool, str | None]:
|
|
2626
3056
|
"""
|
|
2627
3057
|
Validate a single index definition.
|
|
2628
3058
|
|
|
@@ -2637,7 +3067,7 @@ class ManifestValidator:
|
|
|
2637
3067
|
return validate_index_definition(index_def, collection_name, index_name)
|
|
2638
3068
|
|
|
2639
3069
|
@staticmethod
|
|
2640
|
-
def get_schema_version(manifest:
|
|
3070
|
+
def get_schema_version(manifest: dict[str, Any]) -> str:
|
|
2641
3071
|
"""
|
|
2642
3072
|
Get schema version from manifest.
|
|
2643
3073
|
|
|
@@ -2651,8 +3081,8 @@ class ManifestValidator:
|
|
|
2651
3081
|
|
|
2652
3082
|
@staticmethod
|
|
2653
3083
|
def migrate(
|
|
2654
|
-
manifest:
|
|
2655
|
-
) ->
|
|
3084
|
+
manifest: dict[str, Any], target_version: str = CURRENT_SCHEMA_VERSION
|
|
3085
|
+
) -> dict[str, Any]:
|
|
2656
3086
|
"""
|
|
2657
3087
|
Migrate manifest to target schema version.
|
|
2658
3088
|
|
|
@@ -2679,7 +3109,7 @@ class ManifestParser:
|
|
|
2679
3109
|
with automatic validation and migration.
|
|
2680
3110
|
"""
|
|
2681
3111
|
|
|
2682
|
-
def __init__(self, validator:
|
|
3112
|
+
def __init__(self, validator: ManifestValidator | None = None):
|
|
2683
3113
|
"""
|
|
2684
3114
|
Initialize parser.
|
|
2685
3115
|
|
|
@@ -2689,7 +3119,7 @@ class ManifestParser:
|
|
|
2689
3119
|
self.validator = validator or ManifestValidator()
|
|
2690
3120
|
|
|
2691
3121
|
@staticmethod
|
|
2692
|
-
async def load_from_file(path: Any, validate: bool = True) ->
|
|
3122
|
+
async def load_from_file(path: Any, validate: bool = True) -> dict[str, Any]:
|
|
2693
3123
|
"""
|
|
2694
3124
|
Load and validate manifest from file.
|
|
2695
3125
|
|
|
@@ -2720,17 +3150,13 @@ class ManifestParser:
|
|
|
2720
3150
|
if validate:
|
|
2721
3151
|
is_valid, error, paths = ManifestValidator.validate(manifest_data)
|
|
2722
3152
|
if not is_valid:
|
|
2723
|
-
error_path_str = (
|
|
2724
|
-
f" (errors in: {', '.join(paths[:3])})" if paths else ""
|
|
2725
|
-
)
|
|
3153
|
+
error_path_str = f" (errors in: {', '.join(paths[:3])})" if paths else ""
|
|
2726
3154
|
raise ValueError(f"Manifest validation failed: {error}{error_path_str}")
|
|
2727
3155
|
|
|
2728
3156
|
return manifest_data
|
|
2729
3157
|
|
|
2730
3158
|
@staticmethod
|
|
2731
|
-
async def load_from_dict(
|
|
2732
|
-
data: Dict[str, Any], validate: bool = True
|
|
2733
|
-
) -> Dict[str, Any]:
|
|
3159
|
+
async def load_from_dict(data: dict[str, Any], validate: bool = True) -> dict[str, Any]:
|
|
2734
3160
|
"""
|
|
2735
3161
|
Load and validate manifest from dictionary.
|
|
2736
3162
|
|
|
@@ -2748,15 +3174,13 @@ class ManifestParser:
|
|
|
2748
3174
|
if validate:
|
|
2749
3175
|
is_valid, error, paths = ManifestValidator.validate(data)
|
|
2750
3176
|
if not is_valid:
|
|
2751
|
-
error_path_str = (
|
|
2752
|
-
f" (errors in: {', '.join(paths[:3])})" if paths else ""
|
|
2753
|
-
)
|
|
3177
|
+
error_path_str = f" (errors in: {', '.join(paths[:3])})" if paths else ""
|
|
2754
3178
|
raise ValueError(f"Manifest validation failed: {error}{error_path_str}")
|
|
2755
3179
|
|
|
2756
3180
|
return data.copy()
|
|
2757
3181
|
|
|
2758
3182
|
@staticmethod
|
|
2759
|
-
async def load_from_string(content: str, validate: bool = True) ->
|
|
3183
|
+
async def load_from_string(content: str, validate: bool = True) -> dict[str, Any]:
|
|
2760
3184
|
"""
|
|
2761
3185
|
Load and validate manifest from JSON string.
|
|
2762
3186
|
|
|
@@ -2778,8 +3202,8 @@ class ManifestParser:
|
|
|
2778
3202
|
|
|
2779
3203
|
@staticmethod
|
|
2780
3204
|
async def load_and_migrate(
|
|
2781
|
-
manifest:
|
|
2782
|
-
) ->
|
|
3205
|
+
manifest: dict[str, Any], target_version: str = CURRENT_SCHEMA_VERSION
|
|
3206
|
+
) -> dict[str, Any]:
|
|
2783
3207
|
"""
|
|
2784
3208
|
Load manifest and migrate to target version.
|
|
2785
3209
|
|