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
mdb_engine/core/types.py CHANGED
@@ -8,8 +8,7 @@ throughout the codebase.
8
8
  This module is part of MDB_ENGINE - MongoDB Engine.
9
9
  """
10
10
 
11
- from typing import (TYPE_CHECKING, Any, Dict, List, Literal, Optional,
12
- TypedDict, Union)
11
+ from typing import TYPE_CHECKING, Any, Literal, TypedDict
13
12
 
14
13
  if TYPE_CHECKING:
15
14
  from ..memory import Mem0MemoryService
@@ -42,20 +41,20 @@ class IndexDefinitionDict(TypedDict, total=False):
42
41
  "partial",
43
42
  "hybrid",
44
43
  ]
45
- keys: Union[Dict[str, Union[int, str]], List[List[Union[str, int]]]]
44
+ keys: dict[str, int | str] | list[list[str | int]]
46
45
  unique: bool
47
46
  sparse: bool
48
47
  background: bool
49
48
  expireAfterSeconds: int
50
- partialFilterExpression: Dict[str, Any]
51
- weights: Dict[str, int] # For text indexes
49
+ partialFilterExpression: dict[str, Any]
50
+ weights: dict[str, int] # For text indexes
52
51
  default_language: str # For text indexes
53
52
  language_override: str # For text indexes
54
53
  textIndexVersion: int # For text indexes
55
54
  # Vector search specific
56
- fields: List[Dict[str, Any]] # For vectorSearch indexes
55
+ fields: list[dict[str, Any]] # For vectorSearch indexes
57
56
  # Search index specific
58
- mappings: Dict[str, Any] # For search indexes
57
+ mappings: dict[str, Any] # For search indexes
59
58
  # Geospatial specific
60
59
  bucketSize: float # For geoHaystack
61
60
  # TTL specific
@@ -80,12 +79,12 @@ class AuthAuthorizationDict(TypedDict, total=False):
80
79
  model: str
81
80
  policies_collection: str
82
81
  link_users_roles: bool
83
- default_roles: List[str]
82
+ default_roles: list[str]
84
83
  # OSO-specific
85
- api_key: Optional[str]
86
- url: Optional[str]
87
- initial_roles: List[Dict[str, str]]
88
- initial_policies: List[Dict[str, str]]
84
+ api_key: str | None
85
+ url: str | None
86
+ initial_roles: list[dict[str, str]]
87
+ initial_policies: list[dict[str, str]]
89
88
 
90
89
 
91
90
  class AuthPolicyDict(TypedDict, total=False):
@@ -94,12 +93,12 @@ class AuthPolicyDict(TypedDict, total=False):
94
93
  required: bool
95
94
  provider: Literal["casbin", "oso", "custom"]
96
95
  authorization: AuthAuthorizationDict
97
- allowed_roles: List[str]
98
- allowed_users: List[str]
99
- denied_users: List[str]
100
- required_permissions: List[str]
96
+ allowed_roles: list[str]
97
+ allowed_users: list[str]
98
+ denied_users: list[str]
99
+ required_permissions: list[str]
101
100
  custom_resource: str
102
- custom_actions: List[Literal["access", "read", "write", "admin"]]
101
+ custom_actions: list[Literal["access", "read", "write", "admin"]]
103
102
  allow_anonymous: bool
104
103
  owner_can_access: bool
105
104
 
@@ -112,7 +111,7 @@ class DemoUserDict(TypedDict, total=False):
112
111
  role: str
113
112
  auto_create: bool
114
113
  link_to_platform: bool
115
- extra_data: Dict[str, Any]
114
+ extra_data: dict[str, Any]
116
115
 
117
116
 
118
117
  class UsersConfigDict(TypedDict, total=False):
@@ -127,7 +126,7 @@ class UsersConfigDict(TypedDict, total=False):
127
126
  link_platform_users: bool
128
127
  anonymous_user_prefix: str
129
128
  user_id_field: str
130
- demo_users: List[DemoUserDict]
129
+ demo_users: list[DemoUserDict]
131
130
  auto_link_platform_demo: bool
132
131
  demo_user_seed_strategy: Literal["auto", "manual", "disabled"]
133
132
  enable_demo_user_access: bool
@@ -313,7 +312,7 @@ class MetricsConfigDict(TypedDict, total=False):
313
312
  enabled: bool
314
313
  collect_operation_metrics: bool
315
314
  collect_performance_metrics: bool
316
- custom_metrics: List[str]
315
+ custom_metrics: list[str]
317
316
 
318
317
 
319
318
  class LoggingConfigDict(TypedDict, total=False):
@@ -342,13 +341,11 @@ class CORSConfigDict(TypedDict, total=False):
342
341
  """CORS configuration."""
343
342
 
344
343
  enabled: bool
345
- allow_origins: List[str]
344
+ allow_origins: list[str]
346
345
  allow_credentials: bool
347
- allow_methods: List[
348
- Literal["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "*"]
349
- ]
350
- allow_headers: List[str]
351
- expose_headers: List[str]
346
+ allow_methods: list[Literal["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "*"]]
347
+ allow_headers: list[str]
348
+ expose_headers: list[str]
352
349
  max_age: int
353
350
 
354
351
 
@@ -374,22 +371,22 @@ class ManifestDict(TypedDict, total=False):
374
371
  schema_version: str
375
372
  slug: str
376
373
  name: str
377
- description: Optional[str]
374
+ description: str | None
378
375
  status: Literal["active", "draft", "archived", "inactive"]
379
376
  auth_required: bool # Backward compatibility
380
- auth: Optional[AuthConfigDict]
381
- token_management: Optional[TokenManagementDict]
382
- data_scope: List[str]
383
- pip_deps: List[str]
384
- managed_indexes: Optional[ManagedIndexesDict]
385
- collection_settings: Optional[Dict[str, Dict[str, Any]]]
386
- websockets: Optional[WebSocketsDict]
387
- embedding_config: Optional[EmbeddingConfigDict]
388
- memory_config: Optional[MemoryConfigDict]
389
- cors: Optional[CORSConfigDict]
390
- observability: Optional[ObservabilityConfigDict]
391
- initial_data: Optional[InitialDataDict]
392
- developer_id: Optional[str]
377
+ auth: AuthConfigDict | None
378
+ token_management: TokenManagementDict | None
379
+ data_scope: list[str]
380
+ pip_deps: list[str]
381
+ managed_indexes: ManagedIndexesDict | None
382
+ collection_settings: dict[str, dict[str, Any]] | None
383
+ websockets: WebSocketsDict | None
384
+ embedding_config: EmbeddingConfigDict | None
385
+ memory_config: MemoryConfigDict | None
386
+ cors: CORSConfigDict | None
387
+ observability: ObservabilityConfigDict | None
388
+ initial_data: InitialDataDict | None
389
+ developer_id: str | None
393
390
 
394
391
 
395
392
  # ============================================================================
@@ -401,13 +398,13 @@ class HealthStatusDict(TypedDict, total=False):
401
398
  """Health status response."""
402
399
 
403
400
  status: Literal["healthy", "degraded", "unhealthy"]
404
- checks: Dict[str, Dict[str, Any]]
401
+ checks: dict[str, dict[str, Any]]
405
402
  timestamp: str
406
403
 
407
404
 
408
405
  class MetricsDict(TypedDict, total=False):
409
406
  """Metrics response."""
410
407
 
411
- operations: Dict[str, Dict[str, Any]]
412
- summary: Dict[str, Any]
408
+ operations: dict[str, dict[str, Any]]
409
+ summary: dict[str, Any]
413
410
  timestamp: str
@@ -180,9 +180,9 @@ Async-native interface for managing Atlas Search and Vector indexes.
180
180
  ```python
181
181
  from mdb_engine.database import AsyncAtlasIndexManager
182
182
 
183
- # Get index manager from collection
183
+ # Get index manager from collection (automatically uses unscoped collection)
184
184
  collection = db.my_collection
185
- index_manager = AsyncAtlasIndexManager(collection._collection) # Use unscoped collection
185
+ index_manager = collection.index_manager # Secure way to access index manager
186
186
 
187
187
  # Create vector search index
188
188
  await index_manager.create_vector_search_index(
@@ -277,25 +277,121 @@ await index_manager.create_search_index(
277
277
  )
278
278
  ```
279
279
 
280
- ## Connection Pooling
280
+ ## Security Features
281
+
282
+ The database module includes comprehensive security controls to prevent unauthorized access, NoSQL injection, resource exhaustion, and ensure data isolation:
283
+
284
+ ### Query Security
285
+
286
+ All queries are automatically validated for security:
287
+ - **Dangerous operator blocking**: Blocks `$where`, `$eval`, `$function`, and `$accumulator` operators that allow JavaScript execution
288
+ - **Query depth limits**: Prevents deeply nested queries that could cause performance issues
289
+ - **Regex complexity limits**: Prevents ReDoS (Regular Expression Denial of Service) attacks
290
+ - **Pipeline validation**: Validates aggregation pipelines for safety and complexity
291
+
292
+ ```python
293
+ # These queries will be blocked:
294
+ db.collection.find({"$where": "this.status === 'active'"}) # ❌ Dangerous operator
295
+ db.collection.aggregate([{"$match": {}}] * 100) # ❌ Too many pipeline stages
281
296
 
282
- The database module provides shared MongoDB connection pooling for efficient resource usage.
297
+ # These queries are safe:
298
+ db.collection.find({"status": "active"}) # ✅ Safe
299
+ db.collection.find({"age": {"$gt": 18}}) # ✅ Safe
300
+ ```
301
+
302
+ ### Resource Limits
283
303
 
284
- ### Get Shared Client
304
+ All operations have automatic resource limits to prevent resource exhaustion:
305
+ - **Query timeouts**: All queries automatically have `maxTimeMS` set (default: 30 seconds, max: 5 minutes)
306
+ - **Result size limits**: Maximum 10,000 documents per query (configurable)
307
+ - **Batch size limits**: Maximum 1,000 documents per cursor batch
308
+ - **Document size validation**: Documents are validated before insert (16MB MongoDB limit)
285
309
 
286
310
  ```python
287
- from mdb_engine.database import get_shared_mongo_client
311
+ # Timeouts are automatically enforced:
312
+ db.collection.find({"status": "active"}) # Automatically has maxTimeMS=30000
313
+
314
+ # Result limits are enforced:
315
+ db.collection.find({}, limit=20000) # Automatically capped to 10,000
316
+
317
+ # Document sizes are validated:
318
+ large_doc = {"data": "x" * (20 * 1024 * 1024)} # ❌ Exceeds 16MB limit
319
+ await db.collection.insert_one(large_doc) # Raises ResourceLimitExceeded
320
+ ```
321
+
322
+ ### Collection Name Validation
288
323
 
289
- # Get or create shared MongoDB client
290
- client = get_shared_mongo_client(
291
- mongo_uri="mongodb://localhost:27017",
292
- max_pool_size=10,
293
- min_pool_size=1
324
+ All collection names are validated for security:
325
+
326
+ ### Collection Name Validation
327
+
328
+ All collection names are validated for security:
329
+ - **Format validation**: Must match MongoDB naming rules (alphanumeric, underscore, dot, hyphen)
330
+ - **Length limits**: 1-255 characters
331
+ - **Reserved names**: System collections (`apps_config`) are blocked
332
+ - **Reserved prefixes**: Collections starting with `system`, `admin`, `config`, or `local` are blocked
333
+ - **Path traversal protection**: Blocks attempts to use `..`, `/`, or `\` in collection names
334
+
335
+ ```python
336
+ # These will raise ValueError:
337
+ db.system_users # Reserved prefix
338
+ db.apps_config # Reserved name
339
+ db["../other"] # Path traversal attempt
340
+ db["123invalid"] # Invalid format (starts with number)
341
+ ```
342
+
343
+ ### Cross-App Access Control
344
+
345
+ Cross-app collection access is strictly controlled:
346
+ - Apps can only read from collections of apps listed in their `read_scopes`
347
+ - Unauthorized cross-app access attempts are logged and blocked
348
+ - All cross-app access is logged for audit purposes
349
+
350
+ ```python
351
+ # App can only read from authorized apps
352
+ db = engine.get_scoped_db(
353
+ "my_app",
354
+ read_scopes=["my_app", "shared_app"] # Can read from these apps
294
355
  )
295
356
 
296
- db = client["my_database"]
357
+ # This works (authorized):
358
+ collection = db.get_collection("shared_app_data")
359
+
360
+ # This fails (unauthorized):
361
+ collection = db.get_collection("other_app_data") # Raises ValueError
297
362
  ```
298
363
 
364
+ ### Scope Validation
365
+
366
+ The `get_scoped_db()` method validates all scopes:
367
+ - `read_scopes` must be a non-empty list of valid app slugs
368
+ - `write_scope` must be a non-empty string
369
+ - Invalid scopes raise `ValueError` with clear error messages
370
+
371
+ ### Audit Logging
372
+
373
+ All security-relevant events are logged:
374
+ - Invalid collection name attempts
375
+ - Unauthorized cross-app access attempts
376
+ - Reserved name/prefix access attempts
377
+ - Collection name validation failures
378
+
379
+ Logs include app context (app_slug, collection_name, action) for security monitoring.
380
+
381
+ ### Best Practices
382
+
383
+ 1. **Always use scoped databases**: Never access raw MongoDB clients or databases
384
+ 2. **Validate collection names**: Use descriptive, valid collection names
385
+ 3. **Limit cross-app access**: Only grant `read_scopes` to apps that need cross-app data
386
+ 4. **Monitor audit logs**: Review security logs regularly for suspicious patterns
387
+ 5. **Follow naming conventions**: Use lowercase, underscore-separated names (e.g., `user_profiles`)
388
+
389
+ ## Connection Pooling
390
+
391
+ The database module provides shared MongoDB connection pooling for efficient resource usage. Connection pooling is handled automatically by the engine - users should always use `engine.get_scoped_db()` for database access.
392
+
393
+ **Security Note:** Direct MongoDB client creation functions are internal and not part of the public API. Always use scoped databases to ensure proper app isolation.
394
+
299
395
  ### Pool Metrics
300
396
 
301
397
  Monitor connection pool usage:
@@ -474,13 +570,40 @@ except OperationFailure as e:
474
570
  print(f"MongoDB operation failed: {e.details}")
475
571
  except AutoReconnect as e:
476
572
  print(f"MongoDB reconnection: {e}")
477
- except Exception as e:
478
- print(f"Unexpected error: {e}")
573
+ except (ConnectionFailure, ServerSelectionTimeoutError) as e:
574
+ print(f"Connection error: {e}")
479
575
  ```
480
576
 
481
577
  ## Integration Examples
482
578
 
483
- ### FastAPI Integration
579
+ ### FastAPI Integration (Recommended)
580
+
581
+ Use the request-scoped `get_scoped_db` dependency from `mdb_engine.dependencies`:
582
+
583
+ ```python
584
+ from fastapi import Depends
585
+ from mdb_engine import MongoDBEngine
586
+ from mdb_engine.dependencies import get_scoped_db
587
+
588
+ engine = MongoDBEngine(mongo_uri="...", db_name="...")
589
+ app = engine.create_app(slug="my_app", manifest=Path("manifest.json"))
590
+
591
+ @app.get("/data")
592
+ async def get_data(db=Depends(get_scoped_db)):
593
+ # db is automatically scoped to "my_app"
594
+ docs = await db.my_collection.find({}).to_list(length=10)
595
+ return {"data": docs}
596
+
597
+ @app.post("/data")
598
+ async def create_data(db=Depends(get_scoped_db)):
599
+ # Writes are automatically scoped to "my_app"
600
+ result = await db.my_collection.insert_one({"name": "New Document"})
601
+ return {"inserted_id": str(result.inserted_id)}
602
+ ```
603
+
604
+ ### Legacy FastAPI Integration
605
+
606
+ For apps not using `engine.create_app()`:
484
607
 
485
608
  ```python
486
609
  from fastapi import FastAPI, Depends
@@ -508,8 +631,8 @@ db1 = engine.get_scoped_db("app1")
508
631
  db2 = engine.get_scoped_db("app2")
509
632
 
510
633
  # Cross-app read (read from app1, write to app2)
511
- shared_db = ScopedMongoWrapper(
512
- real_db=engine.mongo_db,
634
+ shared_db = engine.get_scoped_db(
635
+ app_slug="shared",
513
636
  read_scopes=["app1", "app2"],
514
637
  write_scope="shared"
515
638
  )
@@ -6,11 +6,20 @@ and MongoDB-style API for familiarity.
6
6
  """
