asynclit
Documentation: asynclit.readthedocs.io
Small async task layer for Streamlit (and similar “sync main thread + rerun” UIs): run sync or async work on a dedicated background event loop, then poll status, results, progress, and cancellation without blocking the UI thread.
Install
pip install asynclit
Optional extras:
- Streamlit (for a typical app environment):
pip install 'asynclit[streamlit]' - APScheduler (optional timers/jobs):
pip install 'asynclit[scheduler]'
Requires Python 3.9+.
Quick start
import streamlit as st
import asynclit
task = asynclit.run(fetch_data)
if task.done:
st.write(task.result)
else:
st.write("Loading…")
On each rerun, check task.done and read task.result when finished.
Example output (from the included test app tests/streamlit_apps/asynclet_poll_app.py via Streamlit AppTest):
run 0 ['wait']
run 1 ['ready:138']
... last ['ready:138']
Public API
| Item | Role |
|---|---|
asynclit.run(func, /, *args, manager=None, retry=None, **kwargs) |
Submit func on the worker; returns a Task. |
Task.done |
Whether the result (or error) is ready. |
Task.result |
Result value; raises if not complete. |
Task.status |
TaskStatus: PENDING, RUNNING, DONE, ERROR, CANCELLED. |
Task.error |
Exception object when status is ERROR, else None. |
Task.cancel() |
Request cancellation (running tasks use asyncio cancellation; pending tasks cancel the result future). |
Task.progress |
Non-blocking drain of progress values (see below). |
TaskManager / get_default_manager() |
Custom registry and cleanup() when you keep many completed tasks. |
session_tasks(session_state) |
Dict stored on st.session_state for named tasks. |
RetryPolicy |
Retry configuration for exception-based retries. |
schedule_interval(...) / schedule_cron(...) |
Schedule periodic task submissions (requires asynclit[scheduler]). |
Progress (Janus)
Progress is supported for async functions only.
Declare a parameter named queue or progress_queue:
- If it is the first parameter, asynclit injects the Janus queue positionally and the remaining positional arguments to
run()map to the rest of the signature. - Otherwise, asynclit injects the queue by keyword (
queue=/progress_queue=).
async def job(queue, steps: int):
for i in range(steps):
await queue.async_q.put(i)
return steps
task = asynclit.run(job, 10)
# Each rerun:
for x in task.progress:
st.write(x)
The UI thread reads via task.progress, which pulls from the sync side of a janus queue.
Streamlit session state
import streamlit as st
import asynclit
tasks = asynclit.session_tasks(st.session_state)
if "load" not in tasks:
tasks["load"] = asynclit.run(load_data)
task = tasks["load"]
Patterns
Named tasks (per session)
Use session_tasks(st.session_state) as a stable dict to store tasks across reruns:
tasks = asynclit.session_tasks(st.session_state)
if "load" not in tasks:
tasks["load"] = asynclit.run(load_data)
task = tasks["load"]
Cleanup (when you create many tasks)
If you create many tasks over time, keep them in a TaskManager and periodically call cleanup() to trim completed entries:
m = asynclit.TaskManager(max_completed=256)
task = asynclit.run(load_data, manager=m)
# ... later:
m.cleanup()
Errors
If the callable raises, task.status becomes ERROR, task.error holds the exception, and reading task.result re-raises it.
if task.status == asynclit.TaskStatus.ERROR:
st.error(f"failed: {task.error!r}")
elif task.done:
st.write(task.result)
else:
st.write("Loading…")
Cancellation
task.cancel() requests cancellation:
- If the task is running, it schedules
asynciocancellation on the worker loop. - If the task is still pending (not yet bound on the worker loop), it cancels the result future.
Treat CANCELLED as a terminal state in UI code.
Cooperative cancellation patterns
- Async jobs: rely on
asynciocancellation; add cancellation checkpoints when doing long CPU work (break work into chunks;awaitoccasionally). - Sync jobs: cancellation is best-effort; the underlying threadpool work may continue running. Prefer chunked work that you can stop between chunks.
Retries
Use RetryPolicy for exception-based retries (opt-in per run() / TaskManager.submit()).
policy = asynclit.RetryPolicy(
max_attempts=3,
base_delay=0.1,
max_delay=1.0,
multiplier=2.0,
jitter=0.0,
)
task = asynclit.run(fetch_data, retry=policy)
Retries stop when attempts are exhausted, the task is cancelled, or a raised exception does not match the policy.
Scheduling (APScheduler)
Install with:
pip install 'asynclit[scheduler]'
Then schedule periodic task submission on the worker loop:
asynclit.schedule_interval(load_data, seconds=60, latest_task_name="load_data")
# Later (poll the most recent task):
m = asynclit.get_default_manager()
latest = m.get("global:load_data")
if latest and latest.done:
st.write(latest.result)
Troubleshooting / FAQ
Why does it keep showing wait?
In rerun-driven UIs, a single script run may finish before the background task completes. The usual pattern is: show wait, then on the next rerun read task.done / task.result.
In tests (or special cases), you may need to allow a small amount of wall time between reruns for the worker to finish.
How it works (short)
- One daemon thread runs a single asyncio event loop.
- Async callables run on that loop; sync callables run via asyncer’s
asyncify(thread pool). - Submissions use
asyncio.run_coroutine_threadsafe; results are bridged with aconcurrent.futures.Futurefor the polling API.
Development
pip install -e '.[dev]'
pytest
Development (uv)
If you use uv, you can run tests in a fresh env like:
uv venv
uv pip install -e '.[dev]'
uv run pytest
The dev extra includes Streamlit so CI can run headless AppTest checks in tests/test_streamlit_apptest.py against the sample apps under tests/streamlit_apps/.
Docs
Build the documentation (Sphinx + MyST):
uv venv
uv pip install -e '.[docs]'
uv run python -m sphinx -b html docs docs/_build/html
License
MIT.
Metadata
Release files for asynclit 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| asynclit-0.2.2.tar.gz | 12.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| asynclit-0.2.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 26.7 kB
Release files / asynclit-0.2.2.tar.gz
| Download URL | asynclit-0.2.2.tar.gz |
|---|---|
| Size | 12.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
94034ced83598f22b1cebf42554110b6da6ba224f1003e9296f04e0dfa733cf4
|
|
BLAKE2b-256 checksum How to use checksums |
1292bd4d5c2d280344ff0c1001ce5b070e16954ece3434ce6c70abe146c42d2d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.13
|
Release files / asynclit-0.2.2-py3-none-any.whl
| Download URL | asynclit-0.2.2-py3-none-any.whl |
|---|---|
| Size | 14.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
20557c8d414fee7ac3009a61ce095dff0342730653d9c55de477f1b73cfaba19
|
|
BLAKE2b-256 checksum How to use checksums |
096cf8e1f36be8c199e9901830f320308eaf6166a54b8d7ae35043cd7eb37a79
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.13
|