taipanstack 0.1.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.
- taipanstack/__init__.py +53 -0
- taipanstack/config/__init__.py +25 -0
- taipanstack/config/generators.py +357 -0
- taipanstack/config/models.py +316 -0
- taipanstack/config/version_config.py +227 -0
- taipanstack/core/__init__.py +47 -0
- taipanstack/core/compat.py +329 -0
- taipanstack/core/optimizations.py +392 -0
- taipanstack/core/result.py +199 -0
- taipanstack/security/__init__.py +55 -0
- taipanstack/security/decorators.py +369 -0
- taipanstack/security/guards.py +362 -0
- taipanstack/security/sanitizers.py +321 -0
- taipanstack/security/validators.py +342 -0
- taipanstack/utils/__init__.py +24 -0
- taipanstack/utils/circuit_breaker.py +268 -0
- taipanstack/utils/filesystem.py +417 -0
- taipanstack/utils/logging.py +328 -0
- taipanstack/utils/metrics.py +272 -0
- taipanstack/utils/retry.py +300 -0
- taipanstack/utils/subprocess.py +344 -0
- taipanstack-0.1.0.dist-info/METADATA +350 -0
- taipanstack-0.1.0.dist-info/RECORD +25 -0
- taipanstack-0.1.0.dist-info/WHEEL +4 -0
- taipanstack-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Result type utilities for functional error handling.
|
|
3
|
+
|
|
4
|
+
Provides Rust-style Result types (Ok/Err) for explicit error handling,
|
|
5
|
+
avoiding exceptions for expected failure cases. This promotes safer,
|
|
6
|
+
more predictable code.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from taipanstack.core.result import safe, Ok, Err
|
|
10
|
+
>>> @safe
|
|
11
|
+
... def divide(a: int, b: int) -> float:
|
|
12
|
+
... if b == 0:
|
|
13
|
+
... raise ValueError("division by zero")
|
|
14
|
+
... return a / b
|
|
15
|
+
>>> result = divide(10, 0)
|
|
16
|
+
>>> match result:
|
|
17
|
+
... case Err(e):
|
|
18
|
+
... print(f"Error: {e}")
|
|
19
|
+
... case Ok(value):
|
|
20
|
+
... print(f"Result: {value}")
|
|
21
|
+
Error: division by zero
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import functools
|
|
28
|
+
from collections.abc import Callable, Iterable
|
|
29
|
+
from typing import ParamSpec, TypeVar
|
|
30
|
+
|
|
31
|
+
from result import Err, Ok, Result
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Err",
|
|
35
|
+
"Ok",
|
|
36
|
+
"Result",
|
|
37
|
+
"collect_results",
|
|
38
|
+
"safe",
|
|
39
|
+
"safe_from",
|
|
40
|
+
"unwrap_or",
|
|
41
|
+
"unwrap_or_else",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
P = ParamSpec("P")
|
|
45
|
+
T = TypeVar("T")
|
|
46
|
+
E = TypeVar("E", bound=Exception)
|
|
47
|
+
U = TypeVar("U")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def safe(
|
|
51
|
+
func: Callable[P, T],
|
|
52
|
+
) -> Callable[P, Result[T, Exception]]:
|
|
53
|
+
"""Decorator to convert exceptions into Err results.
|
|
54
|
+
|
|
55
|
+
Wraps a function so that any exception raised becomes an Err,
|
|
56
|
+
while successful returns become Ok.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
func: The function to wrap.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A wrapped function that returns Result[T, Exception].
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
>>> @safe
|
|
66
|
+
... def parse_int(s: str) -> int:
|
|
67
|
+
... return int(s)
|
|
68
|
+
>>> parse_int("42")
|
|
69
|
+
Ok(42)
|
|
70
|
+
>>> parse_int("invalid")
|
|
71
|
+
Err(ValueError("invalid literal for int()..."))
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@functools.wraps(func)
|
|
76
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[T, Exception]:
|
|
77
|
+
try:
|
|
78
|
+
return Ok(func(*args, **kwargs))
|
|
79
|
+
except Exception as e:
|
|
80
|
+
return Err(e)
|
|
81
|
+
|
|
82
|
+
return wrapper
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def safe_from(
|
|
86
|
+
*exception_types: type[E],
|
|
87
|
+
) -> Callable[[Callable[P, T]], Callable[P, Result[T, E]]]:
|
|
88
|
+
"""Decorator factory to catch specific exceptions as Err.
|
|
89
|
+
|
|
90
|
+
Only catches specified exception types; others propagate normally.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
*exception_types: Exception types to convert to Err.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Decorator that wraps function with selective error handling.
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
>>> @safe_from(ValueError, TypeError)
|
|
100
|
+
... def process(data: str) -> int:
|
|
101
|
+
... return int(data)
|
|
102
|
+
>>> process("abc")
|
|
103
|
+
Err(ValueError(...))
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def decorator(func: Callable[P, T]) -> Callable[P, Result[T, E]]:
|
|
108
|
+
@functools.wraps(func)
|
|
109
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[T, E]:
|
|
110
|
+
try:
|
|
111
|
+
return Ok(func(*args, **kwargs))
|
|
112
|
+
except exception_types as e:
|
|
113
|
+
return Err(e)
|
|
114
|
+
|
|
115
|
+
return wrapper
|
|
116
|
+
|
|
117
|
+
return decorator
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def collect_results(
|
|
121
|
+
results: Iterable[Result[T, E]],
|
|
122
|
+
) -> Result[list[T], E]:
|
|
123
|
+
"""Collect an iterable of Results into a single Result.
|
|
124
|
+
|
|
125
|
+
If all results are Ok, returns Ok with list of values.
|
|
126
|
+
If any result is Err, returns the first Err encountered.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
results: Iterable of Result objects.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Ok(list[T]) if all are Ok, otherwise first Err.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> collect_results([Ok(1), Ok(2), Ok(3)])
|
|
136
|
+
Ok([1, 2, 3])
|
|
137
|
+
>>> collect_results([Ok(1), Err("fail"), Ok(3)])
|
|
138
|
+
Err("fail")
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
values: list[T] = []
|
|
142
|
+
for result in results:
|
|
143
|
+
match result:
|
|
144
|
+
case Ok(value):
|
|
145
|
+
values.append(value)
|
|
146
|
+
case Err() as err:
|
|
147
|
+
return err
|
|
148
|
+
return Ok(values)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def unwrap_or(result: Result[T, E], default: T) -> T:
|
|
152
|
+
"""Extract value from Result or return default.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
result: The Result to unwrap.
|
|
156
|
+
default: Default value if result is Err.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
The Ok value or the default.
|
|
160
|
+
|
|
161
|
+
Example:
|
|
162
|
+
>>> unwrap_or(Ok(42), 0)
|
|
163
|
+
42
|
|
164
|
+
>>> unwrap_or(Err("error"), 0)
|
|
165
|
+
0
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
match result:
|
|
169
|
+
case Ok(value):
|
|
170
|
+
return value
|
|
171
|
+
case Err():
|
|
172
|
+
return default
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def unwrap_or_else(
|
|
176
|
+
result: Result[T, E],
|
|
177
|
+
default_fn: Callable[[E], T],
|
|
178
|
+
) -> T:
|
|
179
|
+
"""Extract value from Result or compute default from error.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
result: The Result to unwrap.
|
|
183
|
+
default_fn: Function to compute default from error.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
The Ok value or computed default.
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
>>> unwrap_or_else(Ok(42), lambda e: 0)
|
|
190
|
+
42
|
|
191
|
+
>>> unwrap_or_else(Err(ValueError("x")), lambda e: len(str(e)))
|
|
192
|
+
1
|
|
193
|
+
|
|
194
|
+
"""
|
|
195
|
+
match result:
|
|
196
|
+
case Ok(value):
|
|
197
|
+
return value
|
|
198
|
+
case Err(error):
|
|
199
|
+
return default_fn(error)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Security package for runtime protection."""
|
|
2
|
+
|
|
3
|
+
from taipanstack.security.decorators import (
|
|
4
|
+
OperationTimeoutError,
|
|
5
|
+
ValidationError,
|
|
6
|
+
deprecated,
|
|
7
|
+
guard_exceptions,
|
|
8
|
+
require_type,
|
|
9
|
+
timeout,
|
|
10
|
+
validate_inputs,
|
|
11
|
+
)
|
|
12
|
+
from taipanstack.security.guards import (
|
|
13
|
+
SecurityError,
|
|
14
|
+
guard_command_injection,
|
|
15
|
+
guard_env_variable,
|
|
16
|
+
guard_file_extension,
|
|
17
|
+
guard_path_traversal,
|
|
18
|
+
)
|
|
19
|
+
from taipanstack.security.sanitizers import (
|
|
20
|
+
sanitize_filename,
|
|
21
|
+
sanitize_path,
|
|
22
|
+
sanitize_string,
|
|
23
|
+
)
|
|
24
|
+
from taipanstack.security.validators import (
|
|
25
|
+
validate_email,
|
|
26
|
+
validate_project_name,
|
|
27
|
+
validate_python_version,
|
|
28
|
+
validate_url,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
# Decorators
|
|
33
|
+
"OperationTimeoutError",
|
|
34
|
+
# Guards
|
|
35
|
+
"SecurityError",
|
|
36
|
+
"ValidationError",
|
|
37
|
+
"deprecated",
|
|
38
|
+
"guard_command_injection",
|
|
39
|
+
"guard_env_variable",
|
|
40
|
+
"guard_exceptions",
|
|
41
|
+
"guard_file_extension",
|
|
42
|
+
"guard_path_traversal",
|
|
43
|
+
"require_type",
|
|
44
|
+
# Sanitizers
|
|
45
|
+
"sanitize_filename",
|
|
46
|
+
"sanitize_path",
|
|
47
|
+
"sanitize_string",
|
|
48
|
+
"timeout",
|
|
49
|
+
# Validators
|
|
50
|
+
"validate_email",
|
|
51
|
+
"validate_inputs",
|
|
52
|
+
"validate_project_name",
|
|
53
|
+
"validate_python_version",
|
|
54
|
+
"validate_url",
|
|
55
|
+
]
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security decorators for robust Python applications.
|
|
3
|
+
|
|
4
|
+
Provides decorators for input validation, exception handling,
|
|
5
|
+
timeout control, and other security patterns. Compatible with
|
|
6
|
+
any Python framework (Flask, FastAPI, Django, etc.).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
import signal
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
from collections.abc import Callable, Mapping
|
|
16
|
+
from typing import Any, ParamSpec, TypeVar
|
|
17
|
+
|
|
18
|
+
from taipanstack.security.guards import SecurityError
|
|
19
|
+
|
|
20
|
+
P = ParamSpec("P")
|
|
21
|
+
R = TypeVar("R")
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OperationTimeoutError(Exception):
|
|
26
|
+
"""Raised when a function exceeds its timeout limit."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, seconds: float, func_name: str = "function") -> None:
|
|
29
|
+
"""Initialize OperationTimeoutError.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
seconds: The timeout that was exceeded.
|
|
33
|
+
func_name: Name of the function that timed out.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
self.seconds = seconds
|
|
37
|
+
self.func_name = func_name
|
|
38
|
+
super().__init__(f"{func_name} timed out after {seconds} seconds")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ValidationError(Exception):
|
|
42
|
+
"""Raised when input validation fails."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
message: str,
|
|
47
|
+
param_name: str | None = None,
|
|
48
|
+
value: Any = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Initialize ValidationError.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
message: Description of the validation failure.
|
|
54
|
+
param_name: Name of the parameter that failed.
|
|
55
|
+
value: The invalid value (sanitized).
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
self.param_name = param_name
|
|
59
|
+
self.value = value
|
|
60
|
+
super().__init__(message)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def validate_inputs(
|
|
64
|
+
**validators: Callable[[Any], Any],
|
|
65
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
66
|
+
"""Decorator to validate function inputs.
|
|
67
|
+
|
|
68
|
+
Validates function arguments using provided validator functions.
|
|
69
|
+
Validators should raise ValueError or ValidationError on invalid input.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
**validators: Mapping of parameter names to validator functions.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Decorated function with input validation.
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
>>> from taipanstack.security.validators import validate_email, validate_port
|
|
79
|
+
>>> @validate_inputs(email=validate_email, port=validate_port)
|
|
80
|
+
... def connect(email: str, port: int) -> None:
|
|
81
|
+
... pass
|
|
82
|
+
>>> connect(email="invalid", port=8080)
|
|
83
|
+
ValidationError: Invalid email format: invalid
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
88
|
+
@functools.wraps(func)
|
|
89
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
90
|
+
# Get function's parameter names
|
|
91
|
+
import inspect
|
|
92
|
+
|
|
93
|
+
sig = inspect.signature(func)
|
|
94
|
+
bound = sig.bind(*args, **kwargs)
|
|
95
|
+
bound.apply_defaults()
|
|
96
|
+
|
|
97
|
+
# Validate each parameter that has a validator
|
|
98
|
+
for param_name, validator in validators.items():
|
|
99
|
+
if param_name in bound.arguments:
|
|
100
|
+
value = bound.arguments[param_name]
|
|
101
|
+
try:
|
|
102
|
+
# Call validator - it should raise on invalid input
|
|
103
|
+
validated = validator(value)
|
|
104
|
+
# Update to validated value if returned
|
|
105
|
+
if validated is not None:
|
|
106
|
+
bound.arguments[param_name] = validated
|
|
107
|
+
except (ValueError, TypeError) as e:
|
|
108
|
+
raise ValidationError(
|
|
109
|
+
str(e),
|
|
110
|
+
param_name=param_name,
|
|
111
|
+
value=repr(value)[:100],
|
|
112
|
+
) from e
|
|
113
|
+
|
|
114
|
+
# Call original function with validated arguments
|
|
115
|
+
return func(*bound.args, **bound.kwargs)
|
|
116
|
+
|
|
117
|
+
return wrapper
|
|
118
|
+
|
|
119
|
+
return decorator
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def guard_exceptions(
|
|
123
|
+
*,
|
|
124
|
+
catch: tuple[type[Exception], ...] = (Exception,),
|
|
125
|
+
reraise_as: type[Exception] | None = None,
|
|
126
|
+
default: Any = None,
|
|
127
|
+
log_errors: bool = True,
|
|
128
|
+
) -> Callable[[Callable[P, R]], Callable[P, R | Any]]:
|
|
129
|
+
"""Decorator to safely handle exceptions.
|
|
130
|
+
|
|
131
|
+
Catches exceptions and optionally re-raises as a different type
|
|
132
|
+
or returns a default value.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
catch: Exception types to catch.
|
|
136
|
+
reraise_as: Exception type to re-raise as (None = don't reraise).
|
|
137
|
+
default: Default value to return if exception caught and not reraised.
|
|
138
|
+
log_errors: Whether to log caught exceptions.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Decorated function with exception handling.
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
>>> @guard_exceptions(catch=(IOError,), reraise_as=SecurityError)
|
|
145
|
+
... def read_file(path: str) -> str:
|
|
146
|
+
... return open(path).read()
|
|
147
|
+
>>> read_file("/nonexistent")
|
|
148
|
+
SecurityError: [guard_exceptions] ...
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R | Any]:
|
|
153
|
+
@functools.wraps(func)
|
|
154
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | Any:
|
|
155
|
+
try:
|
|
156
|
+
return func(*args, **kwargs)
|
|
157
|
+
except catch as e:
|
|
158
|
+
if log_errors:
|
|
159
|
+
import logging
|
|
160
|
+
|
|
161
|
+
logging.getLogger("taipanstack.security").warning(
|
|
162
|
+
"Exception caught in %s: %s",
|
|
163
|
+
func.__name__,
|
|
164
|
+
str(e),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if reraise_as is not None:
|
|
168
|
+
if reraise_as == SecurityError:
|
|
169
|
+
raise SecurityError(
|
|
170
|
+
str(e),
|
|
171
|
+
guard_name="guard_exceptions",
|
|
172
|
+
) from e
|
|
173
|
+
raise reraise_as(str(e)) from e
|
|
174
|
+
|
|
175
|
+
return default
|
|
176
|
+
|
|
177
|
+
return wrapper
|
|
178
|
+
|
|
179
|
+
return decorator
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def timeout(
|
|
183
|
+
seconds: float,
|
|
184
|
+
*,
|
|
185
|
+
use_signal: bool = True,
|
|
186
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
187
|
+
"""Decorator to limit function execution time.
|
|
188
|
+
|
|
189
|
+
Uses signal-based timeout on Unix or thread-based on Windows.
|
|
190
|
+
Signal-based is more reliable but only works in main thread.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
seconds: Maximum execution time in seconds.
|
|
194
|
+
use_signal: Use signal-based timeout (Unix only, main thread only).
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Decorated function with timeout.
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
>>> @timeout(5.0)
|
|
201
|
+
... def slow_operation() -> str:
|
|
202
|
+
... import time
|
|
203
|
+
... time.sleep(10)
|
|
204
|
+
... return "done"
|
|
205
|
+
>>> slow_operation()
|
|
206
|
+
TimeoutError: slow_operation timed out after 5.0 seconds
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
211
|
+
@functools.wraps(func)
|
|
212
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
213
|
+
# Determine if we can use signals
|
|
214
|
+
can_use_signal = (
|
|
215
|
+
use_signal
|
|
216
|
+
and sys.platform != "win32"
|
|
217
|
+
and threading.current_thread() is threading.main_thread()
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if can_use_signal:
|
|
221
|
+
return _timeout_with_signal(func, seconds, args, kwargs)
|
|
222
|
+
return _timeout_with_thread(func, seconds, args, kwargs)
|
|
223
|
+
|
|
224
|
+
return wrapper
|
|
225
|
+
|
|
226
|
+
return decorator
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _timeout_with_signal(
|
|
230
|
+
func: Callable[P, R],
|
|
231
|
+
seconds: float,
|
|
232
|
+
args: tuple[Any, ...],
|
|
233
|
+
kwargs: Mapping[str, Any],
|
|
234
|
+
) -> R:
|
|
235
|
+
"""Implement timeout using Unix signals."""
|
|
236
|
+
|
|
237
|
+
def handler(signum: int, frame: Any) -> None:
|
|
238
|
+
raise OperationTimeoutError(seconds, func.__name__)
|
|
239
|
+
|
|
240
|
+
# Set up signal handler
|
|
241
|
+
old_handler = signal.signal(signal.SIGALRM, handler)
|
|
242
|
+
signal.setitimer(signal.ITIMER_REAL, seconds)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
return func(*args, **kwargs)
|
|
246
|
+
finally:
|
|
247
|
+
# Restore old handler and cancel alarm
|
|
248
|
+
signal.setitimer(signal.ITIMER_REAL, 0)
|
|
249
|
+
signal.signal(signal.SIGALRM, old_handler)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _timeout_with_thread(
|
|
253
|
+
func: Callable[P, R],
|
|
254
|
+
seconds: float,
|
|
255
|
+
args: tuple[Any, ...],
|
|
256
|
+
kwargs: Mapping[str, Any],
|
|
257
|
+
) -> R:
|
|
258
|
+
"""Implement timeout using a separate thread."""
|
|
259
|
+
result: list[R] = []
|
|
260
|
+
exception: list[Exception] = []
|
|
261
|
+
|
|
262
|
+
def target() -> None:
|
|
263
|
+
try:
|
|
264
|
+
result.append(func(*args, **kwargs))
|
|
265
|
+
except Exception as e:
|
|
266
|
+
exception.append(e)
|
|
267
|
+
|
|
268
|
+
thread = threading.Thread(target=target)
|
|
269
|
+
thread.daemon = True
|
|
270
|
+
thread.start()
|
|
271
|
+
thread.join(timeout=seconds)
|
|
272
|
+
|
|
273
|
+
if thread.is_alive():
|
|
274
|
+
# Thread still running - timeout occurred
|
|
275
|
+
raise OperationTimeoutError(seconds, func.__name__)
|
|
276
|
+
|
|
277
|
+
if exception:
|
|
278
|
+
raise exception[0]
|
|
279
|
+
|
|
280
|
+
return result[0]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def deprecated(
|
|
284
|
+
message: str = "",
|
|
285
|
+
*,
|
|
286
|
+
removal_version: str | None = None,
|
|
287
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
288
|
+
"""Mark a function as deprecated.
|
|
289
|
+
|
|
290
|
+
Emits a warning when the decorated function is called.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
message: Additional deprecation message.
|
|
294
|
+
removal_version: Version when function will be removed.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Decorated function that warns on use.
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
>>> @deprecated("Use new_function instead", removal_version="2.0")
|
|
301
|
+
... def old_function() -> None:
|
|
302
|
+
... pass
|
|
303
|
+
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
307
|
+
@functools.wraps(func)
|
|
308
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
309
|
+
import warnings
|
|
310
|
+
|
|
311
|
+
msg = f"{func.__name__} is deprecated."
|
|
312
|
+
if removal_version:
|
|
313
|
+
msg += f" Will be removed in version {removal_version}."
|
|
314
|
+
if message:
|
|
315
|
+
msg += f" {message}"
|
|
316
|
+
|
|
317
|
+
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
318
|
+
return func(*args, **kwargs)
|
|
319
|
+
|
|
320
|
+
return wrapper
|
|
321
|
+
|
|
322
|
+
return decorator
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def require_type(
|
|
326
|
+
**type_hints: type,
|
|
327
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
328
|
+
"""Decorator to enforce runtime type checking.
|
|
329
|
+
|
|
330
|
+
Validates that arguments match specified types at runtime.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
**type_hints: Mapping of parameter names to expected types.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Decorated function with type checking.
|
|
337
|
+
|
|
338
|
+
Example:
|
|
339
|
+
>>> @require_type(name=str, count=int)
|
|
340
|
+
... def greet(name: str, count: int) -> None:
|
|
341
|
+
... print(f"Hello {name}" * count)
|
|
342
|
+
>>> greet(name=123, count=2)
|
|
343
|
+
TypeError: Parameter 'name' expected str, got int
|
|
344
|
+
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
348
|
+
@functools.wraps(func)
|
|
349
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
350
|
+
import inspect
|
|
351
|
+
|
|
352
|
+
sig = inspect.signature(func)
|
|
353
|
+
bound = sig.bind(*args, **kwargs)
|
|
354
|
+
bound.apply_defaults()
|
|
355
|
+
|
|
356
|
+
for param_name, expected_type in type_hints.items():
|
|
357
|
+
if param_name in bound.arguments:
|
|
358
|
+
value = bound.arguments[param_name]
|
|
359
|
+
if not isinstance(value, expected_type):
|
|
360
|
+
raise TypeError(
|
|
361
|
+
f"Parameter '{param_name}' expected "
|
|
362
|
+
f"{expected_type.__name__}, got {type(value).__name__}"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return func(*bound.args, **bound.kwargs)
|
|
366
|
+
|
|
367
|
+
return wrapper
|
|
368
|
+
|
|
369
|
+
return decorator
|