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.
@@ -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.
@@ -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
+ [![CI](https://github.com/jpwm2/tasklane/actions/workflows/ci.yml/badge.svg)](https://github.com/jpwm2/tasklane/actions/workflows/ci.yml)
34
+ [![PyPI](https://img.shields.io/pypi/v/tasklane.svg)](https://pypi.org/project/tasklane/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/tasklane.svg)](https://pypi.org/project/tasklane/)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
37
+ [![Types: typed](https://img.shields.io/badge/types-100%25-blue.svg)](src/tasklane/py.typed)
38
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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