datacurrent 1.0.0__tar.gz
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.
- datacurrent-1.0.0/CHANGELOG.md +59 -0
- datacurrent-1.0.0/LICENSE +20 -0
- datacurrent-1.0.0/MANIFEST.in +15 -0
- datacurrent-1.0.0/PKG-INFO +250 -0
- datacurrent-1.0.0/README.md +204 -0
- datacurrent-1.0.0/datacurrent/__init__.py +157 -0
- datacurrent-1.0.0/datacurrent/_version.py +6 -0
- datacurrent-1.0.0/datacurrent/buffering/__init__.py +8 -0
- datacurrent-1.0.0/datacurrent/buffering/backpressure.py +47 -0
- datacurrent-1.0.0/datacurrent/buffering/queue.py +304 -0
- datacurrent-1.0.0/datacurrent/concurrency/__init__.py +8 -0
- datacurrent-1.0.0/datacurrent/concurrency/limits.py +106 -0
- datacurrent-1.0.0/datacurrent/concurrency/tasks.py +86 -0
- datacurrent-1.0.0/datacurrent/config.py +86 -0
- datacurrent-1.0.0/datacurrent/core/__init__.py +17 -0
- datacurrent-1.0.0/datacurrent/core/_util.py +42 -0
- datacurrent-1.0.0/datacurrent/core/lifecycle.py +81 -0
- datacurrent-1.0.0/datacurrent/core/operators.py +844 -0
- datacurrent-1.0.0/datacurrent/core/pipeline.py +64 -0
- datacurrent-1.0.0/datacurrent/core/sink.py +70 -0
- datacurrent-1.0.0/datacurrent/core/stream.py +566 -0
- datacurrent-1.0.0/datacurrent/events/__init__.py +9 -0
- datacurrent-1.0.0/datacurrent/events/bus.py +380 -0
- datacurrent-1.0.0/datacurrent/events/routing.py +59 -0
- datacurrent-1.0.0/datacurrent/events/subscriptions.py +75 -0
- datacurrent-1.0.0/datacurrent/exceptions.py +191 -0
- datacurrent-1.0.0/datacurrent/integrations/__init__.py +18 -0
- datacurrent-1.0.0/datacurrent/integrations/fastapi.py +76 -0
- datacurrent-1.0.0/datacurrent/integrations/starlette.py +75 -0
- datacurrent-1.0.0/datacurrent/observability/__init__.py +17 -0
- datacurrent-1.0.0/datacurrent/observability/logging.py +128 -0
- datacurrent-1.0.0/datacurrent/observability/metrics.py +127 -0
- datacurrent-1.0.0/datacurrent/py.typed +1 -0
- datacurrent-1.0.0/datacurrent/reliability/__init__.py +14 -0
- datacurrent-1.0.0/datacurrent/reliability/retry.py +134 -0
- datacurrent-1.0.0/datacurrent/reliability/timeout.py +39 -0
- datacurrent-1.0.0/datacurrent/transports/__init__.py +31 -0
- datacurrent-1.0.0/datacurrent/transports/_deps.py +23 -0
- datacurrent-1.0.0/datacurrent/transports/base.py +80 -0
- datacurrent-1.0.0/datacurrent/transports/http.py +211 -0
- datacurrent-1.0.0/datacurrent/transports/memory.py +171 -0
- datacurrent-1.0.0/datacurrent/transports/sse.py +374 -0
- datacurrent-1.0.0/datacurrent/transports/websocket.py +277 -0
- datacurrent-1.0.0/datacurrent.egg-info/PKG-INFO +250 -0
- datacurrent-1.0.0/datacurrent.egg-info/SOURCES.txt +48 -0
- datacurrent-1.0.0/datacurrent.egg-info/dependency_links.txt +1 -0
- datacurrent-1.0.0/datacurrent.egg-info/requires.txt +23 -0
- datacurrent-1.0.0/datacurrent.egg-info/top_level.txt +1 -0
- datacurrent-1.0.0/pyproject.toml +123 -0
- datacurrent-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to DataCurrent are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [1.0.0] — 2026-11-08
|
|
8
|
+
|
|
9
|
+
First stable release. Built and verified locally; not published to PyPI.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `Stream` engine: construction from iterables, async iterables, factories,
|
|
14
|
+
queues, values and other streams; single-consumer semantics with explicit
|
|
15
|
+
`StreamConsumedError`; replayable sources for retries.
|
|
16
|
+
- Operators: `map` (bounded, ordered concurrency), `filter`, `flat_map`,
|
|
17
|
+
`take`, `skip`, `batch`, `buffer`, `distinct`, `enumerate`, `window`,
|
|
18
|
+
`debounce`, `throttle`, `zip`, `merge`, `concat`, `timeout`, `retry`,
|
|
19
|
+
`recover`, `tap`.
|
|
20
|
+
- Terminal operations: `collect`, `count`, `drain`, `first`, `last`, `reduce`.
|
|
21
|
+
- Bounded `BoundedQueue` with `block`, `drop_newest`, `drop_oldest` and `error`
|
|
22
|
+
overflow policies, high-watermark statistics and producer suspension.
|
|
23
|
+
- `ConcurrencyLimiter`, `TaskTracker`, `gather_limited` and `consume()` with
|
|
24
|
+
bounded concurrency and cancellation-safe cleanup.
|
|
25
|
+
- `RetryPolicy` with exponential backoff, jitter, attempt limits and exception
|
|
26
|
+
predicates; `retry_async` helper; `with_timeout`.
|
|
27
|
+
- In-memory `Channel` broadcast with per-subscriber bounded queues and drop
|
|
28
|
+
accounting.
|
|
29
|
+
- `EventBus` with `Event` envelopes, wildcard topic routing, handler isolation,
|
|
30
|
+
stream subscriptions, draining shutdown and statistics.
|
|
31
|
+
- Transports: `SSEStream` (WHATWG parser, `Last-Event-ID`, reconnection),
|
|
32
|
+
`HTTPStream` (chunked iteration, line iteration, byte limits) and
|
|
33
|
+
`WebSocketStream` (text/binary/JSON, heartbeats, size limits, reconnection),
|
|
34
|
+
backed by `httpx` and `websockets`, which install with the package.
|
|
35
|
+
- `StreamTransport` extension base for custom protocols.
|
|
36
|
+
- FastAPI / Starlette helpers: SSE responses, streaming responses, WebSocket
|
|
37
|
+
message streams and lifespan cleanup.
|
|
38
|
+
- Observability: `Metrics`, `Counter`, per-stream `StreamStats`, structured
|
|
39
|
+
logging with credential redaction.
|
|
40
|
+
- Complete exception hierarchy rooted at `DataCurrentError`.
|
|
41
|
+
- 279 tests: unit, property-based, stress, networking with local servers,
|
|
42
|
+
packaging and executable examples (100% line and branch coverage).
|
|
43
|
+
- 16 runnable examples, full documentation, reproducible benchmarks and a
|
|
44
|
+
cross-platform developer task runner.
|
|
45
|
+
|
|
46
|
+
### Security
|
|
47
|
+
|
|
48
|
+
- TLS verification is not disabled anywhere by default.
|
|
49
|
+
- Credentials in URLs, headers and secrets are redacted from logs and errors.
|
|
50
|
+
- Message-size and body-size limits are configurable and enforced.
|
|
51
|
+
- Resource cleanup is guaranteed on cancellation and early exit.
|
|
52
|
+
|
|
53
|
+
### Known limitations
|
|
54
|
+
|
|
55
|
+
- No durable broker adapters (Redis, Kafka, RabbitMQ, NATS) in this release.
|
|
56
|
+
- No OpenTelemetry exporter; metrics are exposed as snapshots only.
|
|
57
|
+
- asyncio only; AnyIO/Trio backends are not supported.
|
|
58
|
+
- The project name has not been trademark-cleared and the package is not on
|
|
59
|
+
PyPI.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CoderProandTester
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include MANIFEST.in
|
|
5
|
+
recursive-include datacurrent *.py py.typed
|
|
6
|
+
prune tests
|
|
7
|
+
prune examples
|
|
8
|
+
prune benchmarks
|
|
9
|
+
prune docs
|
|
10
|
+
prune scripts
|
|
11
|
+
prune .venv
|
|
12
|
+
prune dist
|
|
13
|
+
prune build
|
|
14
|
+
global-exclude *.py[cod]
|
|
15
|
+
global-exclude __pycache__
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datacurrent
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python streaming, made simple: async streams, operators, backpressure and real-time transports.
|
|
5
|
+
Author: DataCurrent contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: streaming,async,asyncio,real-time,data pipeline,stream processing,backpressure,reactive,event bus,pubsub,websocket,server-sent events,http streaming,concurrency,observability,http,https
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Framework :: AsyncIO
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Internet
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Requires-Dist: websockets>=12
|
|
28
|
+
Provides-Extra: starlette
|
|
29
|
+
Requires-Dist: starlette>=0.36; extra == "starlette"
|
|
30
|
+
Provides-Extra: fastapi
|
|
31
|
+
Requires-Dist: fastapi>=0.110; extra == "fastapi"
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: starlette>=0.36; extra == "all"
|
|
34
|
+
Requires-Dist: fastapi>=0.110; extra == "all"
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
39
|
+
Requires-Dist: hypothesis>=6; extra == "dev"
|
|
40
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
41
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
42
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
43
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
44
|
+
Requires-Dist: fastapi>=0.110; extra == "dev"
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
|
|
47
|
+
# DataCurrent
|
|
48
|
+
|
|
49
|
+
### Python streaming, made simple.
|
|
50
|
+
|
|
51
|
+
DataCurrent is a production-ready asynchronous streaming library. It gives you
|
|
52
|
+
one consistent API for lists, async generators, queues, WebSockets, Server-Sent
|
|
53
|
+
Events and streaming HTTP responses.
|
|
54
|
+
|
|
55
|
+
**Version 1.0.0 · Stable / Production**
|
|
56
|
+
|
|
57
|
+
- 279 tests passing, with 100% line and branch coverage.
|
|
58
|
+
- Required networking dependencies install automatically.
|
|
59
|
+
- No broker, database, API key or hosted service.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install datacurrent
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Python 3.11 or newer is required. `httpx` and `websockets` are installed with the
|
|
68
|
+
package, so HTTP streaming, SSE and WebSocket clients work immediately.
|
|
69
|
+
|
|
70
|
+
Framework helpers are optional:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install "datacurrent[fastapi]"
|
|
74
|
+
pip install "datacurrent[starlette]"
|
|
75
|
+
pip install "datacurrent[all]"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import asyncio
|
|
82
|
+
from datacurrent import Stream
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def main() -> None:
|
|
86
|
+
result = await (
|
|
87
|
+
Stream.from_iterable(range(10))
|
|
88
|
+
.filter(lambda number: number % 2 == 0)
|
|
89
|
+
.map(lambda number: number * number)
|
|
90
|
+
.take(4)
|
|
91
|
+
.collect()
|
|
92
|
+
)
|
|
93
|
+
print(result) # [0, 4, 16, 36]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
asyncio.run(main())
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Streams are lazy and single-consumer:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
stream = Stream.from_iterable([1, 2, 3])
|
|
103
|
+
|
|
104
|
+
async for item in stream:
|
|
105
|
+
print(item)
|
|
106
|
+
|
|
107
|
+
# A second iteration raises StreamConsumedError.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Common terminals:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
await Stream.from_iterable([1, 2, 3]).collect() # [1, 2, 3]
|
|
114
|
+
await Stream.from_iterable([1, 2, 3]).count() # 3
|
|
115
|
+
await Stream.from_iterable([3, 1, 2]).reduce(max) # 3
|
|
116
|
+
await Stream.empty().first("fallback") # "fallback"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Async callbacks work everywhere a callback is accepted:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
users = await Stream.from_iterable(user_ids).map(fetch_user, concurrency=8).collect()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Operators
|
|
126
|
+
|
|
127
|
+
`map`, `filter`, `flat_map`, `take`, `skip`, `batch`, `buffer`, `distinct`,
|
|
128
|
+
`enumerate`, `window`, `debounce`, `throttle`, `timeout`, `retry`, `recover`,
|
|
129
|
+
`tap`, `zip`, `merge` and `concat`.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
batches = Stream.from_iterable(range(1_000)).filter(lambda number: number % 3 == 0).batch(100)
|
|
133
|
+
|
|
134
|
+
async for batch in batches:
|
|
135
|
+
print(len(batch))
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Backpressure
|
|
139
|
+
|
|
140
|
+
Buffers are bounded and block the producer by default. Data is not silently
|
|
141
|
+
dropped.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
stream = Stream.from_iterable(range(100_000)).buffer(size=64)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Lossy behaviour must be explicit and is counted:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
stream = Stream.from_iterable(range(10_000)).buffer(
|
|
151
|
+
size=16,
|
|
152
|
+
overflow="drop_oldest",
|
|
153
|
+
)
|
|
154
|
+
items = await stream.collect()
|
|
155
|
+
print(len(items), stream.stats["dropped"])
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Policies: `block`, `drop_newest`, `drop_oldest`, `error`.
|
|
159
|
+
|
|
160
|
+
## Real-time data
|
|
161
|
+
|
|
162
|
+
### WebSocket
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from datacurrent import WebSocketStream
|
|
166
|
+
|
|
167
|
+
async with WebSocketStream.connect("wss://example.com/events") as stream:
|
|
168
|
+
await stream.send_json({"subscribe": "trades"})
|
|
169
|
+
async for message in stream:
|
|
170
|
+
print(message)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Server-Sent Events
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from datacurrent import SSEStream
|
|
177
|
+
|
|
178
|
+
async with SSEStream.connect("https://example.com/events") as stream:
|
|
179
|
+
async for event in stream:
|
|
180
|
+
print(event.event, event.json())
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Streaming HTTP
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from datacurrent import HTTPStream
|
|
187
|
+
|
|
188
|
+
async with HTTPStream("https://example.com/feed.ndjson") as response:
|
|
189
|
+
async for line in response.iter_lines():
|
|
190
|
+
handle(line)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Fan-out and events
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from datacurrent import Channel, EventBus
|
|
197
|
+
|
|
198
|
+
channel = Channel(buffer_size=64)
|
|
199
|
+
first = channel.subscribe()
|
|
200
|
+
second = channel.subscribe()
|
|
201
|
+
await channel.publish("deploy finished")
|
|
202
|
+
|
|
203
|
+
bus = EventBus()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@bus.subscribe("user.*")
|
|
207
|
+
async def on_user(event):
|
|
208
|
+
print(event.topic, event.data)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
await bus.publish("user.created", {"user_id": 123})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Retries
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from datacurrent import RetryPolicy
|
|
218
|
+
|
|
219
|
+
stream = Stream.from_factory(connect_and_read).retry(
|
|
220
|
+
RetryPolicy(max_attempts=3, base_delay=0.2, jitter=0.1)
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Retries require a replayable source, such as a factory or a list. A consumed
|
|
225
|
+
generator raises `NonReplayableStreamError` instead of silently restarting.
|
|
226
|
+
|
|
227
|
+
## Why DataCurrent
|
|
228
|
+
|
|
229
|
+
- One model for iterables, queues, HTTP, SSE and WebSockets.
|
|
230
|
+
- Backpressure by default, with explicit and counted drop policies.
|
|
231
|
+
- Bounded concurrency and cancellation-safe cleanup.
|
|
232
|
+
- No infrastructure: no broker, database or external account.
|
|
233
|
+
- Explicit delivery semantics; no false durability claims.
|
|
234
|
+
- Typed API and sync or async callbacks throughout.
|
|
235
|
+
|
|
236
|
+
HTTP clients such as `httpx` and `aiohttp` handle requests; DataCurrent adds the
|
|
237
|
+
stream engine around them. Reactive libraries provide operator algebra, while
|
|
238
|
+
broker frameworks require Kafka, Redis or RabbitMQ. DataCurrent is the
|
|
239
|
+
in-process streaming layer that works without any of them.
|
|
240
|
+
|
|
241
|
+
## Compatibility and limits
|
|
242
|
+
|
|
243
|
+
- Python 3.11+ on asyncio; no Trio or AnyIO backend.
|
|
244
|
+
- In-memory `Channel` and `EventBus` delivery is at most once.
|
|
245
|
+
- No durable broker adapters or OpenTelemetry exporter in 1.0.0.
|
|
246
|
+
- Not published to PyPI; the project name is provisional.
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# DataCurrent
|
|
2
|
+
|
|
3
|
+
### Python streaming, made simple.
|
|
4
|
+
|
|
5
|
+
DataCurrent is a production-ready asynchronous streaming library. It gives you
|
|
6
|
+
one consistent API for lists, async generators, queues, WebSockets, Server-Sent
|
|
7
|
+
Events and streaming HTTP responses.
|
|
8
|
+
|
|
9
|
+
**Version 1.0.0 · Stable / Production**
|
|
10
|
+
|
|
11
|
+
- 279 tests passing, with 100% line and branch coverage.
|
|
12
|
+
- Required networking dependencies install automatically.
|
|
13
|
+
- No broker, database, API key or hosted service.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install datacurrent
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Python 3.11 or newer is required. `httpx` and `websockets` are installed with the
|
|
22
|
+
package, so HTTP streaming, SSE and WebSocket clients work immediately.
|
|
23
|
+
|
|
24
|
+
Framework helpers are optional:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install "datacurrent[fastapi]"
|
|
28
|
+
pip install "datacurrent[starlette]"
|
|
29
|
+
pip install "datacurrent[all]"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import asyncio
|
|
36
|
+
from datacurrent import Stream
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def main() -> None:
|
|
40
|
+
result = await (
|
|
41
|
+
Stream.from_iterable(range(10))
|
|
42
|
+
.filter(lambda number: number % 2 == 0)
|
|
43
|
+
.map(lambda number: number * number)
|
|
44
|
+
.take(4)
|
|
45
|
+
.collect()
|
|
46
|
+
)
|
|
47
|
+
print(result) # [0, 4, 16, 36]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
asyncio.run(main())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Streams are lazy and single-consumer:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
stream = Stream.from_iterable([1, 2, 3])
|
|
57
|
+
|
|
58
|
+
async for item in stream:
|
|
59
|
+
print(item)
|
|
60
|
+
|
|
61
|
+
# A second iteration raises StreamConsumedError.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Common terminals:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
await Stream.from_iterable([1, 2, 3]).collect() # [1, 2, 3]
|
|
68
|
+
await Stream.from_iterable([1, 2, 3]).count() # 3
|
|
69
|
+
await Stream.from_iterable([3, 1, 2]).reduce(max) # 3
|
|
70
|
+
await Stream.empty().first("fallback") # "fallback"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Async callbacks work everywhere a callback is accepted:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
users = await Stream.from_iterable(user_ids).map(fetch_user, concurrency=8).collect()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Operators
|
|
80
|
+
|
|
81
|
+
`map`, `filter`, `flat_map`, `take`, `skip`, `batch`, `buffer`, `distinct`,
|
|
82
|
+
`enumerate`, `window`, `debounce`, `throttle`, `timeout`, `retry`, `recover`,
|
|
83
|
+
`tap`, `zip`, `merge` and `concat`.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
batches = Stream.from_iterable(range(1_000)).filter(lambda number: number % 3 == 0).batch(100)
|
|
87
|
+
|
|
88
|
+
async for batch in batches:
|
|
89
|
+
print(len(batch))
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Backpressure
|
|
93
|
+
|
|
94
|
+
Buffers are bounded and block the producer by default. Data is not silently
|
|
95
|
+
dropped.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
stream = Stream.from_iterable(range(100_000)).buffer(size=64)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Lossy behaviour must be explicit and is counted:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
stream = Stream.from_iterable(range(10_000)).buffer(
|
|
105
|
+
size=16,
|
|
106
|
+
overflow="drop_oldest",
|
|
107
|
+
)
|
|
108
|
+
items = await stream.collect()
|
|
109
|
+
print(len(items), stream.stats["dropped"])
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Policies: `block`, `drop_newest`, `drop_oldest`, `error`.
|
|
113
|
+
|
|
114
|
+
## Real-time data
|
|
115
|
+
|
|
116
|
+
### WebSocket
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from datacurrent import WebSocketStream
|
|
120
|
+
|
|
121
|
+
async with WebSocketStream.connect("wss://example.com/events") as stream:
|
|
122
|
+
await stream.send_json({"subscribe": "trades"})
|
|
123
|
+
async for message in stream:
|
|
124
|
+
print(message)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Server-Sent Events
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from datacurrent import SSEStream
|
|
131
|
+
|
|
132
|
+
async with SSEStream.connect("https://example.com/events") as stream:
|
|
133
|
+
async for event in stream:
|
|
134
|
+
print(event.event, event.json())
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Streaming HTTP
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from datacurrent import HTTPStream
|
|
141
|
+
|
|
142
|
+
async with HTTPStream("https://example.com/feed.ndjson") as response:
|
|
143
|
+
async for line in response.iter_lines():
|
|
144
|
+
handle(line)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Fan-out and events
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from datacurrent import Channel, EventBus
|
|
151
|
+
|
|
152
|
+
channel = Channel(buffer_size=64)
|
|
153
|
+
first = channel.subscribe()
|
|
154
|
+
second = channel.subscribe()
|
|
155
|
+
await channel.publish("deploy finished")
|
|
156
|
+
|
|
157
|
+
bus = EventBus()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@bus.subscribe("user.*")
|
|
161
|
+
async def on_user(event):
|
|
162
|
+
print(event.topic, event.data)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
await bus.publish("user.created", {"user_id": 123})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Retries
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from datacurrent import RetryPolicy
|
|
172
|
+
|
|
173
|
+
stream = Stream.from_factory(connect_and_read).retry(
|
|
174
|
+
RetryPolicy(max_attempts=3, base_delay=0.2, jitter=0.1)
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Retries require a replayable source, such as a factory or a list. A consumed
|
|
179
|
+
generator raises `NonReplayableStreamError` instead of silently restarting.
|
|
180
|
+
|
|
181
|
+
## Why DataCurrent
|
|
182
|
+
|
|
183
|
+
- One model for iterables, queues, HTTP, SSE and WebSockets.
|
|
184
|
+
- Backpressure by default, with explicit and counted drop policies.
|
|
185
|
+
- Bounded concurrency and cancellation-safe cleanup.
|
|
186
|
+
- No infrastructure: no broker, database or external account.
|
|
187
|
+
- Explicit delivery semantics; no false durability claims.
|
|
188
|
+
- Typed API and sync or async callbacks throughout.
|
|
189
|
+
|
|
190
|
+
HTTP clients such as `httpx` and `aiohttp` handle requests; DataCurrent adds the
|
|
191
|
+
stream engine around them. Reactive libraries provide operator algebra, while
|
|
192
|
+
broker frameworks require Kafka, Redis or RabbitMQ. DataCurrent is the
|
|
193
|
+
in-process streaming layer that works without any of them.
|
|
194
|
+
|
|
195
|
+
## Compatibility and limits
|
|
196
|
+
|
|
197
|
+
- Python 3.11+ on asyncio; no Trio or AnyIO backend.
|
|
198
|
+
- In-memory `Channel` and `EventBus` delivery is at most once.
|
|
199
|
+
- No durable broker adapters or OpenTelemetry exporter in 1.0.0.
|
|
200
|
+
- Not published to PyPI; the project name is provisional.
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT.
|
|
@@ -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
|
+
]
|