datacurrent 1.0.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.
Files changed (43) hide show
  1. datacurrent/__init__.py +157 -0
  2. datacurrent/_version.py +6 -0
  3. datacurrent/buffering/__init__.py +8 -0
  4. datacurrent/buffering/backpressure.py +47 -0
  5. datacurrent/buffering/queue.py +304 -0
  6. datacurrent/concurrency/__init__.py +8 -0
  7. datacurrent/concurrency/limits.py +106 -0
  8. datacurrent/concurrency/tasks.py +86 -0
  9. datacurrent/config.py +86 -0
  10. datacurrent/core/__init__.py +17 -0
  11. datacurrent/core/_util.py +42 -0
  12. datacurrent/core/lifecycle.py +81 -0
  13. datacurrent/core/operators.py +844 -0
  14. datacurrent/core/pipeline.py +64 -0
  15. datacurrent/core/sink.py +70 -0
  16. datacurrent/core/stream.py +566 -0
  17. datacurrent/events/__init__.py +9 -0
  18. datacurrent/events/bus.py +380 -0
  19. datacurrent/events/routing.py +59 -0
  20. datacurrent/events/subscriptions.py +75 -0
  21. datacurrent/exceptions.py +191 -0
  22. datacurrent/integrations/__init__.py +18 -0
  23. datacurrent/integrations/fastapi.py +76 -0
  24. datacurrent/integrations/starlette.py +75 -0
  25. datacurrent/observability/__init__.py +17 -0
  26. datacurrent/observability/logging.py +128 -0
  27. datacurrent/observability/metrics.py +127 -0
  28. datacurrent/py.typed +1 -0
  29. datacurrent/reliability/__init__.py +14 -0
  30. datacurrent/reliability/retry.py +134 -0
  31. datacurrent/reliability/timeout.py +39 -0
  32. datacurrent/transports/__init__.py +31 -0
  33. datacurrent/transports/_deps.py +23 -0
  34. datacurrent/transports/base.py +80 -0
  35. datacurrent/transports/http.py +211 -0
  36. datacurrent/transports/memory.py +171 -0
  37. datacurrent/transports/sse.py +374 -0
  38. datacurrent/transports/websocket.py +277 -0
  39. datacurrent-1.0.0.dist-info/METADATA +250 -0
  40. datacurrent-1.0.0.dist-info/RECORD +43 -0
  41. datacurrent-1.0.0.dist-info/WHEEL +5 -0
  42. datacurrent-1.0.0.dist-info/licenses/LICENSE +20 -0
  43. datacurrent-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,157 @@
