mdb-engine 0.1.6__py3-none-any.whl → 0.2.0__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 +104 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +648 -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 +264 -69
- mdb_engine/auth/config_helpers.py +7 -6
- mdb_engine/auth/cookie_utils.py +3 -7
- mdb_engine/auth/csrf.py +373 -0
- mdb_engine/auth/decorators.py +3 -10
- mdb_engine/auth/dependencies.py +47 -50
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +53 -80
- mdb_engine/auth/jwt.py +2 -6
- mdb_engine/auth/middleware.py +77 -34
- mdb_engine/auth/oso_factory.py +18 -38
- mdb_engine/auth/provider.py +270 -171
- mdb_engine/auth/rate_limiter.py +504 -0
- mdb_engine/auth/restrictions.py +8 -24
- mdb_engine/auth/session_manager.py +14 -29
- mdb_engine/auth/shared_middleware.py +600 -0
- mdb_engine/auth/shared_users.py +759 -0
- mdb_engine/auth/token_store.py +14 -28
- mdb_engine/auth/users.py +54 -113
- mdb_engine/auth/utils.py +213 -15
- mdb_engine/cli/commands/generate.py +545 -9
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +3 -3
- mdb_engine/config.py +7 -21
- 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 +22 -41
- mdb_engine/core/app_secrets.py +290 -0
- mdb_engine/core/connection.py +18 -9
- mdb_engine/core/encryption.py +223 -0
- mdb_engine/core/engine.py +1057 -93
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +459 -150
- mdb_engine/core/ray_integration.py +435 -0
- mdb_engine/core/seeding.py +10 -18
- mdb_engine/core/service_initialization.py +12 -23
- mdb_engine/core/types.py +2 -5
- mdb_engine/database/README.md +140 -17
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +25 -37
- mdb_engine/database/connection.py +11 -18
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +713 -196
- mdb_engine/dependencies.py +426 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +248 -0
- mdb_engine/di/providers.py +205 -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 +37 -154
- mdb_engine/embeddings/service.py +11 -25
- mdb_engine/exceptions.py +92 -0
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +1 -1
- mdb_engine/indexes/manager.py +50 -114
- mdb_engine/memory/README.md +2 -2
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +30 -87
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +8 -9
- mdb_engine/observability/metrics.py +32 -12
- 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 +25 -60
- mdb_engine-0.2.0.dist-info/METADATA +313 -0
- mdb_engine-0.2.0.dist-info/RECORD +96 -0
- 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.2.0.dist-info}/WHEEL +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/top_level.txt +0 -0
mdb_engine/memory/service.py
CHANGED
|
@@ -48,9 +48,7 @@ def _check_mem0_available():
|
|
|
48
48
|
MEM0_AVAILABLE = False
|
|
49
49
|
Memory = None
|
|
50
50
|
except OSError as e:
|
|
51
|
-
logger.warning(
|
|
52
|
-
f"Failed to set up mem0 directory: {e}. Memory features may be limited."
|
|
53
|
-
)
|
|
51
|
+
logger.warning(f"Failed to set up mem0 directory: {e}. Memory features may be limited.")
|
|
54
52
|
MEM0_AVAILABLE = False
|
|
55
53
|
Memory = None
|
|
56
54
|
|
|
@@ -140,9 +138,7 @@ def _build_vector_store_config(
|
|
|
140
138
|
}
|
|
141
139
|
|
|
142
140
|
|
|
143
|
-
def _build_embedder_config(
|
|
144
|
-
provider: str, embedding_model: str, app_slug: str
|
|
145
|
-
) -> Dict[str, Any]:
|
|
141
|
+
def _build_embedder_config(provider: str, embedding_model: str, app_slug: str) -> Dict[str, Any]:
|
|
146
142
|
"""Build embedder configuration for mem0."""
|
|
147
143
|
clean_embedding_model = embedding_model.replace("azure/", "").replace("openai/", "")
|
|
148
144
|
if provider == "azure":
|
|
@@ -256,14 +252,10 @@ def _initialize_memory_instance(mem0_config: Dict[str, Any], app_slug: str) -> t
|
|
|
256
252
|
extra={
|
|
257
253
|
"app_slug": app_slug,
|
|
258
254
|
"config_keys": list(mem0_config.keys()),
|
|
259
|
-
"vector_store_provider": mem0_config.get("vector_store", {}).get(
|
|
260
|
-
"provider"
|
|
261
|
-
),
|
|
255
|
+
"vector_store_provider": mem0_config.get("vector_store", {}).get("provider"),
|
|
262
256
|
"embedder_provider": mem0_config.get("embedder", {}).get("provider"),
|
|
263
257
|
"llm_provider": (
|
|
264
|
-
mem0_config.get("llm", {}).get("provider")
|
|
265
|
-
if mem0_config.get("llm")
|
|
266
|
-
else None
|
|
258
|
+
mem0_config.get("llm", {}).get("provider") if mem0_config.get("llm") else None
|
|
267
259
|
),
|
|
268
260
|
"full_config": mem0_config,
|
|
269
261
|
},
|
|
@@ -305,9 +297,7 @@ def _initialize_memory_instance(mem0_config: Dict[str, Any], app_slug: str) -> t
|
|
|
305
297
|
"error": error_msg,
|
|
306
298
|
"error_type": type(init_error).__name__,
|
|
307
299
|
"config_keys": (
|
|
308
|
-
list(mem0_config.keys())
|
|
309
|
-
if isinstance(mem0_config, dict)
|
|
310
|
-
else "not_dict"
|
|
300
|
+
list(mem0_config.keys()) if isinstance(mem0_config, dict) else "not_dict"
|
|
311
301
|
),
|
|
312
302
|
},
|
|
313
303
|
)
|
|
@@ -372,9 +362,7 @@ class Mem0MemoryService:
|
|
|
372
362
|
self.app_slug = app_slug
|
|
373
363
|
|
|
374
364
|
# Extract config with defaults
|
|
375
|
-
self.collection_name = (config or {}).get(
|
|
376
|
-
"collection_name", f"{app_slug}_memories"
|
|
377
|
-
)
|
|
365
|
+
self.collection_name = (config or {}).get("collection_name", f"{app_slug}_memories")
|
|
378
366
|
config_embedding_dims = (config or {}).get(
|
|
379
367
|
"embedding_model_dims"
|
|
380
368
|
) # Optional - will be auto-detected
|
|
@@ -392,18 +380,14 @@ class Mem0MemoryService:
|
|
|
392
380
|
or os.getenv("CHAT_MODEL")
|
|
393
381
|
or os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o")
|
|
394
382
|
)
|
|
395
|
-
temperature = (config or {}).get(
|
|
396
|
-
"temperature", float(os.getenv("LLM_TEMPERATURE", "0.0"))
|
|
397
|
-
)
|
|
383
|
+
temperature = (config or {}).get("temperature", float(os.getenv("LLM_TEMPERATURE", "0.0")))
|
|
398
384
|
|
|
399
385
|
# Detect provider from environment variables
|
|
400
386
|
provider = _detect_provider_from_env()
|
|
401
387
|
|
|
402
388
|
# Verify required environment variables are set
|
|
403
389
|
if provider == "azure":
|
|
404
|
-
if not os.getenv("AZURE_OPENAI_API_KEY") or not os.getenv(
|
|
405
|
-
"AZURE_OPENAI_ENDPOINT"
|
|
406
|
-
):
|
|
390
|
+
if not os.getenv("AZURE_OPENAI_API_KEY") or not os.getenv("AZURE_OPENAI_ENDPOINT"):
|
|
407
391
|
raise Mem0MemoryServiceError(
|
|
408
392
|
"Azure OpenAI provider requires AZURE_OPENAI_API_KEY and "
|
|
409
393
|
"AZURE_OPENAI_ENDPOINT environment variables to be set."
|
|
@@ -418,9 +402,7 @@ class Mem0MemoryService:
|
|
|
418
402
|
# Detect embedding dimensions using model name (fallback method)
|
|
419
403
|
detected_dims = _detect_embedding_dimensions(embedding_model)
|
|
420
404
|
self.embedding_model_dims = (
|
|
421
|
-
detected_dims
|
|
422
|
-
if detected_dims is not None
|
|
423
|
-
else (config_embedding_dims or 1536)
|
|
405
|
+
detected_dims if detected_dims is not None else (config_embedding_dims or 1536)
|
|
424
406
|
)
|
|
425
407
|
|
|
426
408
|
# Build mem0 config with MongoDB as vector store
|
|
@@ -432,17 +414,13 @@ class Mem0MemoryService:
|
|
|
432
414
|
)
|
|
433
415
|
|
|
434
416
|
# Configure mem0 embedder
|
|
435
|
-
mem0_config["embedder"] = _build_embedder_config(
|
|
436
|
-
provider, embedding_model, app_slug
|
|
437
|
-
)
|
|
417
|
+
mem0_config["embedder"] = _build_embedder_config(provider, embedding_model, app_slug)
|
|
438
418
|
|
|
439
419
|
# Configure LLM for inference (if infer: true)
|
|
440
420
|
if self.infer:
|
|
441
|
-
mem0_config["llm"] = _build_llm_config(
|
|
442
|
-
provider, chat_model, temperature, app_slug
|
|
443
|
-
)
|
|
421
|
+
mem0_config["llm"] = _build_llm_config(provider, chat_model, temperature, app_slug)
|
|
444
422
|
except (ValueError, TypeError, KeyError, AttributeError, ImportError) as e:
|
|
445
|
-
logger.
|
|
423
|
+
logger.exception(
|
|
446
424
|
f"Failed to configure mem0: {e}",
|
|
447
425
|
extra={"app_slug": app_slug, "error": str(e)},
|
|
448
426
|
)
|
|
@@ -464,9 +442,7 @@ class Mem0MemoryService:
|
|
|
464
442
|
|
|
465
443
|
try:
|
|
466
444
|
# Initialize Mem0 Memory instance
|
|
467
|
-
self.memory, init_method = _initialize_memory_instance(
|
|
468
|
-
mem0_config, app_slug
|
|
469
|
-
)
|
|
445
|
+
self.memory, init_method = _initialize_memory_instance(mem0_config, app_slug)
|
|
470
446
|
|
|
471
447
|
# Verify the memory instance has required methods
|
|
472
448
|
if not hasattr(self.memory, "get_all"):
|
|
@@ -491,16 +467,12 @@ class Mem0MemoryService:
|
|
|
491
467
|
"infer": self.infer,
|
|
492
468
|
"has_get_all": hasattr(self.memory, "get_all"),
|
|
493
469
|
"has_add": hasattr(self.memory, "add"),
|
|
494
|
-
"embedder_provider": mem0_config.get("embedder", {}).get(
|
|
495
|
-
"provider"
|
|
496
|
-
),
|
|
470
|
+
"embedder_provider": mem0_config.get("embedder", {}).get("provider"),
|
|
497
471
|
"embedder_model": mem0_config.get("embedder", {})
|
|
498
472
|
.get("config", {})
|
|
499
473
|
.get("model"),
|
|
500
474
|
"llm_provider": (
|
|
501
|
-
mem0_config.get("llm", {}).get("provider")
|
|
502
|
-
if self.infer
|
|
503
|
-
else None
|
|
475
|
+
mem0_config.get("llm", {}).get("provider") if self.infer else None
|
|
504
476
|
),
|
|
505
477
|
"llm_model": (
|
|
506
478
|
mem0_config.get("llm", {}).get("config", {}).get("model")
|
|
@@ -522,9 +494,7 @@ class Mem0MemoryService:
|
|
|
522
494
|
exc_info=True,
|
|
523
495
|
extra={"app_slug": app_slug, "error": str(e)},
|
|
524
496
|
)
|
|
525
|
-
raise Mem0MemoryServiceError(
|
|
526
|
-
f"Failed to initialize Mem0 Memory Service: {e}"
|
|
527
|
-
) from e
|
|
497
|
+
raise Mem0MemoryServiceError(f"Failed to initialize Mem0 Memory Service: {e}") from e
|
|
528
498
|
|
|
529
499
|
def add(
|
|
530
500
|
self,
|
|
@@ -628,8 +598,7 @@ class Mem0MemoryService:
|
|
|
628
598
|
|
|
629
599
|
result_length = len(result) if isinstance(result, list) else 0
|
|
630
600
|
logger.debug(
|
|
631
|
-
f"Raw result from mem0.add(): type={type(result)}, "
|
|
632
|
-
f"length={result_length}",
|
|
601
|
+
f"Raw result from mem0.add(): type={type(result)}, " f"length={result_length}",
|
|
633
602
|
extra={
|
|
634
603
|
"app_slug": self.app_slug,
|
|
635
604
|
"user_id": user_id,
|
|
@@ -652,11 +621,7 @@ class Mem0MemoryService:
|
|
|
652
621
|
"message_count": len(messages),
|
|
653
622
|
"memory_count": len(result) if isinstance(result, list) else 0,
|
|
654
623
|
"memory_ids": (
|
|
655
|
-
[
|
|
656
|
-
m.get("id") or m.get("_id")
|
|
657
|
-
for m in result
|
|
658
|
-
if isinstance(m, dict)
|
|
659
|
-
]
|
|
624
|
+
[m.get("id") or m.get("_id") for m in result if isinstance(m, dict)]
|
|
660
625
|
if result
|
|
661
626
|
else []
|
|
662
627
|
),
|
|
@@ -790,9 +755,7 @@ class Mem0MemoryService:
|
|
|
790
755
|
result = self.memory.get_all(
|
|
791
756
|
user_id=str(user_id), limit=limit, **kwargs
|
|
792
757
|
) # Ensure string
|
|
793
|
-
result_length = (
|
|
794
|
-
len(result) if isinstance(result, (list, dict)) else "N/A"
|
|
795
|
-
)
|
|
758
|
+
result_length = len(result) if isinstance(result, (list, dict)) else "N/A"
|
|
796
759
|
logger.debug(
|
|
797
760
|
f"🟢 RESULT RECEIVED: type={type(result).__name__}, "
|
|
798
761
|
f"length={result_length}",
|
|
@@ -807,7 +770,7 @@ class Mem0MemoryService:
|
|
|
807
770
|
},
|
|
808
771
|
)
|
|
809
772
|
except AttributeError as attr_error:
|
|
810
|
-
logger.
|
|
773
|
+
logger.exception(
|
|
811
774
|
f"Memory.get_all method not available: {attr_error}",
|
|
812
775
|
extra={
|
|
813
776
|
"app_slug": self.app_slug,
|
|
@@ -828,9 +791,7 @@ class Mem0MemoryService:
|
|
|
828
791
|
"result_type": str(type(result)),
|
|
829
792
|
"is_dict": isinstance(result, dict),
|
|
830
793
|
"is_list": isinstance(result, list),
|
|
831
|
-
"result_length": (
|
|
832
|
-
len(result) if isinstance(result, (list, dict)) else 0
|
|
833
|
-
),
|
|
794
|
+
"result_length": (len(result) if isinstance(result, (list, dict)) else 0),
|
|
834
795
|
},
|
|
835
796
|
)
|
|
836
797
|
|
|
@@ -843,16 +804,12 @@ class Mem0MemoryService:
|
|
|
843
804
|
extra={
|
|
844
805
|
"app_slug": self.app_slug,
|
|
845
806
|
"user_id": user_id,
|
|
846
|
-
"result_count": (
|
|
847
|
-
len(result) if isinstance(result, list) else 0
|
|
848
|
-
),
|
|
807
|
+
"result_count": (len(result) if isinstance(result, list) else 0),
|
|
849
808
|
},
|
|
850
809
|
)
|
|
851
810
|
elif "data" in result:
|
|
852
811
|
# Alternative format: {"data": [...]}
|
|
853
|
-
result = (
|
|
854
|
-
result["data"] if isinstance(result["data"], list) else []
|
|
855
|
-
)
|
|
812
|
+
result = result["data"] if isinstance(result["data"], list) else []
|
|
856
813
|
|
|
857
814
|
# Ensure result is always a list for backward compatibility
|
|
858
815
|
if not isinstance(result, list):
|
|
@@ -883,9 +840,7 @@ class Mem0MemoryService:
|
|
|
883
840
|
return result
|
|
884
841
|
|
|
885
842
|
except (AttributeError, TypeError, ValueError, RuntimeError, KeyError) as e:
|
|
886
|
-
attempt_num = (
|
|
887
|
-
attempt + 1 if "attempt" in locals() and attempt is not None else 1
|
|
888
|
-
)
|
|
843
|
+
attempt_num = attempt + 1 if "attempt" in locals() and attempt is not None else 1
|
|
889
844
|
logger.error(
|
|
890
845
|
f"Failed to get memories: {e}",
|
|
891
846
|
exc_info=True,
|
|
@@ -961,9 +916,7 @@ class Mem0MemoryService:
|
|
|
961
916
|
|
|
962
917
|
# Call search - try with filters first, fallback to metadata if needed
|
|
963
918
|
try:
|
|
964
|
-
result = self.memory.search(
|
|
965
|
-
query=query, user_id=user_id, **search_kwargs
|
|
966
|
-
)
|
|
919
|
+
result = self.memory.search(query=query, user_id=user_id, **search_kwargs)
|
|
967
920
|
except (TypeError, ValueError) as e:
|
|
968
921
|
# If filters parameter doesn't work, try with metadata (backward compatibility)
|
|
969
922
|
if "filters" in search_kwargs and metadata:
|
|
@@ -973,9 +926,7 @@ class Mem0MemoryService:
|
|
|
973
926
|
)
|
|
974
927
|
search_kwargs.pop("filters", None)
|
|
975
928
|
search_kwargs["metadata"] = metadata
|
|
976
|
-
result = self.memory.search(
|
|
977
|
-
query=query, user_id=user_id, **search_kwargs
|
|
978
|
-
)
|
|
929
|
+
result = self.memory.search(query=query, user_id=user_id, **search_kwargs)
|
|
979
930
|
else:
|
|
980
931
|
raise
|
|
981
932
|
|
|
@@ -1019,9 +970,7 @@ class Mem0MemoryService:
|
|
|
1019
970
|
)
|
|
1020
971
|
raise Mem0MemoryServiceError(f"Failed to search memories: {e}") from e
|
|
1021
972
|
|
|
1022
|
-
def get(
|
|
1023
|
-
self, memory_id: str, user_id: Optional[str] = None, **kwargs
|
|
1024
|
-
) -> Dict[str, Any]:
|
|
973
|
+
def get(self, memory_id: str, user_id: Optional[str] = None, **kwargs) -> Dict[str, Any]:
|
|
1025
974
|
"""
|
|
1026
975
|
Get a single memory by ID.
|
|
1027
976
|
|
|
@@ -1047,9 +996,7 @@ class Mem0MemoryService:
|
|
|
1047
996
|
# If user_id is provided, verify the memory belongs to that user
|
|
1048
997
|
# by checking metadata or user_id field in the result
|
|
1049
998
|
if user_id and isinstance(result, dict):
|
|
1050
|
-
result_user_id = result.get("user_id") or result.get(
|
|
1051
|
-
"metadata", {}
|
|
1052
|
-
).get("user_id")
|
|
999
|
+
result_user_id = result.get("user_id") or result.get("metadata", {}).get("user_id")
|
|
1053
1000
|
if result_user_id and result_user_id != user_id:
|
|
1054
1001
|
logger.warning(
|
|
1055
1002
|
f"Memory {memory_id} does not belong to user {user_id}",
|
|
@@ -1189,9 +1136,7 @@ class Mem0MemoryService:
|
|
|
1189
1136
|
# Mem0's delete() may not accept user_id directly
|
|
1190
1137
|
# Try with user_id first, fall back without it if it fails
|
|
1191
1138
|
try:
|
|
1192
|
-
result = self.memory.delete(
|
|
1193
|
-
memory_id=memory_id, user_id=user_id, **kwargs
|
|
1194
|
-
)
|
|
1139
|
+
result = self.memory.delete(memory_id=memory_id, user_id=user_id, **kwargs)
|
|
1195
1140
|
except TypeError as e:
|
|
1196
1141
|
if "unexpected keyword argument 'user_id'" in str(e):
|
|
1197
1142
|
# Mem0 doesn't accept user_id, try without it
|
|
@@ -1280,6 +1225,4 @@ def get_memory_service(
|
|
|
1280
1225
|
"Mem0 dependencies not available. Install with: pip install mem0ai"
|
|
1281
1226
|
)
|
|
1282
1227
|
|
|
1283
|
-
return Mem0MemoryService(
|
|
1284
|
-
mongo_uri=mongo_uri, db_name=db_name, app_slug=app_slug, config=config
|
|
1285
|
-
)
|
|
1228
|
+
return Mem0MemoryService(mongo_uri=mongo_uri, db_name=db_name, app_slug=app_slug, config=config)
|
|
@@ -99,9 +99,11 @@ record_operation(
|
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
# Record failed operation
|
|
102
|
+
from pymongo.errors import PyMongoError
|
|
103
|
+
|
|
102
104
|
try:
|
|
103
105
|
await db.collection.insert_one(document)
|
|
104
|
-
except
|
|
106
|
+
except PyMongoError as e:
|
|
105
107
|
record_operation(
|
|
106
108
|
operation_name="database.insert_one",
|
|
107
109
|
duration_ms=0,
|
|
@@ -368,7 +370,7 @@ async def check_custom_service():
|
|
|
368
370
|
message="Service responded with error",
|
|
369
371
|
details={"status_code": response.status_code}
|
|
370
372
|
)
|
|
371
|
-
except
|
|
373
|
+
except (httpx.HTTPError, ConnectionError, TimeoutError) as e:
|
|
372
374
|
return HealthCheckResult(
|
|
373
375
|
name="custom_service",
|
|
374
376
|
status=HealthStatus.UNHEALTHY,
|
|
@@ -5,15 +5,32 @@ Provides structured logging, metrics collection, request tracing,
|
|
|
5
5
|
and health check capabilities.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .health import (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
from .health import (
|
|
9
|
+
HealthChecker,
|
|
10
|
+
HealthCheckResult,
|
|
11
|
+
HealthStatus,
|
|
12
|
+
check_engine_health,
|
|
13
|
+
check_mongodb_health,
|
|
14
|
+
check_pool_health,
|
|
15
|
+
)
|
|
16
|
+
from .logging import (
|
|
17
|
+
ContextualLoggerAdapter,
|
|
18
|
+
clear_app_context,
|
|
19
|
+
clear_correlation_id,
|
|
20
|
+
get_correlation_id,
|
|
21
|
+
get_logger,
|
|
22
|
+
get_logging_context,
|
|
23
|
+
log_operation,
|
|
24
|
+
set_app_context,
|
|
25
|
+
set_correlation_id,
|
|
26
|
+
)
|
|
27
|
+
from .metrics import (
|
|
28
|
+
MetricsCollector,
|
|
29
|
+
OperationMetrics,
|
|
30
|
+
get_metrics_collector,
|
|
31
|
+
record_operation,
|
|
32
|
+
timed_operation,
|
|
33
|
+
)
|
|
17
34
|
|
|
18
35
|
__all__ = [
|
|
19
36
|
# Metrics
|
|
@@ -11,8 +11,11 @@ from datetime import datetime
|
|
|
11
11
|
from enum import Enum
|
|
12
12
|
from typing import Any, Callable, Dict, List, Optional
|
|
13
13
|
|
|
14
|
-
from pymongo.errors import (
|
|
15
|
-
|
|
14
|
+
from pymongo.errors import (
|
|
15
|
+
ConnectionFailure,
|
|
16
|
+
OperationFailure,
|
|
17
|
+
ServerSelectionTimeoutError,
|
|
18
|
+
)
|
|
16
19
|
|
|
17
20
|
logger = logging.getLogger(__name__)
|
|
18
21
|
|
|
@@ -86,9 +89,7 @@ class HealthChecker:
|
|
|
86
89
|
ConnectionError,
|
|
87
90
|
OSError,
|
|
88
91
|
) as e:
|
|
89
|
-
logger.error(
|
|
90
|
-
f"Health check {check_func.__name__} failed: {e}", exc_info=True
|
|
91
|
-
)
|
|
92
|
+
logger.error(f"Health check {check_func.__name__} failed: {e}", exc_info=True)
|
|
92
93
|
results.append(
|
|
93
94
|
HealthCheckResult(
|
|
94
95
|
name=check_func.__name__,
|
|
@@ -137,9 +138,7 @@ async def check_mongodb_health(
|
|
|
137
138
|
|
|
138
139
|
try:
|
|
139
140
|
# Try to ping MongoDB with timeout
|
|
140
|
-
await asyncio.wait_for(
|
|
141
|
-
mongo_client.admin.command("ping"), timeout=timeout_seconds
|
|
142
|
-
)
|
|
141
|
+
await asyncio.wait_for(mongo_client.admin.command("ping"), timeout=timeout_seconds)
|
|
143
142
|
|
|
144
143
|
return HealthCheckResult(
|
|
145
144
|
name="mongodb",
|
|
@@ -206,7 +205,7 @@ async def check_engine_health(engine: Optional[Any]) -> HealthCheckResult:
|
|
|
206
205
|
|
|
207
206
|
|
|
208
207
|
async def check_pool_health(
|
|
209
|
-
get_pool_metrics_func: Optional[Callable[[], Any]] = None
|
|
208
|
+
get_pool_metrics_func: Optional[Callable[[], Any]] = None,
|
|
210
209
|
) -> HealthCheckResult:
|
|
211
210
|
"""
|
|
212
211
|
Check connection pool health.
|
|
@@ -55,16 +55,12 @@ class OperationMetrics:
|
|
|
55
55
|
"count": self.count,
|
|
56
56
|
"avg_duration_ms": round(self.avg_duration_ms, 2),
|
|
57
57
|
"min_duration_ms": (
|
|
58
|
-
round(self.min_duration_ms, 2)
|
|
59
|
-
if self.min_duration_ms != float("inf")
|
|
60
|
-
else 0.0
|
|
58
|
+
round(self.min_duration_ms, 2) if self.min_duration_ms != float("inf") else 0.0
|
|
61
59
|
),
|
|
62
60
|
"max_duration_ms": round(self.max_duration_ms, 2),
|
|
63
61
|
"error_count": self.error_count,
|
|
64
62
|
"error_rate_percent": round(self.error_rate, 2),
|
|
65
|
-
"last_execution": (
|
|
66
|
-
self.last_execution.isoformat() if self.last_execution else None
|
|
67
|
-
),
|
|
63
|
+
"last_execution": (self.last_execution.isoformat() if self.last_execution else None),
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
|
|
@@ -138,9 +134,7 @@ class MetricsCollector:
|
|
|
138
134
|
with self._lock:
|
|
139
135
|
if operation_name:
|
|
140
136
|
metrics = {
|
|
141
|
-
k: v.to_dict()
|
|
142
|
-
for k, v in self._metrics.items()
|
|
143
|
-
if k.startswith(operation_name)
|
|
137
|
+
k: v.to_dict() for k, v in self._metrics.items() if k.startswith(operation_name)
|
|
144
138
|
}
|
|
145
139
|
# Move accessed metrics to end (LRU)
|
|
146
140
|
for key in list(self._metrics.keys()):
|
|
@@ -178,7 +172,7 @@ class MetricsCollector:
|
|
|
178
172
|
# Aggregate by base operation name (without tags)
|
|
179
173
|
aggregated: Dict[str, OperationMetrics] = {}
|
|
180
174
|
|
|
181
|
-
for
|
|
175
|
+
for _key, metric in self._metrics.items():
|
|
182
176
|
base_name = metric.operation_name
|
|
183
177
|
if base_name not in aggregated:
|
|
184
178
|
aggregated[base_name] = OperationMetrics(operation_name=base_name)
|
|
@@ -260,6 +254,32 @@ def timed_operation(operation_name: str, **tags: Any):
|
|
|
260
254
|
...
|
|
261
255
|
"""
|
|
262
256
|
|
|
257
|
+
# Common exceptions that indicate operation failure (not system-level exits)
|
|
258
|
+
# This is comprehensive to handle any decorated function's failures
|
|
259
|
+
_OPERATION_FAILURES = (
|
|
260
|
+
RuntimeError,
|
|
261
|
+
ValueError,
|
|
262
|
+
TypeError,
|
|
263
|
+
KeyError,
|
|
264
|
+
AttributeError,
|
|
265
|
+
IndexError,
|
|
266
|
+
LookupError,
|
|
267
|
+
IOError,
|
|
268
|
+
OSError,
|
|
269
|
+
ConnectionError,
|
|
270
|
+
TimeoutError,
|
|
271
|
+
PermissionError,
|
|
272
|
+
FileNotFoundError,
|
|
273
|
+
AssertionError,
|
|
274
|
+
ArithmeticError,
|
|
275
|
+
BufferError,
|
|
276
|
+
ImportError,
|
|
277
|
+
MemoryError,
|
|
278
|
+
StopIteration,
|
|
279
|
+
StopAsyncIteration,
|
|
280
|
+
UnicodeError,
|
|
281
|
+
)
|
|
282
|
+
|
|
263
283
|
def decorator(func: Callable) -> Callable:
|
|
264
284
|
if hasattr(func, "__code__") and func.__code__.co_flags & 0x80: # CO_COROUTINE
|
|
265
285
|
# Async function
|
|
@@ -269,7 +289,7 @@ def timed_operation(operation_name: str, **tags: Any):
|
|
|
269
289
|
try:
|
|
270
290
|
result = await func(*args, **kwargs)
|
|
271
291
|
return result
|
|
272
|
-
except
|
|
292
|
+
except _OPERATION_FAILURES:
|
|
273
293
|
success = False
|
|
274
294
|
raise
|
|
275
295
|
finally:
|
|
@@ -285,7 +305,7 @@ def timed_operation(operation_name: str, **tags: Any):
|
|
|
285
305
|
try:
|
|
286
306
|
result = func(*args, **kwargs)
|
|
287
307
|
return result
|
|
288
|
-
except
|
|
308
|
+
except _OPERATION_FAILURES:
|
|
289
309
|
success = False
|
|
290
310
|
raise
|
|
291
311
|
finally:
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MDB Engine Repository Pattern
|
|
3
|
+
|
|
4
|
+
Provides abstract repository interfaces and MongoDB implementations
|
|
5
|
+
for clean data access patterns.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from mdb_engine.repositories import Repository, MongoRepository
|
|
9
|
+
|
|
10
|
+
# In domain services
|
|
11
|
+
class UserService:
|
|
12
|
+
def __init__(self, users: Repository[User]):
|
|
13
|
+
self._users = users
|
|
14
|
+
|
|
15
|
+
async def get_user(self, id: str) -> User:
|
|
16
|
+
return await self._users.get(id)
|
|
17
|
+
|
|
18
|
+
# In FastAPI routes using UnitOfWork
|
|
19
|
+
@app.get("/users/{user_id}")
|
|
20
|
+
async def get_user(user_id: str, ctx: RequestContext = Depends()):
|
|
21
|
+
user = await ctx.uow.users.get(user_id)
|
|
22
|
+
return user
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from .base import Entity, Repository
|
|
26
|
+
from .mongo import MongoRepository
|
|
27
|
+
from .unit_of_work import UnitOfWork
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Repository",
|
|
31
|
+
"Entity",
|
|
32
|
+
"MongoRepository",
|
|
33
|
+
"UnitOfWork",
|
|
34
|
+
]
|