7
7
 
8
8
  from .abstraction import AppDB, Collection, get_app_db
9
- from .connection import (close_shared_client, get_pool_metrics,
10
- get_shared_mongo_client, register_client_for_metrics,
11
- verify_shared_client)
12
- from .scoped_wrapper import (AsyncAtlasIndexManager, AutoIndexManager,
13
- ScopedCollectionWrapper, ScopedMongoWrapper)
9
+ from .connection import (
10
+ close_shared_client,
11
+ get_pool_metrics,
12
+ register_client_for_metrics,
13
+ verify_shared_client,
14
+ )
15
+ from .query_validator import QueryValidator
16
+ from .resource_limiter import ResourceLimiter
17
+ from .scoped_wrapper import (
18
+ AsyncAtlasIndexManager,
19
+ AutoIndexManager,
20
+ ScopedCollectionWrapper,
21
+ ScopedMongoWrapper,
22
+ )
14
23
 
15
24
  __all__ = [
16
25
  # Scoped wrappers
@@ -18,12 +27,14 @@ __all__ = [
18
27
  "ScopedCollectionWrapper",
19
28
  "AsyncAtlasIndexManager",
20
29
  "AutoIndexManager",
30
+ # Query security
31
+ "QueryValidator",
32
+ "ResourceLimiter",
21
33
  # Database abstraction
22
34
  "AppDB",
23
35
  "Collection",
24
36
  "get_app_db",
25
37
  # Connection pooling
26
- "get_shared_mongo_client",
27
38
  "verify_shared_client",
28
39
  "get_pool_metrics",
29
40
  "register_client_for_metrics",
@@ -24,15 +24,20 @@ Usage:
24
24
  """
25
25
 
26
26
  import logging
27
- from typing import Any, Callable, Dict, List, Optional
27
+ from collections.abc import Callable
28
+ from typing import Any
28
29
 
29
30
  from ..exceptions import MongoDBEngineError
30
31
  from .scoped_wrapper import ScopedMongoWrapper
31
32
 
32
33
  try:
33
- from pymongo.errors import (AutoReconnect, ConnectionFailure,
34
- InvalidOperation, OperationFailure,
35
- ServerSelectionTimeoutError)
34
+ from pymongo.errors import (
35
+ AutoReconnect,
36
+ ConnectionFailure,
37
+ InvalidOperation,
38
+ OperationFailure,
39
+ ServerSelectionTimeoutError,
40
+ )
36
41
  except ImportError:
37
42
  OperationFailure = Exception
38
43
  AutoReconnect = Exception
@@ -41,8 +46,12 @@ except ImportError:
41
46
 
42
47
  try:
43
48
  from motor.motor_asyncio import AsyncIOMotorCursor
44
- from pymongo.results import (DeleteResult, InsertManyResult,
45
- InsertOneResult, UpdateResult)
49
+ from pymongo.results import (
50
+ DeleteResult,
51
+ InsertManyResult,
52
+ InsertOneResult,
53
+ UpdateResult,
54
+ )
46
55
  except ImportError:
47
56
  AsyncIOMotorCursor = None
48
57
  InsertOneResult = None
@@ -80,8 +89,8 @@ class Collection:
80
89
  self._collection = scoped_collection
81
90
 
82
91
  async def find_one(
83
- self, filter: Optional[Dict[str, Any]] = None, *args, **kwargs
84
- ) -> Optional[Dict[str, Any]]:
92
+ self, filter: dict[str, Any] | None = None, *args, **kwargs
93
+ ) -> dict[str, Any] | None:
85
94
  """
86
95
  Find a single document matching the filter.
87
96
 
@@ -112,9 +121,7 @@ class Collection:
112
121
  context={"operation": "find_one"},
113
122
  ) from e
114
123
 
115
- def find(
116
- self, filter: Optional[Dict[str, Any]] = None, *args, **kwargs
117
- ) -> AsyncIOMotorCursor:
124
+ def find(self, filter: dict[str, Any] | None = None, *args, **kwargs) -> AsyncIOMotorCursor:
118
125
  """
119
126
  Find documents matching the filter.
120
127
 
@@ -140,9 +147,7 @@ class Collection:
140
147
  """
141
148
  return self._collection.find(filter or {}, *args, **kwargs)
142
149
 
143
- async def insert_one(
144
- self, document: Dict[str, Any], *args, **kwargs
145
- ) -> InsertOneResult:
150
+ async def insert_one(self, document: dict[str, Any], *args, **kwargs) -> InsertOneResult:
146
151
  """
147
152
  Insert a single document.
148
153
 
@@ -174,7 +179,7 @@ class Collection:
174
179
  ) from e
175
180
 
176
181
  async def insert_many(
177
- self, documents: List[Dict[str, Any]], *args, **kwargs
182
+ self, documents: list[dict[str, Any]], *args, **kwargs
178
183
  ) -> InsertManyResult:
179
184
  """
180
185
  Insert multiple documents at once.
@@ -209,7 +214,7 @@ class Collection:
209
214
  ) from e