1
+ """DataCurrent — Python streaming, made simple.
2
+
3
+ Async streams, composable operators, real backpressure and modular real-time
4
+ transports for Python applications.
5
+
6
+ from datacurrent import Stream
7
+
8
+ stream = Stream.from_iterable([1, 2, 3]).map(lambda n: n * 2)
9
+ async for value in stream:
10
+ print(value)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from ._version import VERSION, __version__
16
+ from .buffering import BoundedQueue, BufferPolicy, BufferStats
17
+ from .concurrency import ConcurrencyLimiter, TaskTracker, gather_limited
18
+ from .config import (
19
+ DEFAULT_BUFFER_SIZE,
20
+ DEFAULT_CONCURRENCY,
21
+ DEFAULT_MAX_MESSAGE_SIZE,
22
+ DEFAULT_TIMEOUT,
23
+ OverflowPolicy,
24
+ StreamConfig,
25
+ )
26
+ from .core import (
27
+ Stream,
28
+ StreamIterator,
29
+ concat_sources,
30
+ consume,
31
+ drain_into,
32
+ merge_sources,
33
+ pipeline,
34
+ )
35
+ from .events import Event, EventBus, Subscription, matches
36
+ from .exceptions import (
37
+ BufferOverflowError,
38
+ ConfigurationError,
39
+ ConnectionFailedError,
40
+ DataCurrentError,
41
+ EmptyStreamError,
42
+ EventBusError,
43
+ MessageTooLargeError,
44
+ NonReplayableStreamError,
45
+ OptionalDependencyError,
46
+ QueueClosedError,
47
+ QueueEmptyError,
48
+ RetryError,
49
+ SSEProtocolError,
50
+ StreamClosedError,
51
+ StreamConsumedError,
52
+ StreamError,
53
+ StreamProtocolError,
54
+ StreamTimeoutError,
55
+ SubscriptionClosedError,
56
+ TransportError,
57
+ WebSocketClosedError,
58
+ )
59
+ from .observability import (
60
+ Counter,
61
+ Metrics,
62
+ StreamStats,
63
+ get_logger,
64
+ redact_headers,
65
+ redact_secret,
66
+ redact_url,
67
+ )
68
+ from .reliability import RetryPolicy, retry_async, with_timeout
69
+ from .transports import (
70
+ Channel,
71
+ HTTPStream,
72
+ SSEEvent,
73
+ SSEStream,
74
+ StreamTransport,
75
+ WebSocketStream,
76
+ format_heartbeat,
77
+ format_sse,
78
+ parse_sse,
79
+ )
80
+
81
+ __all__ = [
82
+ # Constants
83
+ "DEFAULT_BUFFER_SIZE",
84
+ "DEFAULT_CONCURRENCY",
85
+ "DEFAULT_MAX_MESSAGE_SIZE",
86
+ "DEFAULT_TIMEOUT",
87
+ "VERSION",
88
+ # Buffering
89
+ "BoundedQueue",
90
+ "BufferOverflowError",
91
+ "BufferPolicy",
92
+ "BufferStats",
93
+ # Transports
94
+ "Channel",
95
+ # Concurrency
96
+ "ConcurrencyLimiter",
97
+ "ConfigurationError",
98
+ "ConnectionFailedError",
99
+ "Counter",
100
+ # Exceptions
101
+ "DataCurrentError",
102
+ "EmptyStreamError",
103
+ # Events
104
+ "Event",
105
+ "EventBus",
106
+ "EventBusError",
107
+ "HTTPStream",
108
+ "MessageTooLargeError",
109
+ # Observability
110
+ "Metrics",
111
+ "NonReplayableStreamError",
112
+ "OptionalDependencyError",
113
+ "OverflowPolicy",
114
+ "QueueClosedError",
115
+ "QueueEmptyError",
116
+ "RetryError",
117
+ # Reliability
118
+ "RetryPolicy",
119
+ "SSEEvent",
120
+ "SSEProtocolError",
121
+ "SSEStream",
122
+ # Core engine
123
+ "Stream",
124
+ "StreamClosedError",
125
+ "StreamConfig",
126
+ "StreamConsumedError",
127
+ "StreamError",
128
+ "StreamIterator",
129
+ "StreamProtocolError",
130
+ "StreamStats",
131
+ "StreamTimeoutError",
132
+ "StreamTransport",
133
+ "Subscription",
134
+ "SubscriptionClosedError",
135
+ "TaskTracker",
136
+ "TransportError",
137
+ "WebSocketClosedError",
138
+ "WebSocketStream",
139
+ # Version
140
+ "__version__",
141
+ "concat_sources",
142
+ "consume",
143
+ "drain_into",
144
+ "format_heartbeat",
145
+ "format_sse",
146
+ "gather_limited",
147
+ "get_logger",
148
+ "matches",
149
+ "merge_sources",
150
+ "parse_sse",
151
+ "pipeline",
152
+ "redact_headers",
153
+ "redact_secret",
154
+ "redact_url",
155
+ "retry_async",
156
+ "with_timeout",
157
+ ]
@@ -0,0 +1,6 @@
1
+ """Single source of truth for the DataCurrent version."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "1.0.0"
6
+ VERSION = tuple(int(part) for part in __version__.split("."))
@@ -0,0 +1,8 @@
1
+ """Buffering: bounded queues, overflow policies and backpressure."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .backpressure import BufferPolicy
6
+ from .queue import BoundedQueue, BufferStats
7
+
8
+ __all__ = ["BoundedQueue", "BufferPolicy", "BufferStats"]
@@ -0,0 +1,47 @@
1
+ """Normalized buffer settings shared by streams, channels and the event bus."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from ..config import DEFAULT_BUFFER_SIZE, OverflowPolicy, StreamConfig
9
+ from ..exceptions import ConfigurationError
10
+ from .queue import BoundedQueue
11
+
12
+ __all__ = ["BufferPolicy"]
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class BufferPolicy:
17
+ """A size + overflow policy pair that can build :class:`BoundedQueue` objects."""
18
+
19
+ size: int = DEFAULT_BUFFER_SIZE
20
+ overflow: OverflowPolicy = OverflowPolicy.BLOCK
21
+
22
+ def __post_init__(self) -> None:
23
+ object.__setattr__(self, "overflow", OverflowPolicy.coerce(self.overflow))
24
+ if isinstance(self.size, bool) or not isinstance(self.size, int):
25
+ raise ConfigurationError(
26
+ f"buffer size must be an integer, got {type(self.size).__name__}"
27
+ )
28
+ if self.size < 1:
29
+ raise ConfigurationError(f"buffer size must be >= 1, got {self.size}")
30
+
31
+ def build(self, *, name: str | None = None, on_drop: Any = None) -> BoundedQueue[Any]:
32
+ """Create a queue configured by this policy."""
33
+ return BoundedQueue(self.size, overflow=self.overflow, name=name, on_drop=on_drop)
34
+
35
+ @classmethod
36
+ def from_config(
37
+ cls,
38
+ config: StreamConfig,
39
+ *,
40
+ size: int | None = None,
41
+ overflow: OverflowPolicy | str | None = None,
42
+ ) -> BufferPolicy:
43
+ """Derive a policy from a stream config, allowing per-call overrides."""
44
+ return cls(
45
+ size=config.buffer_size if size is None else size,
46
+ overflow=OverflowPolicy.coerce(config.overflow if overflow is None else overflow),
47
+ )
@@ -0,0 +1,304 @@
1
+ """Bounded asynchronous queues with explicit overflow policies.
2
+
3
+ The queue is the backpressure primitive used by ``Stream.buffer()``,
4
+ ``Stream.merge()``, :class:`~datacurrent.Channel` and the event bus. It never
5
+ silently drops data: the default policy blocks the producer.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from collections import deque
12
+ from collections.abc import AsyncIterator, Callable
13
+ from dataclasses import dataclass
14
+ from typing import Any, Generic, TypeVar
15
+
16
+ from ..config import DEFAULT_BUFFER_SIZE, OverflowPolicy
17
+ from ..exceptions import (
18
+ BufferOverflowError,
19
+ ConfigurationError,
20
+ QueueClosedError,
21
+ QueueEmptyError,
22
+ )
23
+ from ..observability.logging import get_logger
24
+
25
+ T = TypeVar("T")
26
+
27
+ _EMPTY = object()
28
+ _log = get_logger("buffering")
29
+
30
+ __all__ = ["BoundedQueue", "BufferStats"]
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class BufferStats:
35
+ """Point-in-time snapshot of a queue."""
36
+
37
+ size: int
38
+ maxsize: int
39
+ enqueued: int
40
+ dequeued: int
41
+ dropped: int
42
+ overflow: int
43
+ high_watermark: int
44
+ closed: bool
45
+
46
+ def as_dict(self) -> dict[str, Any]:
47
+ return {
48
+ "size": self.size,
49
+ "maxsize": self.maxsize,
50
+ "enqueued": self.enqueued,
51
+ "dequeued": self.dequeued,
52
+ "dropped": self.dropped,
53
+ "overflow": self.overflow,
54
+ "high_watermark": self.high_watermark,
55
+ "closed": self.closed,
56
+ }
57
+
58
+
59
+ class BoundedQueue(Generic[T]):
60
+ """An asyncio queue with a bounded size and a configurable overflow policy.
61
+
62
+ Args:
63
+ maxsize: Maximum number of buffered items (must be >= 1).
64
+ overflow: Policy applied when the buffer is full. ``BLOCK`` applies
65
+ backpressure, ``DROP_NEWEST`` / ``DROP_OLDEST`` discard one item per
66
+ overflow, and ``ERROR`` raises :class:`BufferOverflowError`.
67
+ name: Optional name used in logging and error messages.
68
+ on_drop: Called once per dropped item (used for stream statistics).
69
+
70
+ The queue is intended for use from a single event loop. It is not thread
71
+ safe and does not perform cross-loop wakeups.
72
+ """
73
+
74
+ __slots__ = (
75
+ "_closed",
76
+ "_cond",
77
+ "_dequeued",
78
+ "_dropped",
79
+ "_enqueued",
80
+ "_error",
81
+ "_high_watermark",
82
+ "_items",
83
+ "_maxsize",
84
+ "_name",
85
+ "_on_drop",
86
+ "_overflow",
87
+ "_overflow_count",
88
+ )
89
+
90
+ def __init__(
91
+ self,
92
+ maxsize: int = DEFAULT_BUFFER_SIZE,
93
+ *,
94
+ overflow: OverflowPolicy | str = OverflowPolicy.BLOCK,
95
+ name: str | None = None,
96
+ on_drop: Callable[[], None] | None = None,
97
+ ) -> None:
98
+ if isinstance(maxsize, bool) or not isinstance(maxsize, int):
99
+ raise ConfigurationError(f"maxsize must be an integer, got {type(maxsize).__name__}")
100
+ if maxsize < 1:
101
+ raise ConfigurationError(f"maxsize must be >= 1, got {maxsize}")
102
+ self._items: deque[T] = deque()
103
+ self._maxsize = maxsize
104
+ self._overflow = OverflowPolicy.coerce(overflow)
105
+ self._cond = asyncio.Condition()
106
+ self._closed = False
107
+ self._error: BaseException | None = None
108
+ self._name = name
109
+ self._on_drop = on_drop
110
+ self._enqueued = 0
111
+ self._dequeued = 0
112
+ self._dropped = 0
113
+ self._overflow_count = 0
114
+ self._high_watermark = 0
115
+
116
+ # ------------------------------------------------------------------ #
117
+ # Introspection
118
+ # ------------------------------------------------------------------ #
119
+ @property
120
+ def name(self) -> str | None:
121
+ return self._name
122
+
123
+ @property
124
+ def maxsize(self) -> int:
125
+ return self._maxsize
126
+
127
+ @property
128
+ def overflow(self) -> OverflowPolicy:
129
+ return self._overflow
130
+
131
+ @property
132
+ def closed(self) -> bool:
133
+ return self._closed
134
+
135
+ @property
136
+ def error(self) -> BaseException | None:
137
+ """The failure that terminated the queue, if any."""
138
+ return self._error
139
+
140
+ def qsize(self) -> int:
141
+ return len(self._items)
142
+
143
+ def empty(self) -> bool:
144
+ return not self._items
145
+
146
+ def full(self) -> bool:
147
+ return len(self._items) >= self._maxsize
148
+
149
+ def stats(self) -> BufferStats:
150
+ return BufferStats(
151
+ size=len(self._items),
152
+ maxsize=self._maxsize,
153
+ enqueued=self._enqueued,
154
+ dequeued=self._dequeued,
155
+ dropped=self._dropped,
156
+ overflow=self._overflow_count,
157
+ high_watermark=self._high_watermark,
158
+ closed=self._closed,
159
+ )
160
+
161
+ def _label(self) -> str:
162
+ return f" {self._name!r}" if self._name else ""
163
+
164
+ def _record_drop(self) -> None:
165
+ self._dropped += 1
166
+ self._overflow_count += 1
167
+ if self._on_drop is not None:
168
+ self._on_drop()
169
+
170
+ # ------------------------------------------------------------------ #
171
+ # Writing
172
+ # ------------------------------------------------------------------ #
173
+ async def put(self, item: T) -> bool:
174
+ """Insert ``item``.
175
+
176
+ Returns:
177
+ ``True`` if the item was stored, ``False`` if it was dropped by a
178
+ drop policy.
179
+
180
+ Raises:
181
+ QueueClosedError: The queue has been closed.
182
+ BufferOverflowError: The queue is full and the policy is ``ERROR``.
183
+ """
184
+ async with self._cond:
185
+ while True:
186
+ if self._closed:
187
+ raise QueueClosedError(f"queue{self._label()} is closed")
188
+ if len(self._items) < self._maxsize:
189
+ break
190
+ policy = self._overflow
191
+ if policy is OverflowPolicy.DROP_NEWEST:
192
+ self._record_drop()
193
+ self._cond.notify_all()
194
+ return False
195
+ if policy is OverflowPolicy.DROP_OLDEST:
196
+ self._items.popleft()
197
+ self._record_drop()
198
+ break
199
+ if policy is OverflowPolicy.ERROR:
200
+ self._overflow_count += 1
201
+ raise BufferOverflowError(self._maxsize, policy=policy, dropped=self._dropped)
202
+ await self._cond.wait()
203
+
204
+ self._items.append(item)
205
+ self._enqueued += 1
206
+ if len(self._items) > self._high_watermark:
207
+ self._high_watermark = len(self._items)
208
+ self._cond.notify_all()
209
+ return True
210
+
211
+ async def put_many(self, items: Any) -> int:
212
+ """Insert every item from an iterable, returning the number stored."""
213
+ stored = 0
214
+ for item in items:
215
+ if await self.put(item):
216
+ stored += 1
217
+ return stored
218
+
219
+ # ------------------------------------------------------------------ #
220
+ # Reading
221
+ # ------------------------------------------------------------------ #
222
+ async def _take(self, default: Any) -> Any:
223
+ async with self._cond:
224
+ while not self._items:
225
+ if self._error is not None:
226
+ raise self._error
227
+ if self._closed:
228
+ return default
229
+ await self._cond.wait()
230
+ item = self._items.popleft()
231
+ self._dequeued += 1
232
+ self._cond.notify_all()
233
+ return item
234
+
235
+ async def get(self) -> T:
236
+ """Return the oldest item, waiting if necessary.
237
+
238
+ Raises:
239
+ QueueClosedError: The queue is closed and empty.
240
+ """
241
+ item = await self._take(_EMPTY)
242
+ if item is _EMPTY:
243
+ raise QueueClosedError(f"queue{self._label()} is closed and empty")
244
+ return item
245
+
246
+ async def get_nowait(self) -> T:
247
+ """Return the oldest item without waiting.
248
+
249
+ Raises:
250
+ QueueEmptyError: No item is buffered.
251
+ """
252
+ async with self._cond:
253
+ if not self._items:
254
+ if self._error is not None:
255
+ raise self._error
256
+ raise QueueEmptyError(f"queue{self._label()} is empty")
257
+ item = self._items.popleft()
258
+ self._dequeued += 1
259
+ self._cond.notify_all()
260
+ return item
261
+
262
+ def __aiter__(self) -> AsyncIterator[T]:
263
+ return self._iterate()
264
+
265
+ async def _iterate(self) -> AsyncIterator[T]:
266
+ while True:
267
+ item = await self._take(_EMPTY)
268
+ if item is _EMPTY:
269
+ return
270
+ yield item
271
+
272
+ # ------------------------------------------------------------------ #
273
+ # Lifecycle
274
+ # ------------------------------------------------------------------ #
275
+ async def aclose(self) -> None:
276
+ """Close the queue. Buffered items can still be drained."""
277
+ async with self._cond:
278
+ if self._closed:
279
+ return
280
+ self._closed = True
281
+ self._cond.notify_all()
282
+
283
+ async def fail(self, error: BaseException) -> None:
284
+ """Store ``error`` and close the queue; consumers re-raise it when drained."""
285
+ if not isinstance(error, BaseException):
286
+ raise TypeError("error must be an exception instance")
287
+ async with self._cond:
288
+ if self._error is None:
289
+ self._error = error
290
+ self._closed = True
291
+ self._cond.notify_all()
292
+ _log.debug("queue%s failed: %r", self._label(), error)
293
+
294
+ async def __aenter__(self) -> BoundedQueue[T]:
295
+ return self
296
+
297
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
298
+ await self.aclose()
299
+
300
+ def __repr__(self) -> str:
301
+ return (
302
+ f"BoundedQueue(size={len(self._items)}, maxsize={self._maxsize}, "
303
+ f"overflow={self._overflow.value!r}, closed={self._closed})"
304
+ )
@@ -0,0 +1,8 @@
1
+ """Concurrency: bounded limits and structured background tasks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .limits import ConcurrencyLimiter, gather_limited
6
+ from .tasks import TaskTracker
7
+
8
+ __all__ = ["ConcurrencyLimiter", "TaskTracker", "gather_limited"]
@@ -0,0 +1,106 @@
1
+ """Limits and helpers for bounded concurrency."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Awaitable, Callable, Iterable
7
+ from typing import Any, TypeVar, cast
8
+
9
+ from ..core._util import maybe_await
10
+ from ..exceptions import ConfigurationError
11
+
12
+ T = TypeVar("T")
13
+
14
+ __all__ = ["ConcurrencyLimiter", "gather_limited"]
15
+
16
+
17
+ class ConcurrencyLimiter:
18
+ """A semaphore with introspection.
19
+
20
+ ``limit`` workers may run at once; additional callers wait instead of
21
+ spawning unbounded tasks.
22
+ """
23
+
24
+ __slots__ = ("_active", "_limit", "_peak", "_semaphore", "_waiting")
25
+
26
+ def __init__(self, limit: int) -> None:
27
+ if isinstance(limit, bool) or not isinstance(limit, int):
28
+ raise ConfigurationError(
29
+ f"concurrency limit must be an integer, got {type(limit).__name__}"
30
+ )
31
+ if limit < 1:
32
+ raise ConfigurationError(f"concurrency limit must be >= 1, got {limit}")
33
+ self._limit = limit
34
+ self._semaphore = asyncio.Semaphore(limit)
35
+ self._active = 0
36
+ self._peak = 0
37
+ self._waiting = 0
38
+
39
+ @property
40
+ def limit(self) -> int:
41
+ return self._limit
42
+
43
+ @property
44
+ def active(self) -> int:
45
+ return self._active
46
+
47
+ @property
48
+ def waiting(self) -> int:
49
+ return self._waiting
50
+
51
+ @property
52
+ def peak(self) -> int:
53
+ return self._peak
54
+
55
+ async def acquire(self) -> None:
56
+ self._waiting += 1
57
+ try:
58
+ await self._semaphore.acquire()
59
+ finally:
60
+ self._waiting -= 1
61
+ self._active += 1
62
+ self._peak = max(self._peak, self._active)
63
+
64
+ def release(self) -> None:
65
+ if self._active > 0:
66
+ self._active -= 1
67
+ self._semaphore.release()
68
+
69
+ async def __aenter__(self) -> ConcurrencyLimiter:
70
+ await self.acquire()
71
+ return self
72
+
73
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
74
+ self.release()
75
+
76
+ async def run(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
77
+ async with self:
78
+ return await maybe_await(func(*args, **kwargs))
79
+
80
+ def __repr__(self) -> str: # pragma: no cover - trivial
81
+ return (
82
+ f"ConcurrencyLimiter(limit={self._limit}, active={self._active}, "
83
+ f"waiting={self._waiting}, peak={self._peak})"
84
+ )
85
+
86
+
87
+ async def gather_limited(
88
+ limit: int,
89
+ awaitables: Iterable[Awaitable[T]],
90
+ *,
91
+ return_exceptions: bool = False,
92
+ ) -> list[T]:
93
+ """Await ``awaitables`` with at most ``limit`` running at the same time."""
94
+ limiter = ConcurrencyLimiter(limit)
95
+
96
+ async def run(awaitable: Awaitable[T]) -> T:
97
+ async with limiter:
98
+ return await awaitable
99
+
100
+ return cast(
101
+ "list[T]",
102
+ await asyncio.gather(
103
+ *(run(awaitable) for awaitable in awaitables),
104
+ return_exceptions=return_exceptions,
105
+ ),
106
+ )