pydovetail 0.4.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.
- pydovetail-0.4.0/LICENSE +21 -0
- pydovetail-0.4.0/PKG-INFO +222 -0
- pydovetail-0.4.0/README.md +202 -0
- pydovetail-0.4.0/dovetail/__init__.py +25 -0
- pydovetail-0.4.0/dovetail/_events.py +410 -0
- pydovetail-0.4.0/dovetail/_register.py +118 -0
- pydovetail-0.4.0/dovetail/_task.py +620 -0
- pydovetail-0.4.0/dovetail/_trace.py +100 -0
- pydovetail-0.4.0/dovetail/dovetail.py +302 -0
- pydovetail-0.4.0/pydovetail.egg-info/PKG-INFO +222 -0
- pydovetail-0.4.0/pydovetail.egg-info/SOURCES.txt +14 -0
- pydovetail-0.4.0/pydovetail.egg-info/dependency_links.txt +1 -0
- pydovetail-0.4.0/pydovetail.egg-info/requires.txt +7 -0
- pydovetail-0.4.0/pydovetail.egg-info/top_level.txt +1 -0
- pydovetail-0.4.0/pyproject.toml +31 -0
- pydovetail-0.4.0/setup.cfg +4 -0
pydovetail-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 anthrosystems
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pydovetail
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A lightweight helper for bridging sync and async code, built on Asyncio & ThreadPoolExecutor
|
|
5
|
+
Author-email: anthrosystems <peaceolawuyi01@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/anthrosystems/dovetail
|
|
8
|
+
Project-URL: Repository, https://github.com/anthrosystems/dovetail
|
|
9
|
+
Project-URL: Changelog, https://github.com/anthrosystems/dovetail/blob/main/CHANGELOG.md
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
16
|
+
Requires-Dist: build; extra == "dev"
|
|
17
|
+
Requires-Dist: twine; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Dovetail - Sync / Async wrapper for Python.
|
|
22
|
+
|
|
23
|
+
[](https://github.com/anthrosystems/dovetail/actions/workflows/ci.yml)
|
|
24
|
+
[](https://pypi.org/project/pydovetail/)
|
|
25
|
+
[](LICENSE)
|
|
26
|
+
[](https://github.com/anthrosystems/dovetail/releases/latest)
|
|
27
|
+
|
|
28
|
+
A lightweight helper for bridging sync and async code, built on Asyncio & ThreadPoolExecutor.
|
|
29
|
+
|
|
30
|
+
## Table of Contents
|
|
31
|
+
|
|
32
|
+
- [Why Dovetail](#why-dovetail)
|
|
33
|
+
- [Install](#install)
|
|
34
|
+
- [Quick Start](#quick-start)
|
|
35
|
+
- [API Summary](#api-summary)
|
|
36
|
+
- [Detailed Guides](docs/detailed_guide.md#L1)
|
|
37
|
+
- [Development](#development)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
## Why Dovetail?
|
|
41
|
+
|
|
42
|
+
Asyncio is powerful, but it has a cascading problem: the moment you want to
|
|
43
|
+
`await` something, that function has to be `async`, which means everything
|
|
44
|
+
calling it has to be `async`, and so on until your entire codebase has been
|
|
45
|
+
rewritten around it. If you're adding parallelism to an existing project, or
|
|
46
|
+
just want to speed up one specific bottleneck, that cost is hard to justify.
|
|
47
|
+
|
|
48
|
+
Dovetail's advantage isn't raw performance — `asyncio.gather` will always be
|
|
49
|
+
at least as fast, since Dovetail is built on top of it. The advantage of Dovetail is its
|
|
50
|
+
**surgical approach**: you can parallelise one function in an otherwise
|
|
51
|
+
synchronous codebase without touching anything else.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Existing sync code, completely unchanged around this one call
|
|
55
|
+
def process_report():
|
|
56
|
+
data = dvt.task.map_blocking(fetch_url, urls) # parallel, but sync caller
|
|
57
|
+
return summarise(data)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
With raw asyncio you can't do this — `fetch_url` being async forces
|
|
61
|
+
`process_report` to be async, which forces everything above it to be async.
|
|
62
|
+
Dovetail breaks that chain. If you're starting a greenfield project and are
|
|
63
|
+
comfortable with async/await throughout, you probably don't need it. Dovetail
|
|
64
|
+
is for the developers who are stuck between "I want parallelism" and "I don't want to rewrite everything."
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install pydovetail
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> **Note:** The install name is `pydovetail`, but the import name is `dovetail`.
|
|
74
|
+
> ```python
|
|
75
|
+
> from dovetail import Dovetail
|
|
76
|
+
> ```
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
## Quick Start
|
|
80
|
+
|
|
81
|
+
Preferred usage: create and manage a `Dovetail` instance with a context manager.
|
|
82
|
+
|
|
83
|
+
Sync (recommended for most users):
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from dovetail import Dovetail
|
|
87
|
+
|
|
88
|
+
with Dovetail(max_workers=8) as dvt:
|
|
89
|
+
# Sync caller -> sync function (blocks)
|
|
90
|
+
result = dvt.task.run_blocking(fetch_data)
|
|
91
|
+
|
|
92
|
+
# Sync caller -> sync function in threadpool (blocks)
|
|
93
|
+
result = dvt.task.to_thread_blocking(fetch_data, "target.json")
|
|
94
|
+
|
|
95
|
+
# Sync caller -> batch map in threadpool (blocks, bounded concurrency)
|
|
96
|
+
results = dvt.task.map_blocking(fetch_data, ["a.json", "b.json"], max_concurrency=4)
|
|
97
|
+
|
|
98
|
+
# run an async coroutine from sync code
|
|
99
|
+
value = dvt.task.run_blocking(fetch_data_async())
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Async (use `async with` in async applications):
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
async with Dovetail() as dvt:
|
|
106
|
+
# schedule coroutines and await
|
|
107
|
+
tasks = [dvt.task.schedule(fetch_async(i)) for i in items]
|
|
108
|
+
results = await asyncio.gather(*tasks)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Manual / library patterns (caller-managed ownership):
|
|
112
|
+
|
|
113
|
+
```py
|
|
114
|
+
# Manual creation: by default Dovetail will attempt to shutdown on
|
|
115
|
+
# interpreter exit and on SIGINT/SIGTERM. You can opt out by passing
|
|
116
|
+
# `shutdown_on_exit=False` when constructing the instance.
|
|
117
|
+
dvt = Dovetail() # shutdown_on_exit enabled by default
|
|
118
|
+
try:
|
|
119
|
+
results = dvt.task.map_blocking(fetch, items)
|
|
120
|
+
finally:
|
|
121
|
+
# explicit shutdown is still allowed and idempotent
|
|
122
|
+
dvt.shutdown()
|
|
123
|
+
|
|
124
|
+
# Library-author pattern (prefer caller-managed instance when possible)
|
|
125
|
+
def process(items, dvt: Optional[Dovetail] = None):
|
|
126
|
+
if dvt is None:
|
|
127
|
+
with Dovetail() as _dvt:
|
|
128
|
+
return _dvt.task.map_blocking(worker, items)
|
|
129
|
+
return dvt.task.map_blocking(worker, items)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
> **Note:** `Dovetail` will, by default, register a best-effort shutdown
|
|
133
|
+
> handler that calls `shutdown()` on interpreter exit and when `SIGINT` or
|
|
134
|
+
> `SIGTERM` are received. This is opt-outable via `shutdown_on_exit=False`.
|
|
135
|
+
> Explicitly calling `dvt.shutdown()` is still supported and idempotent.
|
|
136
|
+
>
|
|
137
|
+
> Sync context manager (recommended for synchronous code):
|
|
138
|
+
> ```python
|
|
139
|
+
> with Dovetail(max_workers=8) as dvt:
|
|
140
|
+
> results = dvt.task.map_blocking(fetch, items)
|
|
141
|
+
> # threadpool shut down on block exit
|
|
142
|
+
> ```
|
|
143
|
+
>
|
|
144
|
+
> Async context manager (recommended for async code):
|
|
145
|
+
> ```python
|
|
146
|
+
> async def main():
|
|
147
|
+
> async with Dovetail() as dvt:
|
|
148
|
+
> tasks = [dvt.task.schedule(coro(i)) for i in items]
|
|
149
|
+
> await asyncio.gather(*tasks)
|
|
150
|
+
> # __aexit__ runs shutdown off the event loop to avoid blocking
|
|
151
|
+
> ```
|
|
152
|
+
>
|
|
153
|
+
> Implementation note: `Dovetail` supports both `__enter__/__exit__` and
|
|
154
|
+
> `__aenter__/__aexit__`. The async exit runs `shutdown()` in a background
|
|
155
|
+
> executor to avoid blocking the running loop. `shutdown()` is idempotent and
|
|
156
|
+
> safe to call multiple times.
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## API Summary
|
|
160
|
+
|
|
161
|
+
**Task execution**
|
|
162
|
+
|
|
163
|
+
- `dvt.task.to_thread(func, *args, **kwargs)` — await a sync callable in Dovetail's threadpool from async code; supports default timeout/retry settings.
|
|
164
|
+
- `dvt.task.to_thread_blocking(func, *args, **kwargs)` — synchronous wrapper for `to_thread` when you are not already in an event loop.
|
|
165
|
+
- `dvt.task.schedule(coro_or_callable, *args, **kwargs)` — create and return an `asyncio.Task` from a coroutine object, async function, or sync callable.
|
|
166
|
+
- `dvt.task.run_blocking(func_or_coro, *args, **kwargs)` — run async or sync work from synchronous code. Do not call from inside a running event loop.
|
|
167
|
+
- `dvt.task.map_blocking(func, items, max_concurrency=None, return_exceptions=False)` — run an ordered parallel map over `items` using the threadpool.
|
|
168
|
+
|
|
169
|
+
**Events**
|
|
170
|
+
|
|
171
|
+
- `dvt.events.on_queued(...)` — fires when a task is queued.
|
|
172
|
+
- `dvt.events.on_start(...)` — fires when a task starts executing.
|
|
173
|
+
- `dvt.events.on_end(...)` — fires when a task resolves (including after retries).
|
|
174
|
+
- `dvt.events.on_error(...)` — fires when a task raises an exception.
|
|
175
|
+
- `dvt.events.on_retry(...)` — fires on each retry attempt.
|
|
176
|
+
- `dvt.events.on_cancel(...)` — fires when a task is cancelled.
|
|
177
|
+
- `dvt.events.stats()` — returns cumulative counters: `queued`, `started`, `done`, `error`, `retries`, `throttled`.
|
|
178
|
+
|
|
179
|
+
**Registry (auto-registration)**
|
|
180
|
+
|
|
181
|
+
- `dovetail` now exposes a lightweight registry to track active `Dovetail` instances. By default `Dovetail(..., auto_register=True)` will register itself with the package registry so applications can perform coordinated shutdown.
|
|
182
|
+
- Public helpers:
|
|
183
|
+
- `from dovetail import register, unregister, list_active, shutdown_all, set_app_shutdown_hook`
|
|
184
|
+
- `register(dvt)` — explicitly register an instance (optional; instances auto-register by default).
|
|
185
|
+
- `unregister(dvt)` — remove from the registry (useful if you manage shutdown yourself).
|
|
186
|
+
- `list_active()` — return a list of active (live) instances.
|
|
187
|
+
- `shutdown_all(wait=True)` — best-effort shutdown of all registered instances (useful for app shutdown hooks).
|
|
188
|
+
- `set_app_shutdown_hook(fn)` — provide an application-level hook that the registry will call during process exit.
|
|
189
|
+
|
|
190
|
+
Notes:
|
|
191
|
+
- The registry uses weakrefs and logs a warning if a `Dovetail` is garbage-collected without an explicit `shutdown()` call.
|
|
192
|
+
- To opt-out of auto-registration when creating a `Dovetail`, pass `auto_register=False` to the constructor.
|
|
193
|
+
|
|
194
|
+
All `dvt.events.on_*` methods accept these optional scope parameters:
|
|
195
|
+
|
|
196
|
+
- `function_target` — only receive events for a specific callable.
|
|
197
|
+
- `instance_target` — only receive events for a specific execution id (available
|
|
198
|
+
as `id` in the event payload).
|
|
199
|
+
- `allow_reentry=False` — prevents a listener from triggering itself recursively.
|
|
200
|
+
Set to `True` to allow reentry, bounded by `max_chain_depth`.
|
|
201
|
+
- `max_chain_depth=5` — maximum active callback depth when reentry is enabled.
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
## Detailed Guides
|
|
205
|
+
|
|
206
|
+
Full, expanded guides live in the repository under [docs/detailed_guide.md](docs/detailed_guide.md#L1).
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
## Development
|
|
210
|
+
|
|
211
|
+
Run tests locally:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
python -m pytest -q
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Contributions welcome — open an issue or PR.
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT. See `CHANGELOG.md` for release notes.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Dovetail - Sync / Async wrapper for Python.
|
|
2
|
+
|
|
3
|
+
[](https://github.com/anthrosystems/dovetail/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/pydovetail/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/anthrosystems/dovetail/releases/latest)
|
|
7
|
+
|
|
8
|
+
A lightweight helper for bridging sync and async code, built on Asyncio & ThreadPoolExecutor.
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
- [Why Dovetail](#why-dovetail)
|
|
13
|
+
- [Install](#install)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [API Summary](#api-summary)
|
|
16
|
+
- [Detailed Guides](docs/detailed_guide.md#L1)
|
|
17
|
+
- [Development](#development)
|
|
18
|
+
- [License](#license)
|
|
19
|
+
|
|
20
|
+
## Why Dovetail?
|
|
21
|
+
|
|
22
|
+
Asyncio is powerful, but it has a cascading problem: the moment you want to
|
|
23
|
+
`await` something, that function has to be `async`, which means everything
|
|
24
|
+
calling it has to be `async`, and so on until your entire codebase has been
|
|
25
|
+
rewritten around it. If you're adding parallelism to an existing project, or
|
|
26
|
+
just want to speed up one specific bottleneck, that cost is hard to justify.
|
|
27
|
+
|
|
28
|
+
Dovetail's advantage isn't raw performance — `asyncio.gather` will always be
|
|
29
|
+
at least as fast, since Dovetail is built on top of it. The advantage of Dovetail is its
|
|
30
|
+
**surgical approach**: you can parallelise one function in an otherwise
|
|
31
|
+
synchronous codebase without touching anything else.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
# Existing sync code, completely unchanged around this one call
|
|
35
|
+
def process_report():
|
|
36
|
+
data = dvt.task.map_blocking(fetch_url, urls) # parallel, but sync caller
|
|
37
|
+
return summarise(data)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
With raw asyncio you can't do this — `fetch_url` being async forces
|
|
41
|
+
`process_report` to be async, which forces everything above it to be async.
|
|
42
|
+
Dovetail breaks that chain. If you're starting a greenfield project and are
|
|
43
|
+
comfortable with async/await throughout, you probably don't need it. Dovetail
|
|
44
|
+
is for the developers who are stuck between "I want parallelism" and "I don't want to rewrite everything."
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install pydovetail
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> **Note:** The install name is `pydovetail`, but the import name is `dovetail`.
|
|
54
|
+
> ```python
|
|
55
|
+
> from dovetail import Dovetail
|
|
56
|
+
> ```
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
Preferred usage: create and manage a `Dovetail` instance with a context manager.
|
|
62
|
+
|
|
63
|
+
Sync (recommended for most users):
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from dovetail import Dovetail
|
|
67
|
+
|
|
68
|
+
with Dovetail(max_workers=8) as dvt:
|
|
69
|
+
# Sync caller -> sync function (blocks)
|
|
70
|
+
result = dvt.task.run_blocking(fetch_data)
|
|
71
|
+
|
|
72
|
+
# Sync caller -> sync function in threadpool (blocks)
|
|
73
|
+
result = dvt.task.to_thread_blocking(fetch_data, "target.json")
|
|
74
|
+
|
|
75
|
+
# Sync caller -> batch map in threadpool (blocks, bounded concurrency)
|
|
76
|
+
results = dvt.task.map_blocking(fetch_data, ["a.json", "b.json"], max_concurrency=4)
|
|
77
|
+
|
|
78
|
+
# run an async coroutine from sync code
|
|
79
|
+
value = dvt.task.run_blocking(fetch_data_async())
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Async (use `async with` in async applications):
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
async with Dovetail() as dvt:
|
|
86
|
+
# schedule coroutines and await
|
|
87
|
+
tasks = [dvt.task.schedule(fetch_async(i)) for i in items]
|
|
88
|
+
results = await asyncio.gather(*tasks)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Manual / library patterns (caller-managed ownership):
|
|
92
|
+
|
|
93
|
+
```py
|
|
94
|
+
# Manual creation: by default Dovetail will attempt to shutdown on
|
|
95
|
+
# interpreter exit and on SIGINT/SIGTERM. You can opt out by passing
|
|
96
|
+
# `shutdown_on_exit=False` when constructing the instance.
|
|
97
|
+
dvt = Dovetail() # shutdown_on_exit enabled by default
|
|
98
|
+
try:
|
|
99
|
+
results = dvt.task.map_blocking(fetch, items)
|
|
100
|
+
finally:
|
|
101
|
+
# explicit shutdown is still allowed and idempotent
|
|
102
|
+
dvt.shutdown()
|
|
103
|
+
|
|
104
|
+
# Library-author pattern (prefer caller-managed instance when possible)
|
|
105
|
+
def process(items, dvt: Optional[Dovetail] = None):
|
|
106
|
+
if dvt is None:
|
|
107
|
+
with Dovetail() as _dvt:
|
|
108
|
+
return _dvt.task.map_blocking(worker, items)
|
|
109
|
+
return dvt.task.map_blocking(worker, items)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> **Note:** `Dovetail` will, by default, register a best-effort shutdown
|
|
113
|
+
> handler that calls `shutdown()` on interpreter exit and when `SIGINT` or
|
|
114
|
+
> `SIGTERM` are received. This is opt-outable via `shutdown_on_exit=False`.
|
|
115
|
+
> Explicitly calling `dvt.shutdown()` is still supported and idempotent.
|
|
116
|
+
>
|
|
117
|
+
> Sync context manager (recommended for synchronous code):
|
|
118
|
+
> ```python
|
|
119
|
+
> with Dovetail(max_workers=8) as dvt:
|
|
120
|
+
> results = dvt.task.map_blocking(fetch, items)
|
|
121
|
+
> # threadpool shut down on block exit
|
|
122
|
+
> ```
|
|
123
|
+
>
|
|
124
|
+
> Async context manager (recommended for async code):
|
|
125
|
+
> ```python
|
|
126
|
+
> async def main():
|
|
127
|
+
> async with Dovetail() as dvt:
|
|
128
|
+
> tasks = [dvt.task.schedule(coro(i)) for i in items]
|
|
129
|
+
> await asyncio.gather(*tasks)
|
|
130
|
+
> # __aexit__ runs shutdown off the event loop to avoid blocking
|
|
131
|
+
> ```
|
|
132
|
+
>
|
|
133
|
+
> Implementation note: `Dovetail` supports both `__enter__/__exit__` and
|
|
134
|
+
> `__aenter__/__aexit__`. The async exit runs `shutdown()` in a background
|
|
135
|
+
> executor to avoid blocking the running loop. `shutdown()` is idempotent and
|
|
136
|
+
> safe to call multiple times.
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## API Summary
|
|
140
|
+
|
|
141
|
+
**Task execution**
|
|
142
|
+
|
|
143
|
+
- `dvt.task.to_thread(func, *args, **kwargs)` — await a sync callable in Dovetail's threadpool from async code; supports default timeout/retry settings.
|
|
144
|
+
- `dvt.task.to_thread_blocking(func, *args, **kwargs)` — synchronous wrapper for `to_thread` when you are not already in an event loop.
|
|
145
|
+
- `dvt.task.schedule(coro_or_callable, *args, **kwargs)` — create and return an `asyncio.Task` from a coroutine object, async function, or sync callable.
|
|
146
|
+
- `dvt.task.run_blocking(func_or_coro, *args, **kwargs)` — run async or sync work from synchronous code. Do not call from inside a running event loop.
|
|
147
|
+
- `dvt.task.map_blocking(func, items, max_concurrency=None, return_exceptions=False)` — run an ordered parallel map over `items` using the threadpool.
|
|
148
|
+
|
|
149
|
+
**Events**
|
|
150
|
+
|
|
151
|
+
- `dvt.events.on_queued(...)` — fires when a task is queued.
|
|
152
|
+
- `dvt.events.on_start(...)` — fires when a task starts executing.
|
|
153
|
+
- `dvt.events.on_end(...)` — fires when a task resolves (including after retries).
|
|
154
|
+
- `dvt.events.on_error(...)` — fires when a task raises an exception.
|
|
155
|
+
- `dvt.events.on_retry(...)` — fires on each retry attempt.
|
|
156
|
+
- `dvt.events.on_cancel(...)` — fires when a task is cancelled.
|
|
157
|
+
- `dvt.events.stats()` — returns cumulative counters: `queued`, `started`, `done`, `error`, `retries`, `throttled`.
|
|
158
|
+
|
|
159
|
+
**Registry (auto-registration)**
|
|
160
|
+
|
|
161
|
+
- `dovetail` now exposes a lightweight registry to track active `Dovetail` instances. By default `Dovetail(..., auto_register=True)` will register itself with the package registry so applications can perform coordinated shutdown.
|
|
162
|
+
- Public helpers:
|
|
163
|
+
- `from dovetail import register, unregister, list_active, shutdown_all, set_app_shutdown_hook`
|
|
164
|
+
- `register(dvt)` — explicitly register an instance (optional; instances auto-register by default).
|
|
165
|
+
- `unregister(dvt)` — remove from the registry (useful if you manage shutdown yourself).
|
|
166
|
+
- `list_active()` — return a list of active (live) instances.
|
|
167
|
+
- `shutdown_all(wait=True)` — best-effort shutdown of all registered instances (useful for app shutdown hooks).
|
|
168
|
+
- `set_app_shutdown_hook(fn)` — provide an application-level hook that the registry will call during process exit.
|
|
169
|
+
|
|
170
|
+
Notes:
|
|
171
|
+
- The registry uses weakrefs and logs a warning if a `Dovetail` is garbage-collected without an explicit `shutdown()` call.
|
|
172
|
+
- To opt-out of auto-registration when creating a `Dovetail`, pass `auto_register=False` to the constructor.
|
|
173
|
+
|
|
174
|
+
All `dvt.events.on_*` methods accept these optional scope parameters:
|
|
175
|
+
|
|
176
|
+
- `function_target` — only receive events for a specific callable.
|
|
177
|
+
- `instance_target` — only receive events for a specific execution id (available
|
|
178
|
+
as `id` in the event payload).
|
|
179
|
+
- `allow_reentry=False` — prevents a listener from triggering itself recursively.
|
|
180
|
+
Set to `True` to allow reentry, bounded by `max_chain_depth`.
|
|
181
|
+
- `max_chain_depth=5` — maximum active callback depth when reentry is enabled.
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
## Detailed Guides
|
|
185
|
+
|
|
186
|
+
Full, expanded guides live in the repository under [docs/detailed_guide.md](docs/detailed_guide.md#L1).
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
Run tests locally:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
python -m pytest -q
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Contributions welcome — open an issue or PR.
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT. See `CHANGELOG.md` for release notes.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Package exports.
|
|
3
|
+
|
|
4
|
+
This package intentionally does not provide a module-level `dovetail`
|
|
5
|
+
instance. Consumers should instantiate `Dovetail()` where they need
|
|
6
|
+
an executor helper so they control lifetimes and configuration.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .dovetail import Dovetail
|
|
10
|
+
from ._register import (
|
|
11
|
+
register,
|
|
12
|
+
unregister,
|
|
13
|
+
list_active,
|
|
14
|
+
shutdown_all,
|
|
15
|
+
set_app_shutdown_hook,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Dovetail",
|
|
20
|
+
"register",
|
|
21
|
+
"unregister",
|
|
22
|
+
"list_active",
|
|
23
|
+
"shutdown_all",
|
|
24
|
+
"set_app_shutdown_hook",
|
|
25
|
+
]
|