provide-foundation 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev2__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 (93) hide show
  1. provide/foundation/__init__.py +29 -3
  2. provide/foundation/archive/operations.py +4 -6
  3. provide/foundation/cli/__init__.py +2 -2
  4. provide/foundation/cli/commands/deps.py +13 -7
  5. provide/foundation/cli/commands/logs/__init__.py +1 -1
  6. provide/foundation/cli/commands/logs/query.py +1 -1
  7. provide/foundation/cli/commands/logs/send.py +1 -1
  8. provide/foundation/cli/commands/logs/tail.py +1 -1
  9. provide/foundation/cli/decorators.py +11 -10
  10. provide/foundation/cli/main.py +1 -1
  11. provide/foundation/cli/testing.py +2 -35
  12. provide/foundation/cli/utils.py +21 -17
  13. provide/foundation/config/__init__.py +35 -2
  14. provide/foundation/config/converters.py +479 -0
  15. provide/foundation/config/defaults.py +67 -0
  16. provide/foundation/config/env.py +4 -19
  17. provide/foundation/config/loader.py +9 -3
  18. provide/foundation/console/input.py +5 -5
  19. provide/foundation/console/output.py +35 -13
  20. provide/foundation/context/__init__.py +8 -4
  21. provide/foundation/context/core.py +85 -109
  22. provide/foundation/crypto/certificates/operations.py +1 -1
  23. provide/foundation/errors/__init__.py +2 -3
  24. provide/foundation/errors/decorators.py +0 -231
  25. provide/foundation/errors/types.py +0 -97
  26. provide/foundation/file/directory.py +13 -22
  27. provide/foundation/file/lock.py +3 -1
  28. provide/foundation/hub/components.py +72 -384
  29. provide/foundation/hub/config.py +151 -0
  30. provide/foundation/hub/discovery.py +62 -0
  31. provide/foundation/hub/handlers.py +81 -0
  32. provide/foundation/hub/lifecycle.py +194 -0
  33. provide/foundation/hub/manager.py +4 -4
  34. provide/foundation/hub/processors.py +44 -0
  35. provide/foundation/integrations/__init__.py +11 -0
  36. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  37. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  38. provide/foundation/{observability → integrations}/openobserve/client.py +12 -12
  39. provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
  40. provide/foundation/integrations/openobserve/config.py +37 -0
  41. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  42. provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
  43. provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
  44. provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
  45. provide/foundation/logger/config/logging.py +68 -298
  46. provide/foundation/logger/config/telemetry.py +41 -121
  47. provide/foundation/logger/setup/coordinator.py +1 -1
  48. provide/foundation/observability/__init__.py +2 -2
  49. provide/foundation/process/__init__.py +9 -0
  50. provide/foundation/process/exit.py +47 -0
  51. provide/foundation/process/lifecycle.py +33 -33
  52. provide/foundation/resilience/__init__.py +35 -0
  53. provide/foundation/resilience/circuit.py +164 -0
  54. provide/foundation/resilience/decorators.py +220 -0
  55. provide/foundation/resilience/fallback.py +193 -0
  56. provide/foundation/resilience/retry.py +325 -0
  57. provide/foundation/streams/config.py +79 -0
  58. provide/foundation/streams/console.py +7 -8
  59. provide/foundation/streams/core.py +6 -3
  60. provide/foundation/streams/file.py +12 -2
  61. provide/foundation/testing/__init__.py +7 -2
  62. provide/foundation/testing/cli.py +30 -17
  63. provide/foundation/testing/common/__init__.py +0 -2
  64. provide/foundation/testing/common/fixtures.py +0 -27
  65. provide/foundation/testing/file/content_fixtures.py +316 -0
  66. provide/foundation/testing/file/directory_fixtures.py +107 -0
  67. provide/foundation/testing/file/fixtures.py +45 -516
  68. provide/foundation/testing/file/special_fixtures.py +153 -0
  69. provide/foundation/testing/logger.py +76 -0
  70. provide/foundation/testing/process/async_fixtures.py +405 -0
  71. provide/foundation/testing/process/fixtures.py +50 -571
  72. provide/foundation/testing/process/subprocess_fixtures.py +209 -0
  73. provide/foundation/testing/threading/basic_fixtures.py +101 -0
  74. provide/foundation/testing/threading/data_fixtures.py +99 -0
  75. provide/foundation/testing/threading/execution_fixtures.py +263 -0
  76. provide/foundation/testing/threading/fixtures.py +34 -500
  77. provide/foundation/testing/threading/sync_fixtures.py +97 -0
  78. provide/foundation/testing/time/fixtures.py +4 -4
  79. provide/foundation/tools/cache.py +8 -6
  80. provide/foundation/tools/downloader.py +23 -12
  81. provide/foundation/tracer/spans.py +2 -2
  82. provide/foundation/transport/config.py +26 -95
  83. provide/foundation/transport/middleware.py +30 -36
  84. provide/foundation/utils/deps.py +14 -12
  85. provide/foundation/utils/parsing.py +49 -4
  86. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +1 -1
  87. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/RECORD +93 -68
  88. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  89. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  90. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
  91. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
  92. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
  93. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,209 @@
