mvx-common 0.2.1__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 (38) hide show
  1. mvx/common/__init__.py +1 -0
  2. mvx/common/errors/__init__.py +14 -0
  3. mvx/common/errors/invalid_function_argument_error.py +54 -0
  4. mvx/common/errors/reasoned_error.py +56 -0
  5. mvx/common/errors/runtime_errors.py +79 -0
  6. mvx/common/errors/structured_error.py +85 -0
  7. mvx/common/helpers/__init__.py +18 -0
  8. mvx/common/helpers/api_error_processor.py +122 -0
  9. mvx/common/helpers/document_enum.py +18 -0
  10. mvx/common/helpers/introspection.py +21 -0
  11. mvx/common/helpers/run_with_cancellation_policy.py +161 -0
  12. mvx/common/logger/__init__.py +694 -0
  13. mvx/common/logger/adapter_logging/__init__.py +16 -0
  14. mvx/common/logger/adapter_logging/log_record_factory.py +137 -0
  15. mvx/common/logger/adapter_logging/logging_configs.py +371 -0
  16. mvx/common/logger/adapter_logging/logging_file_sink.py +205 -0
  17. mvx/common/logger/adapter_logging/logging_stream_sink.py +187 -0
  18. mvx/common/logger/asyncio_log_sink/__init__.py +42 -0
  19. mvx/common/logger/asyncio_log_sink/common.py +39 -0
  20. mvx/common/logger/asyncio_log_sink/errors.py +208 -0
  21. mvx/common/logger/asyncio_log_sink/log_sink.py +846 -0
  22. mvx/common/logger/errors.py +332 -0
  23. mvx/common/logger/helpers.py +34 -0
  24. mvx/common/logger/log_components/__init__.py +16 -0
  25. mvx/common/logger/log_components/log_invocation.py +878 -0
  26. mvx/common/logger/log_components/protocols.py +157 -0
  27. mvx/common/logger/log_context/__init__.py +8 -0
  28. mvx/common/logger/log_context/log_context.py +800 -0
  29. mvx/common/logger/log_payload_processor/__init__.py +19 -0
  30. mvx/common/logger/log_payload_processor/log_payload_processor.py +514 -0
  31. mvx/common/logger/log_payload_processor/types.py +52 -0
  32. mvx/common/logger/models.py +279 -0
  33. mvx/common/py.typed +0 -0
  34. mvx_common-0.2.1.dist-info/METADATA +208 -0
  35. mvx_common-0.2.1.dist-info/RECORD +38 -0
  36. mvx_common-0.2.1.dist-info/WHEEL +4 -0
  37. mvx_common-0.2.1.dist-info/licenses/LICENSE +42 -0
  38. mvx_common-0.2.1.dist-info/licenses/NOTICE +4 -0
