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.
Files changed (92) hide show
  1. mdb_engine/__init__.py +116 -11
  2. mdb_engine/auth/ARCHITECTURE.md +112 -0
  3. mdb_engine/auth/README.md +654 -11
  4. mdb_engine/auth/__init__.py +136 -29
  5. mdb_engine/auth/audit.py +592 -0
  6. mdb_engine/auth/base.py +252 -0
  7. mdb_engine/auth/casbin_factory.py +265 -70
  8. mdb_engine/auth/config_defaults.py +5 -5
  9. mdb_engine/auth/config_helpers.py +19 -18
  10. mdb_engine/auth/cookie_utils.py +12 -16
  11. mdb_engine/auth/csrf.py +483 -0
  12. mdb_engine/auth/decorators.py +10 -16
  13. mdb_engine/auth/dependencies.py +69 -71
  14. mdb_engine/auth/helpers.py +3 -3
  15. mdb_engine/auth/integration.py +61 -88
  16. mdb_engine/auth/jwt.py +11 -15
  17. mdb_engine/auth/middleware.py +79 -35
  18. mdb_engine/auth/oso_factory.py +21 -41
  19. mdb_engine/auth/provider.py +270 -171
  20. mdb_engine/auth/rate_limiter.py +505 -0
  21. mdb_engine/auth/restrictions.py +21 -36
  22. mdb_engine/auth/session_manager.py +24 -41
  23. mdb_engine/auth/shared_middleware.py +977 -0
  24. mdb_engine/auth/shared_users.py +775 -0
  25. mdb_engine/auth/token_lifecycle.py +10 -12
  26. mdb_engine/auth/token_store.py +17 -32
  27. mdb_engine/auth/users.py +99 -159
  28. mdb_engine/auth/utils.py +236 -42
  29. mdb_engine/cli/commands/generate.py +546 -10
  30. mdb_engine/cli/commands/validate.py +3 -7
  31. mdb_engine/cli/utils.py +7 -7
  32. mdb_engine/config.py +13 -28
  33. mdb_engine/constants.py +65 -0
  34. mdb_engine/core/README.md +117 -6
  35. mdb_engine/core/__init__.py +39 -7
  36. mdb_engine/core/app_registration.py +31 -50
  37. mdb_engine/core/app_secrets.py +289 -0
  38. mdb_engine/core/connection.py +20 -12
  39. mdb_engine/core/encryption.py +222 -0
  40. mdb_engine/core/engine.py +2862 -115
  41. mdb_engine/core/index_management.py +12 -16
  42. mdb_engine/core/manifest.py +628 -204
  43. mdb_engine/core/ray_integration.py +436 -0
  44. mdb_engine/core/seeding.py +13 -21
  45. mdb_engine/core/service_initialization.py +20 -30
  46. mdb_engine/core/types.py +40 -43
  47. mdb_engine/database/README.md +140 -17
  48. mdb_engine/database/__init__.py +17 -6
  49. mdb_engine/database/abstraction.py +37 -50
  50. mdb_engine/database/connection.py +51 -30
  51. mdb_engine/database/query_validator.py +367 -0
  52. mdb_engine/database/resource_limiter.py +204 -0
  53. mdb_engine/database/scoped_wrapper.py +747 -237
  54. mdb_engine/dependencies.py +427 -0
  55. mdb_engine/di/__init__.py +34 -0
  56. mdb_engine/di/container.py +247 -0
  57. mdb_engine/di/providers.py +206 -0
  58. mdb_engine/di/scopes.py +139 -0
  59. mdb_engine/embeddings/README.md +54 -24
  60. mdb_engine/embeddings/__init__.py +31 -24
  61. mdb_engine/embeddings/dependencies.py +38 -155
  62. mdb_engine/embeddings/service.py +78 -75
  63. mdb_engine/exceptions.py +104 -12
  64. mdb_engine/indexes/README.md +30 -13
  65. mdb_engine/indexes/__init__.py +1 -0
  66. mdb_engine/indexes/helpers.py +11 -11
  67. mdb_engine/indexes/manager.py +59 -123
  68. mdb_engine/memory/README.md +95 -4
  69. mdb_engine/memory/__init__.py +1 -2
  70. mdb_engine/memory/service.py +363 -1168
  71. mdb_engine/observability/README.md +4 -2
  72. mdb_engine/observability/__init__.py +26 -9
  73. mdb_engine/observability/health.py +17 -17
  74. mdb_engine/observability/logging.py +10 -10
  75. mdb_engine/observability/metrics.py +40 -19
  76. mdb_engine/repositories/__init__.py +34 -0
  77. mdb_engine/repositories/base.py +325 -0
  78. mdb_engine/repositories/mongo.py +233 -0
  79. mdb_engine/repositories/unit_of_work.py +166 -0
  80. mdb_engine/routing/README.md +1 -1
  81. mdb_engine/routing/__init__.py +1 -3
  82. mdb_engine/routing/websockets.py +41 -75
  83. mdb_engine/utils/__init__.py +3 -1
  84. mdb_engine/utils/mongo.py +117 -0
  85. mdb_engine-0.4.12.dist-info/METADATA +492 -0
  86. mdb_engine-0.4.12.dist-info/RECORD +97 -0
  87. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/WHEEL +1 -1
  88. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  89. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  90. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/entry_points.txt +0 -0
  91. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/licenses/LICENSE +0 -0
  92. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/top_level.txt +0 -0
@@ -32,21 +32,27 @@ For Scale:
32
32
  import asyncio
33
33
  import hashlib
34
34
  import logging
35
- from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
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 (CURRENT_SCHEMA_VERSION, DEFAULT_SCHEMA_VERSION,
40
- MAX_TTL_SECONDS, MAX_VECTOR_DIMENSIONS,
41
- MIN_TTL_SECONDS, MIN_VECTOR_DIMENSIONS)
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: Dict[str, Dict[str, Any]] = {}
52
+ SCHEMA_REGISTRY: dict[str, dict[str, Any]] = {}
47
53
 
48
54
  # Validation cache: maps (manifest_hash, version) -> validation_result
49
- _validation_cache: Dict[str, Tuple[bool, Optional[str], Optional[List[str]]]] = {}
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: Dict[str, Any]) -> str:
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
- "type": "object",
258
- "properties": {
259
- "role": {"type": "string"},
260
- "resource": {
261
- "type": "string",
262
- "default": "documents",
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
- "action": {"type": "string"},
265
- },
266
- "required": ["role", "action"],
267
- "additionalProperties": False,
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
- "in OSO Cloud on startup. Only used "
272
- "when provider is 'oso'. Example: "
273
- '[{"role": "admin", "resource": '
274
- '"documents", "action": "read"}]'
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: Dict[str, Any]) -> str:
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: Optional[str] = manifest_data.get("schema_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: Dict[str, Any], target_version: str = CURRENT_SCHEMA_VERSION
1766
- ) -> Dict[str, Any]:
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) -> Dict[str, Any]:
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: Dict[str, Any], use_cache: bool = True
1852
- ) -> Tuple[bool, Optional[str], Optional[List[str]]]:
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
- _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
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: List[Dict[str, Any]], use_cache: bool = True
1995
- ) -> List[Tuple[bool, Optional[str], Optional[List[str]], Optional[str]]]:
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: Dict[str, Any]
2010
- ) -> Tuple[bool, Optional[str], Optional[List[str]], Optional[str]]:
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: Optional[Callable[[str], Awaitable[bool]]] = None
2038
- ) -> Tuple[bool, Optional[str]]:
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: Dict[str, Any],
2529
+ manifest_data: dict[str, Any],
2085
2530
  db_validator: Callable[[str], Awaitable[bool]],
2086
2531
  use_cache: bool = True,
2087
- ) -> Tuple[bool, Optional[str], Optional[List[str]]]:
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: Dict[str, Any], use_cache: bool = True
2129
- ) -> Tuple[bool, Optional[str], Optional[List[str]]]:
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: Dict[str, Any], collection_name: str, index_name: str
2173
- ) -> Tuple[bool, Optional[str]]:
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: Dict[str, Any], collection_name: str, index_name: str
2212
- ) -> Tuple[bool, Optional[str]]:
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: Dict[str, Any], collection_name: str, index_name: str
2247
- ) -> Tuple[bool, Optional[str]]:
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: Dict[str, Any], collection_name: str, index_name: str
2268
- ) -> Tuple[bool, Optional[str]]:
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: Dict[str, Any], collection_name: str, index_name: str
2294
- ) -> Tuple[bool, Optional[str]]:
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: Dict[str, Any], collection_name: str, index_name: str, index_type: str
2323
- ) -> Tuple[bool, Optional[str]]:
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: Dict[str, Any], collection_name: str, index_name: str
2371
- ) -> Tuple[bool, Optional[str]]:
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: Dict[str, Any], collection_name: str, index_name: str
2435
- ) -> Tuple[bool, Optional[str]]:
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: Dict[str, List[Dict[str, Any]]]
2482
- ) -> Tuple[bool, Optional[str]]:
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: Dict[str, Any], use_cache: bool = True
2553
- ) -> Tuple[bool, Optional[str], Optional[List[str]]]:
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: Dict[str, Any], use_cache: bool = True
2569
- ) -> Tuple[bool, Optional[str], Optional[List[str]]]:
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: Dict[str, Any],
3020
+ manifest: dict[str, Any],
2589
3021
  db_validator: Callable[[str], Awaitable[bool]],
2590
3022
  use_cache: bool = True,
2591
- ) -> Tuple[bool, Optional[str], Optional[List[str]]]:
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: Dict[str, List[Dict[str, Any]]]
2610
- ) -> Tuple[bool, Optional[str]]:
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: Dict[str, Any], collection_name: str, index_name: str
2625
- ) -> Tuple[bool, Optional[str]]:
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: Dict[str, Any]) -> str:
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: Dict[str, Any], target_version: str = CURRENT_SCHEMA_VERSION
2655
- ) -> Dict[str, Any]:
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: Optional[ManifestValidator] = None):
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) -> Dict[str, Any]:
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) -> Dict[str, Any]:
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: Dict[str, Any], target_version: str = CURRENT_SCHEMA_VERSION
2782
- ) -> Dict[str, Any]:
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