Skip to content

Commit caeb0b2

Browse files
fix tests add di to worker
1 parent 3056a09 commit caeb0b2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+754
-347
lines changed

app/application/repository/exceptions.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
class BaseRepositoryError(Exception):
1+
from app.decorators.mapper.factories import ContextAwareError
2+
3+
4+
class BaseRepositoryError(ContextAwareError):
25
"""Base exception for all repository-specific exceptions."""
36

47

@@ -18,5 +21,17 @@ class RecordCreateError(BaseRepositoryError):
1821
"""Raised when a creation fails."""
1922

2023

21-
class RecordRetreiveError(BaseRepositoryError):
24+
class RecordRetrieveError(BaseRepositoryError):
2225
"""Raised when a get fails."""
26+
27+
28+
class RecordAlreadyExistsError(BaseRepositoryError):
29+
"""Raised when a record already exists."""
30+
31+
32+
class ForeignKeyError(BaseRepositoryError):
33+
"""Raised when a foreign key constraint is violated."""
34+
35+
36+
class ValidationError(BaseRepositoryError):
37+
"""Raised when a validation error occurs."""

app/application/use_cases/record_use_cases.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ def __init__(self, record_repo: ISampleRecordRepository):
2020
async def create_record(
2121
self, request_object: SampleRecordCreateRequestSchema
2222
) -> SampleRecordResponseSchema:
23-
domain_object = SampleRecord(record_data=request_object.record_data)
23+
domain_object = SampleRecord(
24+
record_data=request_object.record_data, name=request_object.name
25+
)
2426
created_record = await self._repo.create(domain_object)
2527
return SampleRecordResponseSchema.from_orm(created_record)
2628

@@ -29,7 +31,9 @@ async def update_record(
2931
) -> SampleRecordResponseSchema:
3032
"""Update an existing record."""
3133
domain_object = SampleRecord(
32-
record_data=update_request.record_data, id=update_request.id
34+
record_data=update_request.record_data,
35+
id=update_request.id,
36+
name=update_request.name
3337
)
3438
updated_record = await self._repo.update(domain_object)
3539
return SampleRecordResponseSchema.from_orm(updated_record)

app/decorators/mapper/context.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from functools import cached_property
2+
from typing import Callable, Any
3+
4+
5+
class ExceptionContext:
6+
SENSITIVE_KEYS: frozenset[str] = frozenset(
7+
("password", "token", "key", "secret", "auth", "credential", "passwd")
8+
)
9+
10+
def __init__(
11+
self,
12+
original_exception: Exception,
13+
func: Callable,
14+
args: tuple[Any, ...],
15+
kwargs: dict[str, Any],
16+
):
17+
self.original_exception = original_exception
18+
self.func = func
19+
self.args = args
20+
self.kwargs = kwargs
21+
22+
@cached_property
23+
def formatted_context(self) -> str:
24+
error_context = [
25+
f"Error in function '{self.func.__module__}.{self.func.__qualname__}'"
26+
]
27+
28+
if self.args:
29+
args_str = ", ".join(self._sanitised_value(arg) for arg in self.args)
30+
error_context.append(f"Args: [{args_str}]")
31+
32+
if self.kwargs:
33+
kwargs_str = ", ".join(
34+
f"{k}={self._sanitised_value(v, k)}" for k, v in self.kwargs.items()
35+
)
36+
error_context.append(f"Kwargs: {kwargs_str}")
37+
38+
return "\n".join(error_context).replace("{", "{{").replace("}", "}}")
39+
40+
def _sanitised_value(
41+
self,
42+
value: Any,
43+
key: str | None = None,
44+
) -> str:
45+
if key is not None and key.lower() in self.SENSITIVE_KEYS:
46+
return "****HIDDEN****"
47+
48+
try:
49+
str_value = str(value)
50+
return f"{str_value[:100]}..." if len(str_value) > 100 else str_value
51+
except Exception:
52+
return f"<{type(value).__name__} object - str() failed>"
Lines changed: 6 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,15 @@
11
"""Decorators to rethrow and log exceptions."""
22