1
+ """
2
+ Subprocess-specific test fixtures for process testing.
3
+
4
+ Provides fixtures for mocking and testing subprocess operations,
5
+ stream handling, and process communication.
6
+ """
7
+
8
+ import asyncio
9
+ from unittest.mock import AsyncMock, Mock
10
+
11
+ import pytest
12
+
13
+
14
+ @pytest.fixture
15
+ def mock_async_process() -> AsyncMock:
16
+ """
17
+ Mock async subprocess for testing.
18
+
19
+ Returns:
20
+ AsyncMock configured as a subprocess with common attributes.
21
+ """
22
+ mock_process = AsyncMock()
23
+ mock_process.communicate = AsyncMock(return_value=(b"output", b""))
24
+ mock_process.returncode = 0
25
+ mock_process.pid = 12345
26
+ mock_process.stdin = AsyncMock()
27
+ mock_process.stdout = AsyncMock()
28
+ mock_process.stderr = AsyncMock()
29
+ mock_process.wait = AsyncMock(return_value=0)
30
+ mock_process.kill = Mock()
31
+ mock_process.terminate = Mock()
32
+
33
+ return mock_process
34
+
35
+
36
+ @pytest.fixture
37
+ async def async_stream_reader() -> AsyncMock:
38
+ """
39
+ Mock async stream reader for subprocess stdout/stderr.
40
+
41
+ Returns:
42
+ AsyncMock configured as a stream reader.
43
+ """
44
+ reader = AsyncMock()
45
+
46
+ # Simulate reading lines
47
+ async def readline_side_effect():
48
+ for line in [b"line1\n", b"line2\n", b""]:
49
+ yield line
50
+
51
+ reader.readline = AsyncMock(side_effect=readline_side_effect().__anext__)
52
+ reader.read = AsyncMock(return_value=b"full content")
53
+ reader.at_eof = Mock(side_effect=[False, False, True])
54
+
55
+ return reader
56
+
57
+
58
+ @pytest.fixture
59
+ def async_subprocess():
60
+ """
61
+ Create mock async subprocess for testing.
62
+
63
+ Returns:
64
+ Function that creates mock subprocess with configurable behavior.
65
+ """
66
+ def _create_subprocess(
67
+ returncode: int = 0,
68
+ stdout: bytes = b"",
69
+ stderr: bytes = b"",
70
+ pid: int = 12345
71
+ ) -> AsyncMock:
72
+ """
73
+ Create a mock async subprocess.
74
+
75
+ Args:
76
+ returncode: Process return code
77
+ stdout: Process stdout output
78
+ stderr: Process stderr output
79
+ pid: Process ID
80
+
81
+ Returns:
82
+ AsyncMock configured as subprocess
83
+ """
84
+ process = AsyncMock()
85
+ process.returncode = returncode
86
+ process.pid = pid
87
+ process.communicate = AsyncMock(return_value=(stdout, stderr))
88
+ process.wait = AsyncMock(return_value=returncode)
89
+ process.kill = Mock()
90
+ process.terminate = Mock()
91
+ process.send_signal = Mock()
92
+
93
+ # Add stdout/stderr as async stream readers
94
+ process.stdout = AsyncMock()
95
+ process.stdout.read = AsyncMock(return_value=stdout)
96
+ process.stdout.readline = AsyncMock(side_effect=[stdout, b""])
97
+ process.stdout.at_eof = Mock(side_effect=[False, True])
98
+
99
+ process.stderr = AsyncMock()
100
+ process.stderr.read = AsyncMock(return_value=stderr)
101
+ process.stderr.readline = AsyncMock(side_effect=[stderr, b""])
102
+ process.stderr.at_eof = Mock(side_effect=[False, True])
103
+
104
+ process.stdin = AsyncMock()
105
+ process.stdin.write = AsyncMock()
106
+ process.stdin.drain = AsyncMock()
107
+ process.stdin.close = Mock()
108
+
109
+ return process
110
+
111
+ return _create_subprocess
112
+
113
+
114
+ @pytest.fixture
115
+ def async_mock_server():
116
+ """
117
+ Create a mock async server for testing.
118
+
119
+ Returns:
120
+ Mock server with async methods.
121
+ """
122
+ class AsyncMockServer:
123
+ def __init__(self):
124
+ self.started = False
125
+ self.connections = []
126
+ self.requests = []
127
+
128
+ async def start(self, host: str = "localhost", port: int = 8080):
129
+ """Start the mock server."""
130
+ self.started = True
131
+ self.host = host
132
+ self.port = port
133
+
134
+ async def stop(self):
135
+ """Stop the mock server."""
136
+ self.started = False
137
+ for conn in self.connections:
138
+ await conn.close()
139
+
140
+ async def handle_connection(self, reader, writer):
141
+ """Mock connection handler."""
142
+ conn = {"reader": reader, "writer": writer}
143
+ self.connections.append(conn)
144
+
145
+ # Mock reading request
146
+ data = await reader.read(1024)
147
+ self.requests.append(data)
148
+
149
+ # Mock sending response
150
+ writer.write(b"HTTP/1.1 200 OK\r\n\r\nOK")
151
+ await writer.drain()
152
+
153
+ writer.close()
154
+ await writer.wait_closed()
155
+
156
+ def get_url(self) -> str:
157
+ """Get server URL."""
158
+ return f"http://{self.host}:{self.port}"
159
+
160
+ return AsyncMockServer()
161
+
162
+
163
+ @pytest.fixture
164
+ def async_test_client():
165
+ """
166
+ Create an async HTTP test client.
167
+
168
+ Returns:
169
+ Mock async HTTP client for testing.
170
+ """
171
+ class AsyncTestClient:
172
+ def __init__(self):
173
+ self.responses = {}
174
+ self.requests = []
175
+
176
+ def set_response(self, url: str, response: dict):
177
+ """Set a mock response for a URL."""
178
+ self.responses[url] = response
179
+
180
+ async def get(self, url: str, **kwargs) -> dict:
181
+ """Mock GET request."""
182
+ self.requests.append({"method": "GET", "url": url, "kwargs": kwargs})
183
+ return self.responses.get(url, {"status": 404, "body": "Not Found"})
184
+
185
+ async def post(self, url: str, data=None, **kwargs) -> dict:
186
+ """Mock POST request."""
187
+ self.requests.append({"method": "POST", "url": url, "data": data, "kwargs": kwargs})
188
+ return self.responses.get(url, {"status": 200, "body": "OK"})
189
+
190
+ async def close(self):
191
+ """Close the client."""
192
+ pass
193
+
194
+ async def __aenter__(self):
195
+ return self
196
+
197
+ async def __aexit__(self, *args):
198
+ await self.close()
199
+
200
+ return AsyncTestClient()
201
+
202
+
203
+ __all__ = [
204
+ "mock_async_process",
205
+ "async_stream_reader",
206
+ "async_subprocess",
207
+ "async_mock_server",
208
+ "async_test_client",
209
+ ]
@@ -0,0 +1,101 @@
1
+ """
2
+ Basic threading test fixtures.
3
+
4
+ Core fixtures for creating threads, thread pools, mocks, and thread-local storage.
5
+ """
6
+
7
+ import threading
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ from typing import Any, Callable
10
+ from unittest.mock import Mock
11
+
12
+ import pytest
13
+
14
+
15
+ @pytest.fixture
16
+ def test_thread():
17
+ """
18
+ Create a test thread with automatic cleanup.
19
+
20
+ Returns:
21
+ Function to create and manage test threads.
22
+ """
23
+ threads = []
24
+
25
+ def _create_thread(target: Callable, args: tuple = (), kwargs: dict = None, daemon: bool = True) -> threading.Thread:
26
+ """
27
+ Create a test thread.
28
+
29
+ Args:
30
+ target: Function to run in thread
31
+ args: Positional arguments for target
32
+ kwargs: Keyword arguments for target
33
+ daemon: Whether thread should be daemon
34
+
35
+ Returns:
36
+ Started thread instance
37
+ """
38
+ kwargs = kwargs or {}
39
+ thread = threading.Thread(target=target, args=args, kwargs=kwargs, daemon=daemon)
40
+ threads.append(thread)
41
+ thread.start()
42
+ return thread
43
+
44
+ yield _create_thread
45
+
46
+ # Cleanup: wait for all threads to complete
47
+ for thread in threads:
48
+ if thread.is_alive():
49
+ thread.join(timeout=1.0)
50
+
51
+
52
+ @pytest.fixture
53
+ def thread_pool():
54
+ """
55
+ Create a thread pool executor for testing.
56
+
57
+ Returns:
58
+ ThreadPoolExecutor instance with automatic cleanup.
59
+ """
60
+ executor = ThreadPoolExecutor(max_workers=4)
61
+ yield executor
62
+ executor.shutdown(wait=True, cancel_futures=True)
63
+
64
+
65
+ @pytest.fixture
66
+ def mock_thread():
67
+ """
68
+ Create a mock thread for testing without actual threading.
69
+
70
+ Returns:
71
+ Mock thread object.
72
+ """
73
+ mock = Mock(spec=threading.Thread)
74
+ mock.is_alive.return_value = False
75
+ mock.daemon = False
76
+ mock.name = "MockThread"
77
+ mock.ident = 12345
78
+ mock.start = Mock()
79
+ mock.join = Mock()
80
+ mock.run = Mock()
81
+
82
+ return mock
83
+
84
+
85
+ @pytest.fixture
86
+ def thread_local_storage():
87
+ """
88
+ Create thread-local storage for testing.
89
+
90
+ Returns:
91
+ Thread-local storage object.
92
+ """
93
+ return threading.local()
94
+
95
+
96
+ __all__ = [
97
+ "test_thread",
98
+ "thread_pool",
99
+ "mock_thread",
100
+ "thread_local_storage",
101
+ ]
@@ -0,0 +1,99 @@
1
+ """
2
+ Thread-safe data structure test fixtures.
3
+
4
+ Fixtures for thread-safe lists, counters, and other data structures for testing.
5
+ """
6
+
7
+ import threading
8
+ from typing import Any
9
+
10
+ import pytest
11
+
12
+
13
+ @pytest.fixture
14
+ def thread_safe_list():
15
+ """
16
+ Create a thread-safe list for collecting results.
17
+
18
+ Returns:
19
+ Thread-safe list implementation.
20
+ """
21
+ class ThreadSafeList:
22
+ def __init__(self):
23
+ self._list = []
24
+ self._lock = threading.Lock()
25
+
26
+ def append(self, item: Any):
27
+ """Thread-safe append."""
28
+ with self._lock:
29
+ self._list.append(item)
30
+
31
+ def extend(self, items):
32
+ """Thread-safe extend."""
33
+ with self._lock:
34
+ self._list.extend(items)
35
+
36
+ def get_all(self) -> list:
37
+ """Get copy of all items."""
38
+ with self._lock:
39
+ return self._list.copy()
40
+
41
+ def clear(self):
42
+ """Clear the list."""
43
+ with self._lock:
44
+ self._list.clear()
45
+
46
+ def __len__(self) -> int:
47
+ with self._lock:
48
+ return len(self._list)
49
+
50
+ def __getitem__(self, index):
51
+ with self._lock:
52
+ return self._list[index]
53
+
54
+ return ThreadSafeList()
55
+
56
+
57
+ @pytest.fixture
58
+ def thread_safe_counter():
59
+ """
60
+ Create a thread-safe counter.
61
+
62
+ Returns:
63
+ Thread-safe counter implementation.
64
+ """
65
+ class ThreadSafeCounter:
66
+ def __init__(self, initial: int = 0):
67
+ self._value = initial
68
+ self._lock = threading.Lock()
69
+
70
+ def increment(self, amount: int = 1) -> int:
71
+ """Thread-safe increment."""
72
+ with self._lock:
73
+ self._value += amount
74
+ return self._value
75
+
76
+ def decrement(self, amount: int = 1) -> int:
77
+ """Thread-safe decrement."""
78
+ with self._lock:
79
+ self._value -= amount
80
+ return self._value
81
+
82
+ @property
83
+ def value(self) -> int:
84
+ """Get current value."""
85
+ with self._lock:
86
+ return self._value
87
+
88
+ def reset(self, value: int = 0):
89
+ """Reset counter."""
90
+ with self._lock:
91
+ self._value = value
92
+
93
+ return ThreadSafeCounter()
94
+
95
+
96
+ __all__ = [
97
+ "thread_safe_list",
98
+ "thread_safe_counter",
99
+ ]
@@ -0,0 +1,263 @@
1
+ """
2
+ Thread execution and testing helper fixtures.
3
+
4
+ Advanced fixtures for concurrent execution, synchronization testing, deadlock detection,
5
+ and exception handling in threaded code.
6
+ """
7
+
8
+ import threading
9
+ import time
10
+ from typing import Any, Callable, Optional
11
+ from concurrent.futures import ThreadPoolExecutor
12
+
13
+ import pytest
14
+
15
+
16
+ @pytest.fixture
17
+ def concurrent_executor():
18
+ """
19
+ Helper for executing functions concurrently in tests.
20
+
21
+ Returns:
22
+ Concurrent execution helper.
23
+ """
24
+ class ConcurrentExecutor:
25
+ def __init__(self):
26
+ self.results = []
27
+ self.exceptions = []
28
+
29
+ def run_concurrent(self, func: Callable, args_list: list[tuple], max_workers: int = 4) -> list[Any]:
30
+ """
31
+ Run function concurrently with different arguments.
32
+
33
+ Args:
34
+ func: Function to execute
35
+ args_list: List of argument tuples
36
+ max_workers: Maximum concurrent workers
37
+
38
+ Returns:
39
+ List of results in order
40
+ """
41
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
42
+ futures = []
43
+ for args in args_list:
44
+ if isinstance(args, tuple):
45
+ future = executor.submit(func, *args)
46
+ else:
47
+ future = executor.submit(func, args)
48
+ futures.append(future)
49
+
50
+ results = []
51
+ for future in futures:
52
+ try:
53
+ result = future.result(timeout=10)
54
+ results.append(result)
55
+ self.results.append(result)
56
+ except Exception as e:
57
+ self.exceptions.append(e)
58
+ results.append(None)
59
+
60
+ return results
61
+
62
+ def run_parallel(self, funcs: list[Callable], timeout: float = 10) -> list[Any]:
63
+ """
64
+ Run different functions in parallel.
65
+
66
+ Args:
67
+ funcs: List of functions to execute
68
+ timeout: Timeout for each function
69
+
70
+ Returns:
71
+ List of results
72
+ """
73
+ with ThreadPoolExecutor(max_workers=len(funcs)) as executor:
74
+ futures = [executor.submit(func) for func in funcs]
75
+ results = []
76
+
77
+ for future in futures:
78
+ try:
79
+ result = future.result(timeout=timeout)
80
+ results.append(result)
81
+ except Exception as e:
82
+ self.exceptions.append(e)
83
+ results.append(None)
84
+
85
+ return results
86
+
87
+ return ConcurrentExecutor()
88
+
89
+
90
+ @pytest.fixture
91
+ def thread_synchronizer():
92
+ """
93
+ Helper for synchronizing test threads.
94
+
95
+ Returns:
96
+ Thread synchronization helper.
97
+ """
98
+ class ThreadSynchronizer:
99
+ def __init__(self):
100
+ self.checkpoints = {}
101
+
102
+ def checkpoint(self, name: str, thread_id: Optional[int] = None):
103
+ """
104
+ Record that a thread reached a checkpoint.
105
+
106
+ Args:
107
+ name: Checkpoint name
108
+ thread_id: Optional thread ID (uses current if None)
109
+ """
110
+ thread_id = thread_id or threading.get_ident()
111
+ if name not in self.checkpoints:
112
+ self.checkpoints[name] = []
113
+ self.checkpoints[name].append((thread_id, time.time()))
114
+
115
+ def wait_for_checkpoint(self, name: str, count: int, timeout: float = 5.0) -> bool:
116
+ """
117
+ Wait for N threads to reach a checkpoint.
118
+
119
+ Args:
120
+ name: Checkpoint name
121
+ count: Number of threads to wait for
122
+ timeout: Maximum wait time
123
+
124
+ Returns:
125
+ True if checkpoint reached, False if timeout
126
+ """
127
+ start = time.time()
128
+ while time.time() - start < timeout:
129
+ if name in self.checkpoints and len(self.checkpoints[name]) >= count:
130
+ return True
131
+ time.sleep(0.01)
132
+ return False
133
+
134
+ def get_order(self, checkpoint: str) -> list[int]:
135
+ """
136
+ Get order in which threads reached checkpoint.
137
+
138
+ Args:
139
+ checkpoint: Checkpoint name
140
+
141
+ Returns:
142
+ List of thread IDs in order
143
+ """
144
+ if checkpoint not in self.checkpoints:
145
+ return []
146
+ return [tid for tid, _ in sorted(self.checkpoints[checkpoint], key=lambda x: x[1])]
147
+
148
+ def clear(self):
149
+ """Clear all checkpoints."""
150
+ self.checkpoints.clear()
151
+
152
+ return ThreadSynchronizer()
153
+
154
+
155
+ @pytest.fixture
156
+ def deadlock_detector():
157
+ """
158
+ Helper for detecting potential deadlocks in tests.
159
+
160
+ Returns:
161
+ Deadlock detection helper.
162
+ """
163
+ class DeadlockDetector:
164
+ def __init__(self):
165
+ self.locks_held = {} # thread_id -> set of locks
166
+ self.lock = threading.Lock()
167
+
168
+ def acquire(self, lock_name: str, thread_id: Optional[int] = None):
169
+ """Record lock acquisition."""
170
+ thread_id = thread_id or threading.get_ident()
171
+ with self.lock:
172
+ if thread_id not in self.locks_held:
173
+ self.locks_held[thread_id] = set()
174
+ self.locks_held[thread_id].add(lock_name)
175
+
176
+ def release(self, lock_name: str, thread_id: Optional[int] = None):
177
+ """Record lock release."""
178
+ thread_id = thread_id or threading.get_ident()
179
+ with self.lock:
180
+ if thread_id in self.locks_held:
181
+ self.locks_held[thread_id].discard(lock_name)
182
+
183
+ def check_circular_wait(self) -> bool:
184
+ """
185
+ Check for potential circular wait conditions.
186
+
187
+ Returns:
188
+ True if potential deadlock detected
189
+ """
190
+ # Simplified check - in practice would need wait-for graph
191
+ with self.lock:
192
+ # Check if multiple threads hold multiple locks
193
+ multi_lock_threads = [
194
+ tid for tid, locks in self.locks_held.items()
195
+ if len(locks) > 1
196
+ ]
197
+ return len(multi_lock_threads) > 1
198
+
199
+ def get_held_locks(self) -> dict[int, set[str]]:
200
+ """Get current lock holdings."""
201
+ with self.lock:
202
+ return self.locks_held.copy()
203
+
204
+ return DeadlockDetector()
205
+
206
+
207
+ @pytest.fixture
208
+ def thread_exception_handler():
209
+ """
210
+ Capture exceptions from threads for testing.
211
+
212
+ Returns:
213
+ Exception handler for threads.
214
+ """
215
+ class ThreadExceptionHandler:
216
+ def __init__(self):
217
+ self.exceptions = []
218
+ self.lock = threading.Lock()
219
+
220
+ def handle(self, func: Callable) -> Callable:
221
+ """
222
+ Wrap function to capture exceptions.
223
+
224
+ Args:
225
+ func: Function to wrap
226
+
227
+ Returns:
228
+ Wrapped function
229
+ """
230
+ def wrapper(*args, **kwargs):
231
+ try:
232
+ return func(*args, **kwargs)
233
+ except Exception as e:
234
+ with self.lock:
235
+ self.exceptions.append({
236
+ 'thread': threading.current_thread().name,
237
+ 'exception': e,
238
+ 'time': time.time()
239
+ })
240
+ raise
241
+
242
+ return wrapper
243
+
244
+ def get_exceptions(self) -> list[dict]:
245
+ """Get all captured exceptions."""
246
+ with self.lock:
247
+ return self.exceptions.copy()
248
+
249
+ def assert_no_exceptions(self):
250
+ """Assert no exceptions were raised."""
251
+ with self.lock:
252
+ if self.exceptions:
253
+ raise AssertionError(f"Thread exceptions occurred: {self.exceptions}")
254
+
255
+ return ThreadExceptionHandler()
256
+
257
+
258
+ __all__ = [
259
+ "concurrent_executor",
260
+ "thread_synchronizer",
261
+ "deadlock_detector",
262
+ "thread_exception_handler",
263
+ ]