210
215
 
211
216
  async def update_one(
212
- self, filter: Dict[str, Any], update: Dict[str, Any], *args, **kwargs
217
+ self, filter: dict[str, Any], update: dict[str, Any], *args, **kwargs
213
218
  ) -> UpdateResult:
214
219
  """
215
220
  Update a single document matching the filter.
@@ -247,7 +252,7 @@ class Collection:
247
252
  ) from e
248
253
 
249
254
  async def update_many(
250
- self, filter: Dict[str, Any], update: Dict[str, Any], *args, **kwargs
255
+ self, filter: dict[str, Any], update: dict[str, Any], *args, **kwargs
251
256
  ) -> UpdateResult:
252
257
  """
253
258
  Update multiple documents matching the filter.
@@ -284,7 +289,7 @@ class Collection:
284
289
  ) from e
285
290
 
286
291
  async def replace_one(
287
- self, filter: Dict[str, Any], replacement: Dict[str, Any], *args, **kwargs
292
+ self, filter: dict[str, Any], replacement: dict[str, Any], *args, **kwargs
288
293
  ) -> UpdateResult:
289
294
  """
290
295
  Replace a single document matching the filter.
@@ -363,7 +368,7 @@ class Collection:
363
368
  context={"operation": "replace_one"},
364
369
  ) from e
365
370
 
366
- async def delete_one(self, filter: Dict[str, Any], *args, **kwargs) -> DeleteResult:
371
+ async def delete_one(self, filter: dict[str, Any], *args, **kwargs) -> DeleteResult:
367
372
  """