3-
from abc import ABC, abstractmethod
4-
from functools import cached_property, wraps
3+
from functools import wraps
54
from inspect import iscoroutinefunction
65
from typing import Any, Callable, Type
76

87
from cachetools import LRUCache # type:ignore
98

9+
from app.decorators.mapper.context import ExceptionContext
10+
from app.decorators.mapper.factories import ExceptionFactory
1011
from app.logger import logger
1112

12-
13-
class ExceptionContext:
14-
SENSITIVE_KEYS: frozenset[str] = frozenset(
15-
("password", "token", "key", "secret", "auth", "credential", "passwd")
16-
)
17-
18-
def __init__(
19-
self,
20-
original_exception: Exception,
21-
func: Callable,
22-
args: tuple[Any, ...],
23-
kwargs: dict[str, Any],
24-
):
25-
self.original_exception = original_exception
26-
self.func = func
27-
self.args = args
28-
self.kwargs = kwargs
29-
30-
@cached_property
31-
def formatted_context(self) -> str:
32-
error_context = [
33-
f"Error in function '{self.func.__module__}.{self.func.__qualname__}'"
34-
]
35-
36-
if self.args:
37-
args_str = ", ".join(self._sanitised_value(arg) for arg in self.args)
38-
error_context.append(f"Args: [{args_str}]")
39-
40-
if self.kwargs:
41-
kwargs_str = ", ".join(
42-
f"{k}={self._sanitised_value(v, k)}" for k, v in self.kwargs.items()
43-
)
44-
error_context.append(f"Kwargs: {kwargs_str}")
45-
46-
return "\n".join(error_context).replace("{", "{{").replace("}", "}}")
47-
48-
def _sanitised_value(
49-
self,
50-
value: Any,
51-
key: str | None = None,
52-
) -> str:
53-
if key is not None and key.lower() in self.SENSITIVE_KEYS:
54-
return "****HIDDEN****"
55-
56-
try:
57-
str_value = str(value)
58-
return f"{str_value[:100]}..." if len(str_value) > 100 else str_value
59-
except Exception:
60-
return f"<{type(value).__name__} object - str() failed>"
61-
62-
63-
class ExceptionFactory(ABC):
64-
"""
65-
Create and describe a factory for exceptions.
66-
67-
This class is an abstract base class meant to define the interface for an
68-
exception factory.
69-
70-
"""
71-
72-
@abstractmethod
73-
def make_exception(self, context: ExceptionContext) -> Exception:
74-
"""Make an exception based on the given context."""
75-
76-
77-
class EnrichedExceptionFactory(ExceptionFactory):
78-
"""
79-
Create and manage enriched exceptions based on a given exception type.
80-
81-
This class provides a mechanism to create exceptions dynamically,
82-
enriching them with a formatted context. It extends the behavior of
83-
the base ExceptionFactory class by incorporating the concept of a
84-
generated error type and formatted context.
85-
86-
:ivar generated_error: The type of exception to generate when creating
87-
an enriched exception.
88-
:type generated_error: type[Exception]
89-
"""
90-
91-
def __init__(self, generated_error: type[Exception]):
92-
self.generated_error = generated_error
93-
94-
def make_exception(self, context: ExceptionContext) -> Exception:
95-
return self.generated_error(context.formatted_context)
96-
97-
9813
ExceptionOrTupleOfExceptions = Type[Exception] | tuple[Type[Exception], ...]
9914

10015

@@ -105,13 +20,11 @@ def __init__(
10520
self,
10621
exception_map: dict[ExceptionOrTupleOfExceptions, ExceptionFactory],
10722
max_cache_size: int = 512,
108-
log_error: bool = True,
10923
is_bound_method: bool = False,
11024
):
111-
self.mapping = self._get_flat_map(exception_map)
25+
self.mapping = self._get_exceptions_flat_map(exception_map)
11226
self.exception_catchall_factory = self.mapping.pop(Exception, None)
11327
self._lru_cache: LRUCache = LRUCache(maxsize=max_cache_size)
114-
self.log_error = log_error
11528
self.is_bound_method = is_bound_method
11629

