tasklane 0.1.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.
- tasklane-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- tasklane-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- tasklane-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +15 -0
- tasklane-0.1.0/.github/dependabot.yml +21 -0
- tasklane-0.1.0/.github/workflows/ai-review.yml +59 -0
- tasklane-0.1.0/.github/workflows/ci.yml +54 -0
- tasklane-0.1.0/.github/workflows/release.yml +48 -0
- tasklane-0.1.0/.gitignore +27 -0
- tasklane-0.1.0/CHANGELOG.md +34 -0
- tasklane-0.1.0/CODE_OF_CONDUCT.md +57 -0
- tasklane-0.1.0/CONTRIBUTING.md +62 -0
- tasklane-0.1.0/LICENSE +21 -0
- tasklane-0.1.0/PKG-INFO +226 -0
- tasklane-0.1.0/README.md +199 -0
- tasklane-0.1.0/pyproject.toml +112 -0
- tasklane-0.1.0/scripts/ai_review.py +154 -0
- tasklane-0.1.0/src/tasklane/__init__.py +40 -0
- tasklane-0.1.0/src/tasklane/_core.py +492 -0
- tasklane-0.1.0/src/tasklane/_lane.py +100 -0
- tasklane-0.1.0/src/tasklane/_progress.py +55 -0
- tasklane-0.1.0/src/tasklane/_ratelimit.py +44 -0
- tasklane-0.1.0/src/tasklane/_retry.py +89 -0
- tasklane-0.1.0/src/tasklane/py.typed +0 -0
- tasklane-0.1.0/tests/__init__.py +0 -0
- tasklane-0.1.0/tests/test_amap.py +128 -0
- tasklane-0.1.0/tests/test_cancellation.py +69 -0
- tasklane-0.1.0/tests/test_gather.py +72 -0
- tasklane-0.1.0/tests/test_lane.py +68 -0
- tasklane-0.1.0/tests/test_progress.py +62 -0
- tasklane-0.1.0/tests/test_ratelimit.py +58 -0
- tasklane-0.1.0/tests/test_retry.py +100 -0
- tasklane-0.1.0/tests/test_stream.py +70 -0
- tasklane-0.1.0/uv.lock +537 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Report something that isn't working as documented
|
|
4
|
+
title: ""
|
|
5
|
+
labels: bug
|
|
6
|
+
assignees: ""
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
**Describe the bug**
|
|
10
|
+
A clear and concise description of what the bug is.
|
|
11
|
+
|
|
12
|
+
**Minimal reproducible example**
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import asyncio
|
|
16
|
+
import tasklane
|
|
17
|
+
|
|
18
|
+
# ... the smallest snippet that reproduces the problem
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Expected behavior**
|
|
22
|
+
What you expected to happen.
|
|
23
|
+
|
|
24
|
+
**Actual behavior**
|
|
25
|
+
What actually happened, including the full traceback if there is one.
|
|
26
|
+
|
|
27
|
+
**Environment**
|
|
28
|
+
|
|
29
|
+
- tasklane version:
|
|
30
|
+
- Python version:
|
|
31
|
+
- OS:
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature request
|
|
3
|
+
about: Suggest an idea for tasklane
|
|
4
|
+
title: ""
|
|
5
|
+
labels: enhancement
|
|
6
|
+
assignees: ""
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
**What problem are you trying to solve?**
|
|
10
|
+
A clear description of the use case or pain point.
|
|
11
|
+
|
|
12
|
+
**Proposed solution**
|
|
13
|
+
What you'd like to see. A sketch of the API you have in mind helps a lot.
|
|
14
|
+
|
|
15
|
+
**Alternatives considered**
|
|
16
|
+
Any workarounds you're using today, or other approaches you weighed.
|
|
17
|
+
|
|
18
|
+
**Additional context**
|
|
19
|
+
Anything else relevant — links, prior art in other libraries, etc.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
<!-- What does this PR change, and why? -->
|
|
4
|
+
|
|
5
|
+
## Checklist
|
|
6
|
+
|
|
7
|
+
- [ ] Tests added or updated (and `uv run pytest` passes)
|
|
8
|
+
- [ ] `uv run ruff check .` and `uv run ruff format --check .` pass
|
|
9
|
+
- [ ] `uv run mypy` passes
|
|
10
|
+
- [ ] Docstrings / README updated if behavior changed
|
|
11
|
+
- [ ] Added a `CHANGELOG.md` entry under `Unreleased`
|
|
12
|
+
|
|
13
|
+
## Notes for reviewers
|
|
14
|
+
|
|
15
|
+
<!-- Anything that needs special attention, trade-offs, follow-ups, etc. -->
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "github-actions"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "weekly"
|
|
7
|
+
groups:
|
|
8
|
+
actions:
|
|
9
|
+
patterns: ["*"]
|
|
10
|
+
commit-message:
|
|
11
|
+
prefix: "ci"
|
|
12
|
+
|
|
13
|
+
- package-ecosystem: "uv"
|
|
14
|
+
directory: "/"
|
|
15
|
+
schedule:
|
|
16
|
+
interval: "weekly"
|
|
17
|
+
groups:
|
|
18
|
+
dev-dependencies:
|
|
19
|
+
patterns: ["*"]
|
|
20
|
+
commit-message:
|
|
21
|
+
prefix: "deps"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: AI review
|
|
2
|
+
|
|
3
|
+
# Advisory Codex review on pull requests. Runs only when the OPENAI_API_KEY
|
|
4
|
+
# secret is configured, which means it never runs for fork PRs (secrets are not
|
|
5
|
+
# exposed to forks). The review is posted as a single sticky comment and never
|
|
6
|
+
# blocks a merge. Override the model with the OPENAI_MODEL repository variable.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
pull_request:
|
|
10
|
+
types: [opened, synchronize, reopened]
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
pull-requests: write
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: ai-review-${{ github.event.pull_request.number }}
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
codex-review:
|
|
22
|
+
name: Codex review
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v6
|
|
26
|
+
with:
|
|
27
|
+
fetch-depth: 0
|
|
28
|
+
- name: Check for API key
|
|
29
|
+
id: guard
|
|
30
|
+
env:
|
|
31
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
32
|
+
run: |
|
|
33
|
+
if [ -n "$OPENAI_API_KEY" ]; then
|
|
34
|
+
echo "enabled=true" >> "$GITHUB_OUTPUT"
|
|
35
|
+
else
|
|
36
|
+
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
|
37
|
+
echo "OPENAI_API_KEY not set; skipping AI review (this is fine)."
|
|
38
|
+
fi
|
|
39
|
+
- name: Set up Python
|
|
40
|
+
if: steps.guard.outputs.enabled == 'true'
|
|
41
|
+
uses: actions/setup-python@v6
|
|
42
|
+
with:
|
|
43
|
+
python-version: "3.12"
|
|
44
|
+
- name: Collect PR diff
|
|
45
|
+
if: steps.guard.outputs.enabled == 'true'
|
|
46
|
+
env:
|
|
47
|
+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
48
|
+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
49
|
+
run: git diff "$BASE_SHA...$HEAD_SHA" > pr.diff
|
|
50
|
+
- name: Run Codex review
|
|
51
|
+
if: steps.guard.outputs.enabled == 'true'
|
|
52
|
+
env:
|
|
53
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
54
|
+
OPENAI_MODEL: ${{ vars.OPENAI_MODEL }}
|
|
55
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
56
|
+
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
57
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
58
|
+
DIFF_FILE: pr.diff
|
|
59
|
+
run: python3 scripts/ai_review.py
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
lint:
|
|
18
|
+
name: lint & type-check
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v7
|
|
24
|
+
with:
|
|
25
|
+
enable-cache: true
|
|
26
|
+
- name: Sync dependencies
|
|
27
|
+
run: uv sync --locked
|
|
28
|
+
- name: Ruff (lint)
|
|
29
|
+
run: uv run ruff check --output-format=github .
|
|
30
|
+
- name: Ruff (format)
|
|
31
|
+
run: uv run ruff format --check .
|
|
32
|
+
- name: Mypy
|
|
33
|
+
run: uv run mypy
|
|
34
|
+
|
|
35
|
+
test:
|
|
36
|
+
name: test (py${{ matrix.python-version }})
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
strategy:
|
|
39
|
+
fail-fast: false
|
|
40
|
+
matrix:
|
|
41
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v6
|
|
44
|
+
- name: Install uv
|
|
45
|
+
uses: astral-sh/setup-uv@v7
|
|
46
|
+
with:
|
|
47
|
+
python-version: ${{ matrix.python-version }}
|
|
48
|
+
enable-cache: true
|
|
49
|
+
- name: Sync dependencies
|
|
50
|
+
run: uv sync --locked
|
|
51
|
+
- name: Run tests with coverage
|
|
52
|
+
run: |
|
|
53
|
+
uv run coverage run -m pytest
|
|
54
|
+
uv run coverage report
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Build distributions
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v7
|
|
19
|
+
with:
|
|
20
|
+
enable-cache: true
|
|
21
|
+
- name: Build sdist and wheel
|
|
22
|
+
run: uv build
|
|
23
|
+
- name: Check distribution metadata
|
|
24
|
+
run: uvx twine check dist/*
|
|
25
|
+
- name: Upload artifacts
|
|
26
|
+
uses: actions/upload-artifact@v7
|
|
27
|
+
with:
|
|
28
|
+
name: dist
|
|
29
|
+
path: dist/
|
|
30
|
+
|
|
31
|
+
publish:
|
|
32
|
+
name: Publish to PyPI
|
|
33
|
+
needs: build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
# Trusted Publishing: configure this environment on PyPI, no API token needed.
|
|
36
|
+
environment:
|
|
37
|
+
name: pypi
|
|
38
|
+
url: https://pypi.org/p/tasklane
|
|
39
|
+
permissions:
|
|
40
|
+
id-token: write
|
|
41
|
+
steps:
|
|
42
|
+
- name: Download artifacts
|
|
43
|
+
uses: actions/download-artifact@v8
|
|
44
|
+
with:
|
|
45
|
+
name: dist
|
|
46
|
+
path: dist/
|
|
47
|
+
- name: Publish
|
|
48
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
|
|
14
|
+
# Tooling caches
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
.coverage.*
|
|
20
|
+
htmlcov/
|
|
21
|
+
coverage.xml
|
|
22
|
+
|
|
23
|
+
# Editors / OS
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
.claude/
|
|
27
|
+
.DS_Store
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-06-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `amap()` — concurrent map with a bounded concurrency limit, returning results
|
|
14
|
+
in input order. Supports sync and async iterables in constant memory.
|
|
15
|
+
- `stream()` — async iterator yielding results in completion order.
|
|
16
|
+
- `gather()` — a concurrency-limited drop-in for `asyncio.gather`, closing any
|
|
17
|
+
un-awaited coroutines on fail-fast.
|
|
18
|
+
- `Lane` — an immutable, reusable bundle of settings.
|
|
19
|
+
- Retries with `Backoff` strategies (`exponential`, `linear`, `constant`) and a
|
|
20
|
+
flexible `retry_on` (type, tuple, or predicate).
|
|
21
|
+
- Per-task `timeout` and per-second `rate_limit`.
|
|
22
|
+
- `Progress` snapshots via an `on_progress` callback.
|
|
23
|
+
- `return_exceptions` to collect failures instead of raising.
|
|
24
|
+
- Fully typed, `py.typed`, zero runtime dependencies, Python 3.10–3.14.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `timeout` always surfaces the builtin `TimeoutError`. On Python 3.10,
|
|
29
|
+
`asyncio.wait_for` raises the distinct `asyncio.TimeoutError` (the two types
|
|
30
|
+
were unified in 3.11), so `except TimeoutError` and `retry_on=TimeoutError`
|
|
31
|
+
now behave consistently across Python 3.10–3.14.
|
|
32
|
+
|
|
33
|
+
[Unreleased]: https://github.com/jpwm2/tasklane/compare/v0.1.0...HEAD
|
|
34
|
+
[0.1.0]: https://github.com/jpwm2/tasklane/releases/tag/v0.1.0
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
|
10
|
+
orientation.
|
|
11
|
+
|
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
+
diverse, inclusive, and healthy community.
|
|
14
|
+
|
|
15
|
+
## Our Standards
|
|
16
|
+
|
|
17
|
+
Examples of behavior that contributes to a positive environment include:
|
|
18
|
+
|
|
19
|
+
- Demonstrating empathy and kindness toward other people
|
|
20
|
+
- Being respectful of differing opinions, viewpoints, and experiences
|
|
21
|
+
- Giving and gracefully accepting constructive feedback
|
|
22
|
+
- Accepting responsibility and apologizing to those affected by our mistakes
|
|
23
|
+
- Focusing on what is best for the overall community
|
|
24
|
+
|
|
25
|
+
Examples of unacceptable behavior include:
|
|
26
|
+
|
|
27
|
+
- The use of sexualized language or imagery, and sexual attention or advances
|
|
28
|
+
- Trolling, insulting or derogatory comments, and personal or political attacks
|
|
29
|
+
- Public or private harassment
|
|
30
|
+
- Publishing others' private information without explicit permission
|
|
31
|
+
- Other conduct which could reasonably be considered inappropriate
|
|
32
|
+
|
|
33
|
+
## Enforcement Responsibilities
|
|
34
|
+
|
|
35
|
+
Community maintainers are responsible for clarifying and enforcing our standards
|
|
36
|
+
and will take appropriate and fair corrective action in response to any behavior
|
|
37
|
+
they deem inappropriate, threatening, offensive, or harmful.
|
|
38
|
+
|
|
39
|
+
## Scope
|
|
40
|
+
|
|
41
|
+
This Code of Conduct applies within all community spaces, and also applies when
|
|
42
|
+
an individual is officially representing the community in public spaces.
|
|
43
|
+
|
|
44
|
+
## Enforcement
|
|
45
|
+
|
|
46
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
47
|
+
reported to the maintainers privately via
|
|
48
|
+
[GitHub Security Advisories](https://github.com/jpwm2/tasklane/security/advisories/new).
|
|
49
|
+
All complaints will be reviewed and investigated promptly and fairly.
|
|
50
|
+
|
|
51
|
+
## Attribution
|
|
52
|
+
|
|
53
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
54
|
+
version 2.1, available at
|
|
55
|
+
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
|
|
56
|
+
|
|
57
|
+
[homepage]: https://www.contributor-covenant.org
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Contributing to tasklane
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in improving `tasklane`! This project aims to be small,
|
|
4
|
+
correct, and a pleasure to depend on, so contributions of all sizes are welcome —
|
|
5
|
+
bug reports, docs, tests, and features alike.
|
|
6
|
+
|
|
7
|
+
## Development setup
|
|
8
|
+
|
|
9
|
+
`tasklane` uses [uv](https://docs.astral.sh/uv/) for environment management.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
git clone https://github.com/jpwm2/tasklane
|
|
13
|
+
cd tasklane
|
|
14
|
+
uv sync
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## The checks that CI runs
|
|
18
|
+
|
|
19
|
+
Please make sure these pass locally before opening a pull request:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv run pytest # test suite
|
|
23
|
+
uv run ruff check . # lint
|
|
24
|
+
uv run ruff format --check . # formatting
|
|
25
|
+
uv run mypy # static types (strict)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
To auto-fix formatting and lint:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv run ruff format .
|
|
32
|
+
uv run ruff check --fix .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Automated review
|
|
36
|
+
|
|
37
|
+
Pull requests opened from this repository receive an **advisory** Codex review
|
|
38
|
+
that posts a single sticky comment (it only runs when a maintainer has configured
|
|
39
|
+
the `OPENAI_API_KEY` secret, so it never runs on fork PRs). It is a helper, not a
|
|
40
|
+
gate — a green CI run is what's required to merge.
|
|
41
|
+
|
|
42
|
+
## Guidelines
|
|
43
|
+
|
|
44
|
+
- **Keep the runtime dependency-free.** A core promise of `tasklane` is zero
|
|
45
|
+
third-party runtime dependencies. Dev/test dependencies are fine.
|
|
46
|
+
- **Stay typed.** The codebase passes `mypy --strict`. New public API needs full
|
|
47
|
+
type annotations.
|
|
48
|
+
- **Add tests.** Bug fixes should come with a regression test; features with
|
|
49
|
+
tests covering the happy path and the obvious edge cases.
|
|
50
|
+
- **Update the docs.** If you change behavior, update the README and docstrings.
|
|
51
|
+
- **Add a changelog entry** under the `Unreleased` heading in
|
|
52
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
53
|
+
|
|
54
|
+
## Reporting bugs and requesting features
|
|
55
|
+
|
|
56
|
+
Open an issue using one of the templates. For bugs, a minimal reproducible
|
|
57
|
+
example and your Python version go a long way.
|
|
58
|
+
|
|
59
|
+
## Code of Conduct
|
|
60
|
+
|
|
61
|
+
This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md). By
|
|
62
|
+
participating, you are expected to uphold it.
|
tasklane-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tasklane contributors
|
|
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.
|
tasklane-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tasklane
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bounded-concurrency async for Python: run, map, and stream awaitables with limits, retries, rate limiting, and progress — in one typed call.
|
|
5
|
+
Project-URL: Homepage, https://github.com/jpwm2/tasklane
|
|
6
|
+
Project-URL: Repository, https://github.com/jpwm2/tasklane
|
|
7
|
+
Project-URL: Issues, https://github.com/jpwm2/tasklane/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/jpwm2/tasklane/blob/main/CHANGELOG.md
|
|
9
|
+
Author: jpwm2
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: async,asyncio,concurrency,gather,parallel,rate-limit,retry,semaphore,task-pool,throttle
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# tasklane
|
|
29
|
+
|
|
30
|
+
**Bounded-concurrency async for Python — run, map, and stream awaitables with a
|
|
31
|
+
concurrency limit, retries, backoff, rate limiting, and progress, in one typed call.**
|
|
32
|
+
|
|
33
|
+
[](https://github.com/jpwm2/tasklane/actions/workflows/ci.yml)
|
|
34
|
+
[](https://pypi.org/project/tasklane/)
|
|
35
|
+
[](https://pypi.org/project/tasklane/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
[](src/tasklane/py.typed)
|
|
38
|
+
[](https://github.com/astral-sh/ruff)
|
|
39
|
+
|
|
40
|
+
Every Python project that fans out async work eventually rewrites the same block:
|
|
41
|
+
an `asyncio.Semaphore` to cap concurrency, a `try/except` retry loop, a counter
|
|
42
|
+
for progress, maybe a sleep to stay under a rate limit. `tasklane` is that block,
|
|
43
|
+
done once — correct, fully typed, and **zero runtime dependencies**.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import asyncio
|
|
47
|
+
import httpx
|
|
48
|
+
import tasklane
|
|
49
|
+
|
|
50
|
+
async def fetch(url: str) -> int:
|
|
51
|
+
async with httpx.AsyncClient() as client:
|
|
52
|
+
return len((await client.get(url)).text)
|
|
53
|
+
|
|
54
|
+
async def main() -> None:
|
|
55
|
+
urls = [f"https://example.com/{i}" for i in range(1000)]
|
|
56
|
+
|
|
57
|
+
sizes = await tasklane.amap(
|
|
58
|
+
fetch, urls,
|
|
59
|
+
limit=20, # at most 20 requests in flight
|
|
60
|
+
retries=3, # retry failures up to 3x with exponential backoff
|
|
61
|
+
rate_limit=50, # start at most 50 requests per second
|
|
62
|
+
timeout=10, # per-attempt timeout (seconds)
|
|
63
|
+
)
|
|
64
|
+
print(sum(sizes))
|
|
65
|
+
|
|
66
|
+
asyncio.run(main())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install tasklane
|
|
73
|
+
# or
|
|
74
|
+
uv add tasklane
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Requires Python 3.10+. No third-party dependencies.
|
|
78
|
+
|
|
79
|
+
## Why not just `asyncio.gather`?
|
|
80
|
+
|
|
81
|
+
`asyncio.gather` starts **everything at once**. Fan out 10,000 requests and you
|
|
82
|
+
open 10,000 sockets, trip rate limits, and OOM. The usual fixes are scattered
|
|
83
|
+
across the stdlib and third-party libs; `tasklane` brings them together:
|
|
84
|
+
|
|
85
|
+
| | `asyncio.gather` | `Semaphore` + `gather` | `aiometer` | **tasklane** |
|
|
86
|
+
| ---------------------------- | :--------------: | :--------------------: | :--------: | :----------: |
|
|
87
|
+
| Concurrency limit | ✗ | manual | ✓ | ✓ |
|
|
88
|
+
| Results in input order | ✓ | ✓ | ✓ | ✓ |
|
|
89
|
+
| Stream results as completed | `as_completed` | manual | ✓ | ✓ |
|
|
90
|
+
| Retries + backoff | ✗ | ✗ | ✗ | ✓ |
|
|
91
|
+
| Rate limiting (per second) | ✗ | ✗ | ✓ | ✓ |
|
|
92
|
+
| Progress callbacks | ✗ | ✗ | ✗ | ✓ |
|
|
93
|
+
| Per-task timeout | ✗ | manual | ✗ | ✓ |
|
|
94
|
+
| Backpressure on huge inputs | ✗ | manual | ✓ | ✓ |
|
|
95
|
+
| Runtime dependencies | stdlib | stdlib | `anyio` | **none** |
|
|
96
|
+
|
|
97
|
+
## Features
|
|
98
|
+
|
|
99
|
+
### `amap` — concurrent map, results in order
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
results = await tasklane.amap(fetch, urls, limit=10)
|
|
103
|
+
# results[i] corresponds to urls[i]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Accepts both sync and **async** iterables, and works in constant memory thanks to
|
|
107
|
+
a bounded internal queue — you can map over a million-item generator without
|
|
108
|
+
materializing a million tasks.
|
|
109
|
+
|
|
110
|
+
### `stream` — react to results as they finish
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
async for size in tasklane.stream(fetch, urls, limit=10):
|
|
114
|
+
print(size) # arrives in completion order, fastest first
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `gather` — a drop-in `asyncio.gather` with a limit
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
results = await tasklane.gather(*(fetch(u) for u in urls), limit=10)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
On fail-fast, the remaining coroutines are cancelled **and closed**, so you never
|
|
124
|
+
see a `coroutine was never awaited` warning.
|
|
125
|
+
|
|
126
|
+
### Retries with backoff
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from tasklane import Backoff
|
|
130
|
+
|
|
131
|
+
await tasklane.amap(
|
|
132
|
+
fetch, urls,
|
|
133
|
+
retries=5,
|
|
134
|
+
backoff=Backoff.exponential(0.2, factor=2, max_delay=30), # 0.2, 0.4, 0.8, ... + jitter
|
|
135
|
+
retry_on=(TimeoutError, ConnectionError), # type, tuple, or predicate
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`Backoff.exponential()` (the default when `retries > 0`), `Backoff.linear()`, and
|
|
140
|
+
`Backoff.constant()` cover the common cases. `retry_on` accepts an exception type,
|
|
141
|
+
a tuple of types, or a `Callable[[BaseException], bool]` predicate.
|
|
142
|
+
|
|
143
|
+
### Rate limiting
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# Never start more than 100 tasks per second, regardless of the concurrency limit.
|
|
147
|
+
await tasklane.amap(call_api, items, limit=50, rate_limit=100)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Progress
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from tasklane import Progress
|
|
154
|
+
|
|
155
|
+
def show(p: Progress) -> None:
|
|
156
|
+
print(f"{p.completed}/{p.total} ({p.failed} failed) {p.rate:.0f}/s")
|
|
157
|
+
|
|
158
|
+
await tasklane.amap(fetch, urls, limit=10, on_progress=show)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`Progress` carries `completed`, `total`, `succeeded`, `failed`, `in_flight`, and
|
|
162
|
+
`elapsed`, plus `remaining`, `fraction`, and `rate` helpers. Plug it into `tqdm`,
|
|
163
|
+
a logger, or a web UI — no progress-bar dependency is imposed on you.
|
|
164
|
+
|
|
165
|
+
### Collect errors instead of raising
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
results = await tasklane.amap(fetch, urls, return_exceptions=True)
|
|
169
|
+
ok = [r for r in results if not isinstance(r, Exception)]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `Lane` — configure once, reuse everywhere
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from tasklane import Lane
|
|
176
|
+
|
|
177
|
+
# One policy for a specific downstream API.
|
|
178
|
+
github = Lane(limit=8, retries=3, rate_limit=20, timeout=10)
|
|
179
|
+
|
|
180
|
+
repos = await github.map(fetch_repo, repo_names)
|
|
181
|
+
async for issue in github.stream(fetch_issue, issue_ids):
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
# Lanes are immutable; derive a variant with .replace()
|
|
185
|
+
bulk = github.replace(limit=32)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## How it works
|
|
189
|
+
|
|
190
|
+
`tasklane` runs a fixed pool of `limit` worker coroutines that pull items off a
|
|
191
|
+
bounded `asyncio.Queue`. The bounded queue is what gives you backpressure and
|
|
192
|
+
constant memory; the worker pool is what enforces the concurrency limit exactly.
|
|
193
|
+
Retries, per-attempt timeouts, and rate limiting are applied inside each worker,
|
|
194
|
+
and completions are streamed back to the caller — collected into order for
|
|
195
|
+
`amap`, or yielded as-they-finish for `stream`. On any early exit (fail-fast,
|
|
196
|
+
`break`, or external cancellation) every in-flight task is cancelled and awaited,
|
|
197
|
+
so nothing leaks.
|
|
198
|
+
|
|
199
|
+
## API reference
|
|
200
|
+
|
|
201
|
+
| Symbol | Description |
|
|
202
|
+
| ------ | ----------- |
|
|
203
|
+
| `amap(func, items, *, limit, retries, backoff, retry_on, timeout, return_exceptions, rate_limit, on_progress)` | Concurrent map; returns a list in input order. |
|
|
204
|
+
| `stream(func, items, *, ...)` | Async iterator yielding results in completion order. |
|
|
205
|
+
| `gather(*coros, limit, timeout, rate_limit, return_exceptions, on_progress)` | Concurrency-limited `asyncio.gather`. |
|
|
206
|
+
| `Lane(...)` | Reusable, immutable bundle of settings with `.map`, `.stream`, `.gather`, `.replace`. |
|
|
207
|
+
| `Backoff` | Retry delay strategy: `.exponential`, `.linear`, `.constant`. |
|
|
208
|
+
| `Progress` | Immutable progress snapshot passed to `on_progress`. |
|
|
209
|
+
|
|
210
|
+
Full signatures and docstrings ship with the package and are surfaced by your
|
|
211
|
+
editor (the library is fully typed and marked with `py.typed`).
|
|
212
|
+
|
|
213
|
+
## Contributing
|
|
214
|
+
|
|
215
|
+
Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). In short:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
uv sync
|
|
219
|
+
uv run pytest # tests
|
|
220
|
+
uv run ruff check . # lint
|
|
221
|
+
uv run mypy # types
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
[MIT](LICENSE) © tasklane contributors
|