368
373
  Delete a single document matching the filter.
369
374
 
@@ -396,7 +401,7 @@ class Collection:
396
401
  ) from e
397
402
 
398
403
  async def delete_many(
399
- self, filter: Optional[Dict[str, Any]] = None, *args, **kwargs
404
+ self, filter: dict[str, Any] | None = None, *args, **kwargs
400
405
  ) -> DeleteResult:
401
406
  """
402
407
  Delete multiple documents matching the filter.
@@ -428,9 +433,7 @@ class Collection:
428
433
  context={"operation": "delete_many"},
429
434
  ) from e
430
435
 
431
- async def count_documents(
432
- self, filter: Optional[Dict[str, Any]] = None, *args, **kwargs
433
- ) -> int:
436
+ async def count_documents(self, filter: dict[str, Any] | None = None, *args, **kwargs) -> int:
434
437
  """
435
438
  Count documents matching the filter.
436
439
 
@@ -460,9 +463,7 @@ class Collection:
460
463
  context={"operation": "count_documents"},
461
464
  ) from e
462
465
 
463
- def aggregate(
464
- self, pipeline: List[Dict[str, Any]], *args, **kwargs
465
- ) -> AsyncIOMotorCursor:
466
+ def aggregate(self, pipeline: list[dict[str, Any]], *args, **kwargs) -> AsyncIOMotorCursor:
466
467
  """
467
468
  Perform aggregation pipeline.
468
469
 
@@ -518,7 +519,7 @@ class AppDB:
518
519
  raise RuntimeError("ScopedMongoWrapper is not available. Check imports.")
519
520
 
520
521
  self._wrapper = scoped_wrapper
521
- self._collection_cache: Dict[str, Collection] = {}
522
+ self._collection_cache: dict[str, Collection] = {}
522
523
 
523
524
  def collection(self, name: str) -> Collection:
524
525
  """