11730
def __call__(self, func: Callable) -> Callable:
@@ -121,7 +34,7 @@ def __call__(self, func: Callable) -> Callable:
12134
else self._sync_wrapper(func)
12235
)
12336

124-
def _get_flat_map(
37+
def _get_exceptions_flat_map(
12538
self,
12639
exception_map: dict[ExceptionOrTupleOfExceptions, ExceptionFactory],
12740
) -> dict[Type[Exception], ExceptionFactory]:
@@ -164,11 +77,8 @@ def _handle_exception_logic(
16477
args: tuple[Any, ...],
16578
kwargs: dict[str, Any],
16679
) -> None:
167-
context = ExceptionContext(exc, func, self._filtered_args(args), kwargs)
168-
if self.log_error:
169-
logger.error(context.formatted_context, exc_info=True)
170-
17180
if exception_factory := self._get_exception_factory(type(exc)):
81+
context = ExceptionContext(exc, func, self._filtered_args(args), kwargs)
17282
raise exception_factory.make_exception(context) from exc
17383

17484
raise exc

app/decorators/mapper/factories.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from abc import ABC, abstractmethod
2+
3+
from app.decorators.mapper.context import ExceptionContext
4+
5+
6+
class ContextAwareError(Exception):
7+
def __init__(self, message: str, context: ExceptionContext = None, *args):
8+
super().__init__(message, *args)
9+
self.context = context
10+
11+
def __str__(self) -> str:
12+
base = super().__str__()
13+
if self.context:
14+
return f"{base} | context={self.context.formatted_context}"
15+
return base
16+
17+
18+
class ExceptionFactory(ABC):
19+
"""
20+
Create and describe a factory for exceptions.
21+
22+
This class is an abstract base class meant to define the interface for an
23+
exception factory.
24+
25+
"""
26+
27+
@abstractmethod
28+
def make_exception(self, context: ExceptionContext) -> Exception:
29+
"""Make an exception based on the given context."""
30+
31+
32+
class PassThroughExceptionFactory(ExceptionFactory):
33+
"""Factory for exceptions that should be passed through without create a new one.
34+
35+
Useful for cases when broad Exception cached and wrapped to a common Exception type.
36+
"""
37+
38+
def make_exception(self, context: ExceptionContext) -> Exception:
39+
return context.original_exception
40+
41+
42+
class EnrichedExceptionFactory(ExceptionFactory):
43+
"""
44+
Create and manage enriched exceptions based on a given exception type.
45+
46+
This class provides a mechanism to create exceptions dynamically,
47+
enriching them with a formatted context. It extends the behavior of
48+
the base ExceptionFactory class by incorporating the concept of a
49+
generated error type and formatted context.
50+
51+
:ivar generated_error: The type of exception to generate when creating
52+
an enriched exception.
53+
:type generated_error: type[Exception]
54+
"""
55+
56+
def __init__(self, generated_error: type[ContextAwareError]):
57+
self.generated_error = generated_error
58+
59+
def make_exception(self, context: ExceptionContext) -> ContextAwareError:
60+
return self.generated_error(str(context.original_exception), context=context)

app/domain/entities/sample_record.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,16 @@
22

33
from dataclasses import dataclass
44

5-
from app.domain.exceptions.domain_exceptions import WrongRecordDataError
65

76

87
@dataclass
98
class SampleRecord:
109
"""Record entity representing a simple record in the system."""
1110

1211
record_data: str
12+
name: str
1313
id: int | None = None
1414

1515
def __str__(self) -> str:
1616
"""Return string representation of the record."""
17-
return self.record_data
18-
19-
def __post_init__(self) -> None:
20-
"""Insert business validation here
21-
For example for some reason record data shouldn't start with A123
22-
"""
23-
if self.record_data.startswith("A123"):
24-
raise WrongRecordDataError("Record data shouldn't start with A")
17+
return f"id={self.id}, record_data={self.record_data}, name={self.name}"

app/domain/exceptions/domain_exceptions.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)