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.
@@ -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