@@ -554,11 +555,17 @@ class AppDB:
554
555
  Example:
555
556
  db.users.get("user_123") # Instead of db.collection("users").get("user_123")
556
557
  """
557
- # Only proxy collection names, not internal attributes
558
- if name.startswith("_"):
558
+ # Explicitly block access to 'database' property (removed for security)
559
+ if name == "database":
559
560
  raise AttributeError(
560
- f"'{type(self).__name__}' object has no attribute '{name}'"
561
+ "'database' property has been removed for security. "
562
+ "Use collection.index_manager for index operations. "
563
+ "All data access must go through scoped collections."
561
564
  )
565
+
566
+ # Only proxy collection names, not internal attributes
567
+ if name.startswith("_"):
568
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
562
569
  return self.collection(name)
563
570
 
564
571
  @property
@@ -576,26 +583,6 @@ class AppDB:
576
583
  """
577
584
  return self._wrapper
578
585
 
579
- @property
580
- def database(self):
581
- """
582
- Access the underlying AsyncIOMotorDatabase (unscoped).
583
-
584
- This is useful for advanced operations that need direct access to the
585
- real database without scoping, such as index management or administrative
586
- operations.
587
-
588
- Returns:
589
- The underlying AsyncIOMotorDatabase instance
590
-
591
- Example:
592
- # Access underlying database for index management
593
- real_db = db.database
594
- collection = real_db["my_collection"]
595
- index_manager = AsyncAtlasIndexManager(collection)
596
- """
597
- return self._wrapper.database
598
-
599
586
 
600
587
  # FastAPI dependency helper
601
588
  async def get_app_db(request, get_scoped_db_func: Callable) -> AppDB: