Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Deploy Docs

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.13"

- name: Install dependencies
run: uv sync --group docs

- name: Build docs (strict)
run: uv run mkdocs build --strict

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: site

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
17 changes: 16 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4

Expand All @@ -26,3 +27,17 @@ jobs:

- name: Run tests
run: uv run pytest tests/ -v

docs-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Install dependencies
run: uv sync --group docs

- name: Build docs (strict)
run: uv run mkdocs build --strict
28 changes: 24 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
# Byte-compiled / caches
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
.venv/
.pytest_cache/
.ruff_cache/
.mypy_cache/

# Coverage
.coverage
.coverage.*
htmlcov/
coverage.xml

# Build artifacts
dist/
build/
*.egg-info/

# Environments
.venv/
venv/

# Docs build
site/

# OS / editor
.DS_Store
.idea/
.vscode/
70 changes: 70 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0] - 2026-07-03

### Added

- Bilingual (English / 日本語) documentation built with MkDocs Material +
mkdocs-static-i18n + mkdocstrings, deployed to GitHub Pages:
guides for waiting, iterating, deriving/composing, tasks/cancellation,
and events/streams, plus a full API reference.
- Runnable examples under `examples/` (state machine, debouncing,
composing values).
- `py.typed` marker so type checkers pick up the annotations.
- Docs build job in CI and a GitHub Pages deployment workflow.
- Regression test suites for `held_for`, raising predicates, task
helpers, and stream helpers.

### Fixed

- `AsyncValue.wait_value(..., held_for=N)` raised `asyncio.TimeoutError`
when the condition *successfully* held for the duration; it now
returns the value.
- `AsyncValue.eventual_values(..., held_for=N)` had the hold logic
inverted (yielded on flicker, raised `TimeoutError` on a stable hold).
- A raising predicate corrupted delivery: the exception surfaced in the
assigning task and later waiters were never notified. Predicate
exceptions are now delivered to their own waiter; transform/compose
errors are re-raised only after all notifications went out.
- `compose_values()` silently swallowed exceptions raised by
`_transform_`, freezing the composite output at its last good value.
- `move_on_when()` leaked `CancelledError` to the caller (and never ran
the body) when the trigger completed without awaiting, e.g.
`move_on_when(already_set_event.wait)`.
- `move_on_when()` swallowed exceptions raised by the trigger; they now
cancel the body and propagate.
- `run_and_cancelling()` / `start_and_cancelling()` masked the body's
exception when the background task had also failed.
- `wait_any()` / `wait_all()` / `wait_any_map()` leaked already-started
tasks when creating a later task raised synchronously.
- `MulticastQueue` treated a broadcast `None` as end-of-stream,
terminating listeners early; a private sentinel is used instead.
- `periodic()` drifted: scheduling is now anchored to the absolute
`start + k * period` grid and re-syncs after an overrun.

### Changed

- Requires Python 3.10+ (was 3.8+); CI covers 3.10–3.14.
- Project metadata: description, keywords, classifiers, documentation
URL pointing to GitHub Pages.

## [0.0.1] - 2024-05-15

### Added

- Initial port of trio-util APIs to asyncio, created for a
[PyCon US 2024 talk](https://us.pycon.org/2024/schedule/presentation/142/):
`AsyncValue`, `AsyncBool`, `compose_values`, `open_held_for`,
`open_hysteresis`, `wait_any`/`wait_all`/`wait_any_map`,
`move_on_when`/`CancelScope`, `run_and_cancelling`/
`start_and_cancelling`, `MulticastQueue`, `RepeatedEvent`,
`periodic`, `azip`/`azip_longest`,
`iter_move_on_after`/`iter_fail_after`.

[0.1.0]: https://github.com/jrfk/asyncio-util/releases/tag/v0.1.0
[0.0.1]: https://github.com/jrfk/asyncio-util/commit/acf53ed
122 changes: 122 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# asyncio-util

[![Test](https://github.com/jrfk/asyncio-util/actions/workflows/test.yml/badge.svg)](https://github.com/jrfk/asyncio-util/actions/workflows/test.yml)
[![Docs](https://github.com/jrfk/asyncio-util/actions/workflows/docs.yml/badge.svg)](https://jrfk.github.io/asyncio-util/ja/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://spdx.org/licenses/MIT.html)

**[trio-util](https://github.com/groove-x/trio-util) から asyncio へ移植した
ユーティリティ集 — await できる値、キャンセルスコープ、繰り返しイベント、
その他いろいろ。**

[ドキュメント](https://jrfk.github.io/asyncio-util/ja/) | [English README](README.md)

> [!TIP]
> このプロジェクトは、trio-util を asyncio に移植する PyCon US 2024 の
> [トーク](https://us.pycon.org/2024/schedule/presentation/142/)のサンプル
> コードとして始まり、独立したライブラリに成長しました。

-----

中心となるのは `AsyncValue` — 任意の数のタスクが状態や遷移を *await* できる
値です。ポーリングループも、`Event` の手動管理も、取りこぼしの心配も
いりません。

```python
import asyncio
from asyncio_util import AsyncValue

connection_state = AsyncValue("disconnected")

async def watcher():
# 値が条件を満たすまで待つ…
await connection_state.wait_value("connected")
print("接続しました!")

# …あるいは値の変化に逐次反応する。
async for state in connection_state.eventual_values():
print(f"現在の状態: {state}")

async def main():
task = asyncio.ensure_future(watcher())
await asyncio.sleep(0)
connection_state.value = "connecting"
connection_state.value = "connected"
await asyncio.sleep(0.1)
task.cancel()

asyncio.run(main())
```

マッチ判定は**代入時に同期的に**行われるため、値が直後に元へ戻っても
マッチする変化を取りこぼしません。

```python
av = AsyncValue(10)

# 別の場所で: av.value = 20; av.value = 10 (高速な往復)
value = await av.wait_value(20) # それでも 20 が返る
```

## 機能

**await できる値**

- `AsyncValue` / `AsyncBool` — `wait_value()`・`wait_transition()`・
`eventual_values()`・`transitions()`。述語、`timeout=`、`held_for=`
デバウンスに対応
- `open_transform(f)` — `f(value)` を追跡する派生 `AsyncValue`
- `compose_values(x=..., y=...)` — 複数の値にまたがる条件を await
- `open_held_for()` / `open_hysteresis()` — バタつく信号のための時間ベース
フィルタ

**タスクとキャンセル**

- `wait_any()` / `wait_all()` / `wait_any_map()` — 構造化されたレースと
ジョイン(開始したタスクは戻る前に必ずキャンセル・待機される)
- `move_on_when(trigger)` — イベント発火でブロックをキャンセル
- `run_and_cancelling(fn)` / `start_and_cancelling(fn)` — ブロックに
スコープされたバックグラウンドタスク

**イベント・ストリーム・タイミング**

- `RepeatedEvent` — 何度も発火でき、複数リスナーを持てるイベント
- `MulticastQueue` — 各アイテムを全リスナーへブロードキャスト
- `periodic(period)` — ドリフトしない周期イテレーション
- `azip()` / `azip_longest()` — 非同期イテレータの async `zip`
- `iter_move_on_after()` / `iter_fail_after()` — アイテム単位のタイムアウト

依存ゼロ、型付き(`py.typed`)、Python 3.10+(CPython & PyPy)。

## インストール

> [!WARNING]
> PyPI にはまだ公開されていません。

```console
pip install git+https://github.com/jrfk/asyncio-util
```

## ドキュメント

ガイド(日本語 / English)と API リファレンス:
**https://jrfk.github.io/asyncio-util/ja/**

実行できるサンプルは [`examples/`](examples/) にあります。

## 開発

```console
uv sync --group dev
uv run pytest # テスト
uv run mkdocs serve # ドキュメントのプレビュー (uv sync --group docs)
```

## 謝辞

API は GROOVE X の [trio-util](https://github.com/groove-x/trio-util) を
モデルに、asyncio のプリミティブの上にゼロから再実装したものです。

## ライセンス

`asyncio-util` は [MIT](https://spdx.org/licenses/MIT.html) ライセンスの下で
配布されています。
Loading
Loading