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.
Files changed (87) hide show
  1. mdb_engine/__init__.py +104 -11
  2. mdb_engine/auth/ARCHITECTURE.md +112 -0
  3. mdb_engine/auth/README.md +648 -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 +264 -69
  8. mdb_engine/auth/config_helpers.py +7 -6
  9. mdb_engine/auth/cookie_utils.py +3 -7
  10. mdb_engine/auth/csrf.py +373 -0
  11. mdb_engine/auth/decorators.py +3 -10
  12. mdb_engine/auth/dependencies.py +47 -50
  13. mdb_engine/auth/helpers.py +3 -3
  14. mdb_engine/auth/integration.py +53 -80
  15. mdb_engine/auth/jwt.py +2 -6
  16. mdb_engine/auth/middleware.py +77 -34
  17. mdb_engine/auth/oso_factory.py +18 -38
  18. mdb_engine/auth/provider.py +270 -171
  19. mdb_engine/auth/rate_limiter.py +504 -0
  20. mdb_engine/auth/restrictions.py +8 -24
  21. mdb_engine/auth/session_manager.py +14 -29
  22. mdb_engine/auth/shared_middleware.py +600 -0
  23. mdb_engine/auth/shared_users.py +759 -0
  24. mdb_engine/auth/token_store.py +14 -28
  25. mdb_engine/auth/users.py +54 -113
  26. mdb_engine/auth/utils.py +213 -15
  27. mdb_engine/cli/commands/generate.py +545 -9
  28. mdb_engine/cli/commands/validate.py +3 -7
  29. mdb_engine/cli/utils.py +3 -3
  30. mdb_engine/config.py +7 -21
  31. mdb_engine/constants.py +65 -0
  32. mdb_engine/core/README.md +117 -6
  33. mdb_engine/core/__init__.py +39 -7
  34. mdb_engine/core/app_registration.py +22 -41
  35. mdb_engine/core/app_secrets.py +290 -0
  36. mdb_engine/core/connection.py +18 -9
  37. mdb_engine/core/encryption.py +223 -0
  38. mdb_engine/core/engine.py +1057 -93
  39. mdb_engine/core/index_management.py +12 -16
  40. mdb_engine/core/manifest.py +459 -150
  41. mdb_engine/core/ray_integration.py +435 -0
  42. mdb_engine/core/seeding.py +10 -18
  43. mdb_engine/core/service_initialization.py +12 -23
  44. mdb_engine/core/types.py +2 -5
  45. mdb_engine/database/README.md +140 -17
  46. mdb_engine/database/__init__.py +17 -6
  47. mdb_engine/database/abstraction.py +25 -37
  48. mdb_engine/database/connection.py +11 -18
  49. mdb_engine/database/query_validator.py +367 -0
  50. mdb_engine/database/resource_limiter.py +204 -0
  51. mdb_engine/database/scoped_wrapper.py +713 -196
  52. mdb_engine/dependencies.py +426 -0
  53. mdb_engine/di/__init__.py +34 -0
  54. mdb_engine/di/container.py +248 -0
  55. mdb_engine/di/providers.py +205 -0
  56. mdb_engine/di/scopes.py +139 -0
  57. mdb_engine/embeddings/README.md +54 -24
  58. mdb_engine/embeddings/__init__.py +31 -24
  59. mdb_engine/embeddings/dependencies.py +37 -154
  60. mdb_engine/embeddings/service.py +11 -25
  61. mdb_engine/exceptions.py +92 -0
  62. mdb_engine/indexes/README.md +30 -13
  63. mdb_engine/indexes/__init__.py +1 -0
  64. mdb_engine/indexes/helpers.py +1 -1
  65. mdb_engine/indexes/manager.py +50 -114
  66. mdb_engine/memory/README.md +2 -2
  67. mdb_engine/memory/__init__.py +1 -2
  68. mdb_engine/memory/service.py +30 -87
  69. mdb_engine/observability/README.md +4 -2
  70. mdb_engine/observability/__init__.py +26 -9
  71. mdb_engine/observability/health.py +8 -9
  72. mdb_engine/observability/metrics.py +32 -12
  73. mdb_engine/repositories/__init__.py +34 -0
  74. mdb_engine/repositories/base.py +325 -0
  75. mdb_engine/repositories/mongo.py +233 -0
  76. mdb_engine/repositories/unit_of_work.py +166 -0
  77. mdb_engine/routing/README.md +1 -1
  78. mdb_engine/routing/__init__.py +1 -3
  79. mdb_engine/routing/websockets.py +25 -60
  80. mdb_engine-0.2.0.dist-info/METADATA +313 -0
  81. mdb_engine-0.2.0.dist-info/RECORD +96 -0
  82. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  83. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  84. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/WHEEL +0 -0
  85. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
  86. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
  87. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/top_level.txt +0 -0
@@ -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.error(
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.error(
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 Exception as e:
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 Exception as e:
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 (HealthChecker, HealthCheckResult, HealthStatus,
9
- check_engine_health, check_mongodb_health,
10
- check_pool_health)
11
- from .logging import (ContextualLoggerAdapter, clear_app_context,
12
- clear_correlation_id, get_correlation_id, get_logger,
13
- get_logging_context, log_operation, set_app_context,
14
- set_correlation_id)
15
- from .metrics import (MetricsCollector, OperationMetrics,
16
- get_metrics_collector, record_operation, timed_operation)
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 (ConnectionFailure, OperationFailure,
15
- ServerSelectionTimeoutError)
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 key, metric in self._metrics.items():
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 Exception:
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 Exception:
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
+ ]