mcp-mesh 0.5.4__py3-none-any.whl → 0.5.6__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.
@@ -1,223 +0,0 @@
1
- """Threading utilities for sync-to-async bridging with atexit bug fixes.
2
-
3
- Provides a consolidated implementation for running async operations from sync contexts
4
- while avoiding the Python 3.8+ atexit bug that occurs in daemon thread contexts.
5
- """
6
-
7
- import asyncio
8
- import logging
9
- import queue
10
- import threading
11
- from collections.abc import Callable
12
- from typing import Any, Union
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- class ThreadingUtils:
18
- """Utilities for safe sync-to-async bridging avoiding Python 3.8+ atexit issues."""
19
-
20
- @staticmethod
21
- def run_sync_from_async(
22
- coro_or_func: Union[Any, Callable],
23
- timeout: float = 60.0,
24
- context_name: str = "operation",
25
- ) -> Any:
26
- """Convert async coroutine to sync call avoiding atexit bug.
27
-
28
- Handles both coroutines and coroutine creation functions safely.
29
-
30
- Args:
31
- coro_or_func: Either a coroutine object or a function that returns a coroutine
32
- timeout: Operation timeout in seconds
33
- context_name: Name for logging/debugging context
34
-
35
- Returns:
36
- The result of the async operation
37
-
38
- Raises:
39
- TimeoutError: If operation exceeds timeout
40
- RuntimeError: If operation fails or returns no result
41
- """
42
- import inspect
43
-
44
- # If it's a function, call it to get the coroutine
45
- if callable(coro_or_func) and not inspect.iscoroutine(coro_or_func):
46
- try:
47
- loop = asyncio.get_event_loop()
48
- if loop.is_running():
49
- # In running loop context, use thread-safe approach
50
- return ThreadingUtils._run_in_thread_safe(
51
- coro_or_func, timeout, context_name
52
- )
53
- else:
54
- # No running loop, create coroutine and run directly
55
- coro = coro_or_func()
56
- return loop.run_until_complete(coro)
57
- except RuntimeError:
58
- # No event loop, use thread-safe approach
59
- return ThreadingUtils._run_in_thread_safe(
60
- coro_or_func, timeout, context_name
61
- )
62
-
63
- # It's already a coroutine, handle it
64
- coro = coro_or_func
65
-
66
- try:
67
- loop = asyncio.get_event_loop()
68
- if loop.is_running():
69
- # Use thread-safe approach for running loops
70
- return ThreadingUtils._run_coroutine_in_thread(
71
- coro, timeout, context_name
72
- )
73
- else:
74
- # No running loop, safe to use directly
75
- return loop.run_until_complete(coro)
76
- except RuntimeError:
77
- # No event loop exists, use thread-safe approach
78
- return ThreadingUtils._run_coroutine_in_thread(coro, timeout, context_name)
79
-
80
- @staticmethod
81
- def _run_in_thread_safe(
82
- coro_func: Callable, timeout: float, context_name: str
83
- ) -> Any:
84
- """Run coroutine creation function in thread-safe manner."""
85
- result_queue: queue.Queue = queue.Queue()
86
-
87
- def _thread_runner():
88
- """Execute coroutine function in isolated thread context."""
89
- try:
90
- # Apply atexit bypass for this thread
91
- ThreadingUtils._apply_atexit_bypass()
92
-
93
- # Create fresh event loop
94
- new_loop = asyncio.new_event_loop()
95
- asyncio.set_event_loop(new_loop)
96
-
97
- try:
98
- # Create coroutine in this thread's context
99
- coro = coro_func()
100
- result = new_loop.run_until_complete(coro)
101
- result_queue.put(("success", result))
102
- except Exception as e:
103
- logger.error(
104
- f"❌ {context_name} failed in thread: {e}", exc_info=True
105
- )
106
- result_queue.put(("error", e))
107
- finally:
108
- # Manual cleanup without relying on atexit
109
- try:
110
- new_loop.close()
111
- finally:
112
- asyncio.set_event_loop(None)
113
-
114
- except Exception as e:
115
- logger.error(
116
- f"❌ {context_name} thread setup failed: {e}", exc_info=True
117
- )
118
- result_queue.put(("error", e))
119
-
120
- # Use non-daemon thread to avoid atexit issues
121
- thread = threading.Thread(
122
- target=_thread_runner, daemon=False, name=f"MCPMesh-{context_name}"
123
- )
124
- thread.start()
125
- thread.join(timeout=timeout)
126
-
127
- if thread.is_alive():
128
- logger.error(f"⏰ {context_name} timed out after {timeout}s")
129
- raise TimeoutError(f"{context_name} timed out after {timeout}s")
130
-
131
- if result_queue.empty():
132
- raise RuntimeError(f"No result from {context_name}")
133
-
134
- status, result = result_queue.get()
135
- if status == "error":
136
- raise result
137
-
138
- logger.debug(f"✅ {context_name} completed successfully in thread")
139
- return result
140
-
141
- @staticmethod
142
- def _run_coroutine_in_thread(coro: Any, timeout: float, context_name: str) -> Any:
143
- """Run existing coroutine in thread-safe manner."""
144
- result_queue: queue.Queue = queue.Queue()
145
-
146
- def _thread_runner():
147
- """Execute coroutine in isolated thread context."""
148
- try:
149
- # Apply atexit bypass for this thread
150
- ThreadingUtils._apply_atexit_bypass()
151
-
152
- # Create fresh event loop
153
- new_loop = asyncio.new_event_loop()
154
- asyncio.set_event_loop(new_loop)
155
-
156
- try:
157
- result = new_loop.run_until_complete(coro)
158
- result_queue.put(("success", result))
159
- except Exception as e:
160
- logger.error(
161
- f"❌ {context_name} coroutine failed: {e}", exc_info=True
162
- )
163
- result_queue.put(("error", e))
164
- finally:
165
- # Manual cleanup without relying on atexit
166
- try:
167
- new_loop.close()
168
- finally:
169
- asyncio.set_event_loop(None)
170
-
171
- except Exception as e:
172
- logger.error(
173
- f"❌ {context_name} thread setup failed: {e}", exc_info=True
174
- )
175
- result_queue.put(("error", e))
176
-
177
- # Use non-daemon thread to avoid atexit issues
178
- thread = threading.Thread(
179
- target=_thread_runner, daemon=False, name=f"MCPMesh-{context_name}"
180
- )
181
- thread.start()
182
- thread.join(timeout=timeout)
183
-
184
- if thread.is_alive():
185
- logger.error(f"⏰ {context_name} timed out after {timeout}s")
186
- raise TimeoutError(f"{context_name} timed out after {timeout}s")
187
-
188
- if result_queue.empty():
189
- raise RuntimeError(f"No result from {context_name}")
190
-
191
- status, result = result_queue.get()
192
- if status == "error":
193
- raise result
194
-
195
- logger.debug(f"✅ {context_name} completed successfully in thread")
196
- return result
197
-
198
- @staticmethod
199
- def _apply_atexit_bypass():
200
- """Apply atexit bypass for current thread to prevent registration errors."""
201
- try:
202
- import atexit
203
- import threading
204
-
205
- # Store originals
206
- original_atexit_register = atexit.register
207
- original_thread_register = getattr(threading, "_register_atexit", None)
208
-
209
- # Apply temporary bypass
210
- def _noop_register(*args, **kwargs):
211
- """No-op atexit registration to prevent threading issues."""
212
- pass
213
-
214
- atexit.register = _noop_register
215
- if original_thread_register:
216
- threading._register_atexit = _noop_register
217
-
218
- # Store cleanup function for potential restoration
219
- # (In practice, these threads are short-lived so restoration isn't critical)
220
-
221
- except Exception as e:
222
- # If atexit bypass fails, log but continue
223
- logger.warning(f"⚠️ Failed to apply atexit bypass: {e}")