mvx/common/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # common/src/mvx/common/__init__.py
@@ -0,0 +1,14 @@
1
+ # common/src/mvx/common/errors/__init__.py
2
+
3
+ from .structured_error import StructuredError
4
+ from .reasoned_error import ReasonedError
5
+ from .runtime_errors import RuntimeExtendedError, RuntimeUnexpectedError
6
+ from .invalid_function_argument_error import InvalidFunctionArgumentError
7
+
8
+ __all__ = (
9
+ "StructuredError",
10
+ "ReasonedError",
11
+ "RuntimeExtendedError",
12
+ "RuntimeUnexpectedError",
13
+ "InvalidFunctionArgumentError",
14
+ )
@@ -0,0 +1,54 @@
1
+ # common/src/mvx/common/errors/invalid_function_argument_error.py
2
+
3
+ from __future__ import annotations
4
+ from typing import Any, Mapping, Optional
5
+
6
+ from .structured_error import StructuredError
7
+
8
+ __all__ = ("InvalidFunctionArgumentError",)
9
+
10
+
11
+ class InvalidFunctionArgumentError(StructuredError):
12
+ """
13
+ Error raised when a function argument fails validation.
14
+
15
+ Wraps an underlying validation exception and adds structured context about
16
+ the function, argument, offending value, and validation error type.
17
+
18
+ Args:
19
+ func: Function name where validation failed.
20
+ arg: Argument name that failed validation.
21
+ value: Offending argument value, when safe and useful to log.
22
+ cause: Underlying validation exception.
23
+ details: Additional log-friendly diagnostic context.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ *,
29
+ func: Optional[str],
30
+ arg: Optional[str],
31
+ value: Optional[Any] = None,
32
+ cause: Exception,
33
+ details: Optional[Mapping[str, Any]] = None,
34
+ ) -> None:
35
+ msg = f"invalid argument -> {str(cause)}"
36
+
37
+ func = func or "<unknown>"
38
+ arg = arg or "<unknown>"
39
+
40
+ base_details: dict[str, Any] = {
41
+ "func": func,
42
+ "arg": arg,
43
+ "error_type": type(cause).__name__,
44
+ }
45
+ if value is not None:
46
+ base_details["value"] = value
47
+ if details:
48
+ base_details.update(details)
49
+
50
+ super().__init__(
51
+ message=msg,
52
+ details=base_details,
53
+ cause=cause,
54
+ )
@@ -0,0 +1,56 @@
1
+ # common/src/mvx/common/errors/reasoned_error.py
2
+ from __future__ import annotations
3
+ from typing import Any, Optional, Mapping
4
+
5
+ from .structured_error import StructuredError
6
+
7
+ __all__ = ("ReasonedError",)
8
+
9
+
10
+ class ReasonedError(StructuredError):
11
+ """
12
+ Structured error with an optional stable reason code.
13
+
14
+ Extends StructuredError by adding `reason_code`, which can be used as a
15
+ machine-readable classifier for logging, metrics, and tests.
16
+
17
+ Args:
18
+ message: Human-readable error message.
19
+ details: Optional log-friendly diagnostic context.
20
+ cause: Optional underlying exception.
21
+ reason: Optional stable reason code.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ *,
27
+ message: str,
28
+ details: Optional[Mapping[str, Any]] = None,
29
+ cause: Optional[Exception] = None,
30
+ reason: Optional[str] = None,
31
+ ) -> None:
32
+
33
+ self.reason_code: Optional[str] = reason
34
+
35
+ super().__init__(
36
+ message=message,
37
+ details=details,
38
+ cause=cause,
39
+ )
40
+
41
+ def to_log_payload(self) -> dict[str, Any]:
42
+ """
43
+ Extend base log payload with the reason code.
44
+
45
+ Returns:
46
+ Log payload including "reason" when present.
47
+ """
48
+
49
+ payload: dict[str, Any] = {}
50
+
51
+ if self.reason_code is not None:
52
+ payload["reason"] = self.reason_code
53
+
54
+ payload.update(super().to_log_payload())
55
+
56
+ return payload
@@ -0,0 +1,79 @@
1
+ # common/src/mvx/common/errors/runtime_errors.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional, Mapping, Any
5
+
6
+ from .structured_error import StructuredError
7
+
8
+ __all__ = (
9
+ "RuntimeExtendedError",
10
+ "RuntimeUnexpectedError",
11
+ )
12
+
13
+
14
+ class RuntimeExtendedError(StructuredError, RuntimeError):
15
+ """
16
+ RuntimeError variant with structured diagnostic context.
17
+
18
+ Extends RuntimeError with the structured error payload provided by
19
+ StructuredError and optional source metadata.
20
+
21
+ Args:
22
+ message: Human-readable error message.
23
+ details: Optional log-friendly diagnostic context.
24
+ cause: Optional underlying exception.
25
+ module: Optional module name associated with the error.
26
+ qualname: Optional qualified name associated with the error.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ *,
32
+ message: str,
33
+ details: Optional[Mapping[str, Any]] = None,
34
+ cause: Optional[Exception] = None,
35
+ module: Optional[str] = None,
36
+ qualname: Optional[str] = None,
37
+ ) -> None:
38
+
39
+ self.module = None if module is None else (module.strip() or None)
40
+ self.qualname = None if qualname is None else (qualname.strip() or None)
41
+
42
+ RuntimeError.__init__(self, str(message))
43
+
44
+ StructuredError.__init__(
45
+ self,
46
+ message=message,
47
+ details=details,
48
+ cause=cause,
49
+ )
50
+
51
+ def to_log_payload(self) -> dict[str, Any]:
52
+ """
53
+ Return a structured logging payload with optional source metadata.
54
+
55
+ Returns:
56
+ Log-friendly error payload.
57
+ """
58
+ base = StructuredError.to_log_payload(self)
59
+
60
+ payload: dict[str, Any] = {}
61
+ if self.module is not None:
62
+ payload["module"] = self.module
63
+ if self.qualname is not None:
64
+ payload["qualname"] = self.qualname
65
+
66
+ payload.update(base)
67
+ return payload
68
+
69
+
70
+ class RuntimeUnexpectedError(Exception):
71
+ """
72
+ Marker base class for runtime errors classified as unexpected.
73
+
74
+ This class is intended for multiple inheritance with concrete domain
75
+ errors. It marks an error as unexpected without replacing the domain-specific
76
+ error hierarchy.
77
+ """
78
+
79
+ ...
@@ -0,0 +1,85 @@
1
+ # common/src/mvx/common/errors/structured_error.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Mapping, Optional, Self
5
+
6
+ __all__ = ("StructuredError",)
7
+
8
+
9
+ class StructuredError(Exception):
10
+ """
11
+ Base exception class for errors with structured diagnostic context.
12
+
13
+ Args:
14
+ message: Human-readable error message.
15
+ details: Optional log-friendly diagnostic context.
16
+ cause: Optional underlying exception.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ message: str,
23
+ details: Optional[Mapping[str, Any]] = None,
24
+ cause: Optional[Exception] = None,
25
+ ) -> None:
26
+ self.message: str = message
27
+ self.details: dict[str, Any] = dict(details or {})
28
+
29
+ self.cause: Optional[Exception] = cause
30
+
31
+ super().__init__(message)
32
+
33
+ def __str__(self) -> str:
34
+ base = f"{self.__class__.__name__}: {self.message}"
35
+ if self.details:
36
+ return f"{base} | details={self.details!r}"
37
+ return base
38
+
39
+ def with_detail(self, key: str, value: Any) -> Self:
40
+ """
41
+ Add or replace one detail entry.
42
+
43
+ Args:
44
+ key: Detail key.
45
+ value: Detail value.
46
+
47
+ Returns:
48
+ This error instance.
49
+ """
50
+ self.details[key] = value
51
+ return self
52
+
53
+ def with_details(self, extra: Mapping[str, Any]) -> Self:
54
+ """
55
+ Merge multiple detail entries.
56
+
57
+ Args:
58
+ extra: Detail entries to merge.
59
+
60
+ Returns:
61
+ This error instance.
62
+ """
63
+ self.details.update(extra)
64
+ return self
65
+
66
+ def to_log_payload(self) -> dict[str, Any]:
67
+ """
68
+ Return a stable dictionary representation for structured logging.
69
+
70
+ Returns:
71
+ Log-friendly error payload.
72
+ """
73
+ payload: dict[str, Any] = {
74
+ "kind": self.__class__.__name__,
75
+ "message": self.message,
76
+ "details": dict(self.details),
77
+ }
78
+
79
+ if self.cause is not None:
80
+ payload["cause"] = {
81
+ "kind": self.cause.__class__.__name__,
82
+ "message": str(self.cause),
83
+ }
84
+
85
+ return payload
@@ -0,0 +1,18 @@
1
+ # common/src/mvx/common/helpers/__init__.py
2
+ from .introspection import (
3
+ get_func_module_and_qualname,
4
+ )
5
+
6
+ from .api_error_processor import api_error_processor
7
+
8
+ from .run_with_cancellation_policy import run_with_cancellation_policy, CancellationPolicy
9
+
10
+ __all__ = (
11
+ # from introspection.py
12
+ "get_func_module_and_qualname",
13
+ # from api_error_processor.py
14
+ "api_error_processor",
15
+ # from run_with_cancellation_policy.py
16
+ "run_with_cancellation_policy",
17
+ "CancellationPolicy",
18
+ )
@@ -0,0 +1,122 @@
1
+ # common/src/mvx/common/helpers/api_error_processor.py
2
+ """
3
+ Decorator for normalizing public API errors.
4
+
5
+ The decorator lets declared API errors pass through unchanged and wraps
6
+ unexpected exceptions into a configured RuntimeExtendedError subclass.
7
+ Cancellation is always propagated unchanged.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Callable, TypeVar, Any, cast
13
+
14
+ import asyncio
15
+ from functools import wraps
16
+ import inspect
17
+
18
+ from ..errors import RuntimeExtendedError
19
+
20
+ __all__ = ("api_error_processor",)
21
+
22
+ F = TypeVar("F", bound=Callable[..., Any])
23
+
24
+
25
+ def api_error_processor(
26
+ *,
27
+ passthrough_error_types: tuple[type[Exception], ...],
28
+ raise_error_type: type[RuntimeExtendedError],
29
+ ) -> Callable[[F], F]:
30
+ """
31
+ Build a decorator for public API exception normalization.
32
+
33
+ Args:
34
+ passthrough_error_types: Exception types that must pass through unchanged.
35
+ raise_error_type: RuntimeExtendedError subclass used to wrap unexpected
36
+ exceptions.
37
+
38
+ Returns:
39
+ Decorator that applies the public API error policy to sync or async
40
+ callables.
41
+ """
42
+
43
+ def decorate(func: F) -> F:
44
+ module = getattr(func, "__module__", "<unknown>")
45
+ qualname = getattr(func, "__qualname__", getattr(func, "__name__", "<unknown>"))
46
+
47
+ target = inspect.unwrap(func)
48
+
49
+ if inspect.iscoroutinefunction(target):
50
+
51
+ @wraps(func)
52
+ async def wrapped_async(*args: Any, **kwargs: Any) -> Any:
53
+ try:
54
+ return await func(*args, **kwargs)
55
+ except asyncio.CancelledError:
56
+ raise
57
+ except Exception as exc:
58
+ if isinstance(exc, passthrough_error_types):
59
+ raise
60
+
61
+ if isinstance(exc, RuntimeExtendedError):
62
+ if exc.module is None:
63
+ exc.module = module
64
+ if exc.qualname is None:
65
+ exc.qualname = qualname
66
+ raise
67
+
68
+ # noinspection PyBroadException
69
+ try:
70
+ err = raise_error_type(
71
+ message=f"runtime unexpected error: {str(exc)}",
72
+ module=module,
73
+ qualname=qualname,
74
+ cause=exc,
75
+ )
76
+ except Exception:
77
+ err = raise_error_type(message=str(exc))
78
+ err.cause = exc
79
+ err.module = module
80
+ err.qualname = qualname
81
+
82
+ raise err from exc
83
+
84
+ # noinspection PyUnnecessaryCast
85
+ return cast(F, wrapped_async)
86
+
87
+ @wraps(func)
88
+ def wrapped_sync(*args: Any, **kwargs: Any) -> Any:
89
+ try:
90
+ return func(*args, **kwargs)
91
+ except asyncio.CancelledError:
92
+ raise
93
+ except Exception as exc:
94
+ if isinstance(exc, passthrough_error_types):
95
+ raise
96
+ if isinstance(exc, RuntimeExtendedError):
97
+ if exc.module is None:
98
+ exc.module = module
99
+ if exc.qualname is None:
100
+ exc.qualname = qualname
101
+ raise
102
+
103
+ # noinspection PyBroadException
104
+ try:
105
+ err = raise_error_type(
106
+ message=f"runtime unexpected error: {str(exc)}",
107
+ module=module,
108
+ qualname=qualname,
109
+ cause=exc,
110
+ )
111
+ except Exception:
112
+ err = raise_error_type(message=str(exc))
113
+ err.cause = exc
114
+ err.module = module
115
+ err.qualname = qualname
116
+
117
+ raise err from exc
118
+
119
+ # noinspection PyUnnecessaryCast
120
+ return cast(F, wrapped_sync)
121
+
122
+ return decorate
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import TypeVar, cast
5
+
6
+ EnumClassT = TypeVar("EnumClassT", bound=type[Enum])
7
+
8
+ try:
9
+ from enum_tools.documentation import document_enum as _document_enum
10
+ except ImportError:
11
+
12
+ def document_enum(enum_class: EnumClassT) -> EnumClassT:
13
+ return enum_class
14
+
15
+ else:
16
+
17
+ def document_enum(enum_class: EnumClassT) -> EnumClassT:
18
+ return cast(EnumClassT, _document_enum(enum_class))
@@ -0,0 +1,21 @@
1
+ # common/src/mvx/common/helpers/introspection.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Callable
5
+
6
+ __all__ = ("get_func_module_and_qualname",)
7
+
8
+
9
+ def get_func_module_and_qualname(func: Callable) -> tuple[str, str]:
10
+ """
11
+ Return module and qualified name of a callable.
12
+
13
+ Args:
14
+ func: Callable object.
15
+
16
+ Returns:
17
+ Tuple of (module, qualname).
18
+ """
19
+ module = getattr(func, "__module__", "<unknown>")
20
+ qualname = getattr(func, "__qualname__", getattr(func, "__name__", "<unknown>"))
21
+ return module, qualname
@@ -0,0 +1,161 @@
1
+ # common/src/mvx/common/helpers/run_with_cancellation_policy.py
2
+ """
3
+ Utilities for running awaitables under explicit cancellation policies.
4
+
5
+ The module provides a small primitive for cases where cancellation of the
6
+ caller task must either be handled normally, deferred and reported as a flag,
7
+ or deferred and re-raised after the protected operation completes.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Coroutine, Literal, TypeVar, Tuple, overload, Union
13
+ from collections.abc import Awaitable, Callable
14
+
15
+ from enum import StrEnum
16
+ import asyncio
17
+
18
+ __all__ = (
19
+ "CancellationPolicy",
20
+ "run_with_cancellation_policy",
21
+ )
22
+
23
+ T = TypeVar("T")
24
+
25
+
26
+ class CancellationPolicy(StrEnum):
27
+ """
28
+ Cancellation handling policy for run_with_cancellation_policy().
29
+ """
30
+
31
+ PLAIN = "PLAIN"
32
+ DEFER_RERAISE = "DEFER_RERAISE"
33
+ DEFER_FLAG = "DEFER_FLAG"
34
+
35
+
36
+ async def _as_coro(a: Awaitable[T]) -> T:
37
+ """
38
+ Wrap an arbitrary awaitable into a coroutine.
39
+
40
+ Args:
41
+ a: Awaitable to execute.
42
+
43
+ Returns:
44
+ Result produced by the awaitable.
45
+ """
46
+ return await a
47
+
48
+
49
+ def _start_core_task(core_func: Callable[[], Awaitable[T]], op_name: str) -> asyncio.Task[T]:
50
+ """
51
+ Start the core awaitable as a named asyncio task.
52
+
53
+ Args:
54
+ core_func: Zero-argument callable returning the awaitable to run.
55
+ op_name: Name assigned to the created task.
56
+
57
+ Returns:
58
+ Started asyncio task.
59
+ """
60
+ a = core_func()
61
+ coro: Coroutine[Any, Any, T] = _as_coro(a)
62
+ return asyncio.create_task(coro, name=op_name)
63
+
64
+
65
+ @overload
66
+ async def run_with_cancellation_policy(
67
+ core_func: Callable[[], Awaitable[T]],
68
+ *,
69
+ policy: Literal[CancellationPolicy.PLAIN],
70
+ ) -> T: ...
71
+
72
+
73
+ @overload
74
+ async def run_with_cancellation_policy(
75
+ core_func: Callable[[], Awaitable[T]],
76
+ *,
77
+ policy: Literal[CancellationPolicy.DEFER_RERAISE],
78
+ ) -> T: ...
79
+
80
+
81
+ @overload
82
+ async def run_with_cancellation_policy(
83
+ core_func: Callable[[], Awaitable[T]],
84
+ *,
85
+ policy: Literal[CancellationPolicy.DEFER_FLAG] = CancellationPolicy.DEFER_FLAG,
86
+ ) -> Tuple[bool, T]: ...
87
+
88
+
89
+ @overload
90
+ async def run_with_cancellation_policy(
91
+ core_func: Callable[[], Awaitable[T]],
92
+ *,
93
+ policy: CancellationPolicy,
94
+ op_name: str = "unknown",
95
+ ) -> Union[T, Tuple[bool, T]]: ...
96
+
97
+
98
+ async def run_with_cancellation_policy(
99
+ core_func: Callable[[], Awaitable[T]],
100
+ *,
101
+ policy: CancellationPolicy = CancellationPolicy.DEFER_FLAG,
102
+ op_name: str = "unknown",
103
+ ) -> Union[T, Tuple[bool, T]]:
104
+ """
105
+ Run one awaitable under the selected cancellation policy.
106
+
107
+ Args:
108
+ core_func: Zero-argument callable returning the awaitable to run.
109
+ policy: Cancellation policy applied while awaiting the operation.
110
+ op_name: Name assigned to the internal task in deferred policies.
111
+
112
+ Returns:
113
+ The operation result, or ``(cancel_requested, result)`` when using
114
+ ``CancellationPolicy.DEFER_FLAG``.
115
+
116
+ Raises:
117
+ asyncio.CancelledError: Raised according to the selected cancellation
118
+ policy or when the core task itself is cancelled.
119
+ Exception: Propagates exceptions raised by the core awaitable.
120
+ """
121
+ if policy is CancellationPolicy.PLAIN:
122
+ return await core_func()
123
+
124
+ core_task: asyncio.Task[T] = _start_core_task(core_func, op_name)
125
+ cancel_requested = False
126
+
127
+ while True:
128
+ try:
129
+ result = await asyncio.shield(core_task)
130
+ break
131
+ except asyncio.CancelledError:
132
+ # If the core task itself was cancelled, propagate core cancellation.
133
+ if core_task.cancelled():
134
+ raise
135
+
136
+ # Otherwise this is caller-task cancellation while core is still running (deferrable).
137
+ cancel_requested = True
138
+
139
+ current = asyncio.current_task()
140
+ if current is not None:
141
+ # Drop all pending cancellation requests for this caller task so that
142
+ # the next await does not immediately raise again.
143
+ while current.uncancel() > 0:
144
+ pass
145
+ # Keep waiting for core_task to finish.
146
+
147
+ # Drain any pending cancellation that might have arrived in a tight race window
148
+ # after the await resumed but before we return/raise.
149
+ current = asyncio.current_task()
150
+ if current is not None and current.cancelling() > 0:
151
+ cancel_requested = True
152
+ while current.uncancel() > 0:
153
+ pass
154
+
155
+ if policy is CancellationPolicy.DEFER_FLAG:
156
+ return cancel_requested, result
157
+
158
+ # policy is DEFER_RERAISE
159
+ if cancel_requested:
160
+ raise asyncio.CancelledError()
161
+ return result