ttflow 0.5.8__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.
- ttflow-0.5.8/.github/workflows/ci.yml +40 -0
- ttflow-0.5.8/.github/workflows/docs.yml +27 -0
- ttflow-0.5.8/.github/workflows/python-package.yml +62 -0
- ttflow-0.5.8/.gitignore +147 -0
- ttflow-0.5.8/CLAUDE.md +72 -0
- ttflow-0.5.8/Makefile +35 -0
- ttflow-0.5.8/PKG-INFO +13 -0
- ttflow-0.5.8/README.md +241 -0
- ttflow-0.5.8/docs/api/client.md +5 -0
- ttflow-0.5.8/docs/api/errors.md +5 -0
- ttflow-0.5.8/docs/api/pause.md +5 -0
- ttflow-0.5.8/docs/api/powerup.md +17 -0
- ttflow-0.5.8/docs/api/run_context.md +5 -0
- ttflow-0.5.8/docs/api/state.md +10 -0
- ttflow-0.5.8/docs/api/state_repository.md +23 -0
- ttflow-0.5.8/docs/api/ttflow.md +13 -0
- ttflow-0.5.8/docs/api/workflow.md +11 -0
- ttflow-0.5.8/docs/events.md +168 -0
- ttflow-0.5.8/docs/guide/events.md +168 -0
- ttflow-0.5.8/docs/guide/state_repository.md +133 -0
- ttflow-0.5.8/docs/index.md +47 -0
- ttflow-0.5.8/docs/state_repository.md +133 -0
- ttflow-0.5.8/docs//351/226/213/347/231/272/343/203/241/343/203/242/00_/343/203/257/343/203/274/343/202/257/343/203/225/343/203/255/343/203/274/343/202/250/343/203/263/343/202/270/343/203/263/351/201/270/345/256/232.md +538 -0
- ttflow-0.5.8/docs//351/226/213/347/231/272/343/203/241/343/203/242/01_/351/226/213/347/231/272/345/206/215/351/226/213.md +49 -0
- ttflow-0.5.8/examples/cli.py +22 -0
- ttflow-0.5.8/examples/home_security.py +42 -0
- ttflow-0.5.8/examples/infinite_loop.py +26 -0
- ttflow-0.5.8/examples/infinite_loop_with_sideeffect.py +36 -0
- ttflow-0.5.8/examples/sample.py +89 -0
- ttflow-0.5.8/examples/watch_price.py +47 -0
- ttflow-0.5.8/mkdocs.yml +66 -0
- ttflow-0.5.8/pyproject.toml +59 -0
- ttflow-0.5.8/tests/__init__.py +0 -0
- ttflow-0.5.8/tests/conftest.py +8 -0
- ttflow-0.5.8/tests/state_repository/test_s3_state_repository.py +97 -0
- ttflow-0.5.8/tests/state_repository/test_state_repository.py +29 -0
- ttflow-0.5.8/tests/test_async_workflow.py +315 -0
- ttflow-0.5.8/tests/test_completed_lod.py +25 -0
- ttflow-0.5.8/tests/test_dynamodb_state.py +148 -0
- ttflow-0.5.8/tests/test_error_handling.py +20 -0
- ttflow-0.5.8/tests/test_event.py +98 -0
- ttflow-0.5.8/tests/test_every.py +23 -0
- ttflow-0.5.8/tests/test_load.py +56 -0
- ttflow-0.5.8/tests/test_load_workflow_hash.py +57 -0
- ttflow-0.5.8/tests/test_new_completed_and_logs.py +122 -0
- ttflow-0.5.8/tests/test_new_lock.py +198 -0
- ttflow-0.5.8/tests/test_new_pause.py +232 -0
- ttflow-0.5.8/tests/test_new_setup.py +173 -0
- ttflow-0.5.8/tests/test_new_sideeffect.py +135 -0
- ttflow-0.5.8/tests/test_new_state.py +195 -0
- ttflow-0.5.8/tests/test_new_workflow.py +274 -0
- ttflow-0.5.8/tests/test_notations.py +44 -0
- ttflow-0.5.8/tests/test_pause.py +198 -0
- ttflow-0.5.8/tests/test_skip.py +203 -0
- ttflow-0.5.8/tests/test_usecase__temperature_monitoring.py +171 -0
- ttflow-0.5.8/tests/test_usecase__watch_price.py +65 -0
- ttflow-0.5.8/tests/test_web.py +157 -0
- ttflow-0.5.8/tests/utils.py +10 -0
- ttflow-0.5.8/ttflow/__init__.py +15 -0
- ttflow-0.5.8/ttflow/constants.py +8 -0
- ttflow-0.5.8/ttflow/core/__init__.py +1 -0
- ttflow-0.5.8/ttflow/core/context.py +28 -0
- ttflow-0.5.8/ttflow/core/event.py +71 -0
- ttflow-0.5.8/ttflow/core/global_env.py +44 -0
- ttflow-0.5.8/ttflow/core/pause.py +46 -0
- ttflow-0.5.8/ttflow/core/run_context.py +48 -0
- ttflow-0.5.8/ttflow/core/state.py +66 -0
- ttflow-0.5.8/ttflow/core/system_events/every.py +29 -0
- ttflow-0.5.8/ttflow/core/system_events/pause.py +51 -0
- ttflow-0.5.8/ttflow/core/trigger.py +9 -0
- ttflow-0.5.8/ttflow/core/workflow.py +183 -0
- ttflow-0.5.8/ttflow/errors.py +22 -0
- ttflow-0.5.8/ttflow/powerup/__init__.py +0 -0
- ttflow-0.5.8/ttflow/powerup/run_by_cli.py +43 -0
- ttflow-0.5.8/ttflow/powerup/run_by_lambda.py +30 -0
- ttflow-0.5.8/ttflow/state_repository/__init__.py +0 -0
- ttflow-0.5.8/ttflow/state_repository/base.py +36 -0
- ttflow-0.5.8/ttflow/state_repository/buffer_cache_proxy.py +62 -0
- ttflow-0.5.8/ttflow/state_repository/dynamodb.py +91 -0
- ttflow-0.5.8/ttflow/state_repository/local_file_state.py +40 -0
- ttflow-0.5.8/ttflow/state_repository/on_memory_state.py +28 -0
- ttflow-0.5.8/ttflow/state_repository/s3.py +52 -0
- ttflow-0.5.8/ttflow/system_states/README.md +6 -0
- ttflow-0.5.8/ttflow/system_states/completed.py +64 -0
- ttflow-0.5.8/ttflow/system_states/event_log.py +32 -0
- ttflow-0.5.8/ttflow/system_states/logs.py +32 -0
- ttflow-0.5.8/ttflow/system_states/run_state.py +93 -0
- ttflow-0.5.8/ttflow/ttflow.py +237 -0
- ttflow-0.5.8/ttflow/utils.py +11 -0
- ttflow-0.5.8/ttflow/web/__init__.py +3 -0
- ttflow-0.5.8/ttflow/web/app.py +129 -0
- ttflow-0.5.8/ttflow/web/static/index.html +450 -0
- ttflow-0.5.8/uv.lock +1897 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
name: lint
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v5
|
|
14
|
+
- uses: astral-sh/setup-uv@v7
|
|
15
|
+
- uses: actions/setup-python@v6
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- run: uv sync --extra web
|
|
19
|
+
- run: make lint
|
|
20
|
+
|
|
21
|
+
test:
|
|
22
|
+
name: test (Python ${{ matrix.python-version }})
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
strategy:
|
|
25
|
+
matrix:
|
|
26
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v5
|
|
29
|
+
- uses: astral-sh/setup-uv@v7
|
|
30
|
+
- uses: actions/setup-python@v6
|
|
31
|
+
with:
|
|
32
|
+
python-version: ${{ matrix.python-version }}
|
|
33
|
+
- run: uv sync --extra web
|
|
34
|
+
- run: uv run pytest --cov --cov-report=xml
|
|
35
|
+
- name: Upload coverage to Codecov
|
|
36
|
+
if: matrix.python-version == '3.12'
|
|
37
|
+
uses: codecov/codecov-action@v5
|
|
38
|
+
with:
|
|
39
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
40
|
+
files: coverage.xml
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Deploy Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
deploy:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
run: uv python install
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: uv sync --group docs
|
|
25
|
+
|
|
26
|
+
- name: Build and deploy docs
|
|
27
|
+
run: uv run --group docs mkdocs gh-deploy --force
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: build package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v5
|
|
16
|
+
- uses: astral-sh/setup-uv@v7
|
|
17
|
+
- uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: uv sync --extra web
|
|
21
|
+
- run: uv run pytest
|
|
22
|
+
|
|
23
|
+
build:
|
|
24
|
+
needs: test
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v5
|
|
28
|
+
- uses: astral-sh/setup-uv@v7
|
|
29
|
+
- uses: actions/setup-python@v6
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.12"
|
|
32
|
+
- run: uv build
|
|
33
|
+
- uses: actions/upload-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
|
|
38
|
+
github-release:
|
|
39
|
+
needs: build
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
permissions:
|
|
42
|
+
contents: write
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/download-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
- uses: softprops/action-gh-release@v3
|
|
49
|
+
with:
|
|
50
|
+
files: dist/*
|
|
51
|
+
|
|
52
|
+
pypi-publish:
|
|
53
|
+
needs: build
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
permissions:
|
|
56
|
+
id-token: write # Trusted Publisher認証に必要
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/download-artifact@v4
|
|
59
|
+
with:
|
|
60
|
+
name: dist
|
|
61
|
+
path: dist/
|
|
62
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
ttflow-0.5.8/.gitignore
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
### https://raw.github.com/github/gitignore/218a941be92679ce67d0484547e3e142b2f5f6f0/Python.gitignore
|
|
2
|
+
|
|
3
|
+
# Byte-compiled / optimized / DLL files
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*$py.class
|
|
7
|
+
|
|
8
|
+
# C extensions
|
|
9
|
+
*.so
|
|
10
|
+
|
|
11
|
+
# Distribution / packaging
|
|
12
|
+
.Python
|
|
13
|
+
build/
|
|
14
|
+
develop-eggs/
|
|
15
|
+
dist/
|
|
16
|
+
downloads/
|
|
17
|
+
eggs/
|
|
18
|
+
.eggs/
|
|
19
|
+
lib/
|
|
20
|
+
lib64/
|
|
21
|
+
parts/
|
|
22
|
+
sdist/
|
|
23
|
+
var/
|
|
24
|
+
wheels/
|
|
25
|
+
share/python-wheels/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
MANIFEST
|
|
30
|
+
|
|
31
|
+
# PyInstaller
|
|
32
|
+
# Usually these files are written by a python script from a template
|
|
33
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
34
|
+
*.manifest
|
|
35
|
+
*.spec
|
|
36
|
+
|
|
37
|
+
# Installer logs
|
|
38
|
+
pip-log.txt
|
|
39
|
+
pip-delete-this-directory.txt
|
|
40
|
+
|
|
41
|
+
# Unit test / coverage reports
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.nox/
|
|
45
|
+
.coverage
|
|
46
|
+
.coverage.*
|
|
47
|
+
.cache
|
|
48
|
+
nosetests.xml
|
|
49
|
+
coverage.xml
|
|
50
|
+
*.cover
|
|
51
|
+
*.py,cover
|
|
52
|
+
.hypothesis/
|
|
53
|
+
.pytest_cache/
|
|
54
|
+
cover/
|
|
55
|
+
|
|
56
|
+
# Translations
|
|
57
|
+
*.mo
|
|
58
|
+
*.pot
|
|
59
|
+
|
|
60
|
+
# Django stuff:
|
|
61
|
+
*.log
|
|
62
|
+
local_settings.py
|
|
63
|
+
db.sqlite3
|
|
64
|
+
db.sqlite3-journal
|
|
65
|
+
|
|
66
|
+
# Flask stuff:
|
|
67
|
+
instance/
|
|
68
|
+
.webassets-cache
|
|
69
|
+
|
|
70
|
+
# Scrapy stuff:
|
|
71
|
+
.scrapy
|
|
72
|
+
|
|
73
|
+
# Sphinx documentation
|
|
74
|
+
docs/_build/
|
|
75
|
+
|
|
76
|
+
# PyBuilder
|
|
77
|
+
.pybuilder/
|
|
78
|
+
target/
|
|
79
|
+
|
|
80
|
+
# Jupyter Notebook
|
|
81
|
+
.ipynb_checkpoints
|
|
82
|
+
|
|
83
|
+
# IPython
|
|
84
|
+
profile_default/
|
|
85
|
+
ipython_config.py
|
|
86
|
+
|
|
87
|
+
# pyenv
|
|
88
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
89
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
90
|
+
# .python-version
|
|
91
|
+
|
|
92
|
+
# pipenv
|
|
93
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
94
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
95
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
96
|
+
# install all needed dependencies.
|
|
97
|
+
#Pipfile.lock
|
|
98
|
+
|
|
99
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
100
|
+
__pypackages__/
|
|
101
|
+
|
|
102
|
+
# Celery stuff
|
|
103
|
+
celerybeat-schedule
|
|
104
|
+
celerybeat.pid
|
|
105
|
+
|
|
106
|
+
# SageMath parsed files
|
|
107
|
+
*.sage.py
|
|
108
|
+
|
|
109
|
+
# Environments
|
|
110
|
+
.env
|
|
111
|
+
.venv
|
|
112
|
+
env/
|
|
113
|
+
venv/
|
|
114
|
+
ENV/
|
|
115
|
+
env.bak/
|
|
116
|
+
venv.bak/
|
|
117
|
+
|
|
118
|
+
# Spyder project settings
|
|
119
|
+
.spyderproject
|
|
120
|
+
.spyproject
|
|
121
|
+
|
|
122
|
+
# Rope project settings
|
|
123
|
+
.ropeproject
|
|
124
|
+
|
|
125
|
+
# mkdocs documentation
|
|
126
|
+
/site
|
|
127
|
+
|
|
128
|
+
# mypy
|
|
129
|
+
.mypy_cache/
|
|
130
|
+
.dmypy.json
|
|
131
|
+
dmypy.json
|
|
132
|
+
|
|
133
|
+
# Pyre type checker
|
|
134
|
+
.pyre/
|
|
135
|
+
|
|
136
|
+
# pytype static type analyzer
|
|
137
|
+
.pytype/
|
|
138
|
+
|
|
139
|
+
# Cython debug symbols
|
|
140
|
+
cython_debug/
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
states/*
|
|
145
|
+
|
|
146
|
+
# Claude Code
|
|
147
|
+
.claude/settings.local.json
|
ttflow-0.5.8/CLAUDE.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## プロジェクト概要
|
|
6
|
+
|
|
7
|
+
ttflowは軽量なPythonワークフローエンジン。イベント駆動でワークフローを実行し、pause/resumeによる長時間ワークフローをサポートする。状態はStateRepositoryに永続化され、ワークフロー関数自体はステートレスかつ再実行可能。
|
|
8
|
+
|
|
9
|
+
## 開発コマンド
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
make test # pytest実行
|
|
13
|
+
make fmt # ruffでフォーマット+自動修正
|
|
14
|
+
make lint # ruffでリント+フォーマットチェック
|
|
15
|
+
|
|
16
|
+
# 単一テスト実行
|
|
17
|
+
uv run pytest tests/test_pause.py -v
|
|
18
|
+
uv run pytest tests/test_pause.py::test_pause_once -v
|
|
19
|
+
|
|
20
|
+
# ネットワークテスト(通常スキップ)を含める場合
|
|
21
|
+
uv run pytest -m network
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## アーキテクチャ
|
|
25
|
+
|
|
26
|
+
### 実行モデル
|
|
27
|
+
|
|
28
|
+
`client.run(trigger_name)` が呼ばれると:
|
|
29
|
+
1. StateRepositoryのロックを取得
|
|
30
|
+
2. バッファリングモード開始(状態読み書きをキャッシュ)
|
|
31
|
+
3. イベントキューにトリガーイベントを追加し、キューが空になるまで処理
|
|
32
|
+
4. バッファをflushしてロック解放
|
|
33
|
+
|
|
34
|
+
### コア構造
|
|
35
|
+
|
|
36
|
+
- **`ttflow/ttflow.py`** — `Client`クラスと`setup()`。エントリポイント
|
|
37
|
+
- **`ttflow/core/workflow.py`** — `@workflow`/`@sideeffect`デコレータ、`exec_workflow()`
|
|
38
|
+
- **`ttflow/core/pause.py`** — `PauseException`によるpause/resume機構
|
|
39
|
+
- **`ttflow/core/state.py`** — `get_state`/`set_state`(再実行時はキャッシュから返す)
|
|
40
|
+
- **`ttflow/core/event.py`** — イベントキューイングと永続化
|
|
41
|
+
- **`ttflow/core/run_context.py`** — ワークフロー内で使うAPI(`c.get_state`, `c.log`, `c.pause_once`等)
|
|
42
|
+
- **`ttflow/state_repository/`** — 永続化バックエンド(local file, S3, DynamoDB, on-memory)
|
|
43
|
+
- **`ttflow/state_repository/buffer_cache_proxy.py`** — 読み取りキャッシュ+書き込みバッファのプロキシ層
|
|
44
|
+
|
|
45
|
+
### 冪等性の仕組み
|
|
46
|
+
|
|
47
|
+
ワークフローはpause/resume時に最初から再実行される。安全に再実行するため:
|
|
48
|
+
- `set_state()` — 初回のみ実行、再実行時はno-op
|
|
49
|
+
- `get_state()` — 初回でキャッシュ、再実行時はキャッシュ値を返す
|
|
50
|
+
- `@sideeffect()` — 副作用関数を`run_id`ごとに1回だけ実行
|
|
51
|
+
|
|
52
|
+
### トリガー種別
|
|
53
|
+
|
|
54
|
+
- `event_trigger(name)` — 名前付きイベントで発火
|
|
55
|
+
- `every_trigger()` — 毎回の`client.run()`で発火
|
|
56
|
+
- `state_trigger(state_name)` — 状態変更時に発火
|
|
57
|
+
|
|
58
|
+
## コーディング規約
|
|
59
|
+
|
|
60
|
+
- コメント・ドキュメントは日本語
|
|
61
|
+
- コミットメッセージは日本語
|
|
62
|
+
- パッケージ管理: uv
|
|
63
|
+
- フォーマッタ/リンタ: ruff
|
|
64
|
+
- テストには`@pytest.mark.network`マーカーでネットワーク依存テストを分離
|
|
65
|
+
- **コード変更後は必ず `make fmt` と `make lint` を実行し、エラーがない状態にすること**(CIで同じチェックが走る)
|
|
66
|
+
|
|
67
|
+
## ドキュメント運用
|
|
68
|
+
|
|
69
|
+
- **リファクタリング計画**: `docs/refactor_and_modernize.md` に全体計画を記載
|
|
70
|
+
- **開発メモ**: `docs/開発メモ/` に議事録を連番で蓄積する(例: `01_開発再開.md`, `02_xxx.md`)
|
|
71
|
+
- 議論と方針決定があるたびに新しいファイルを作成する
|
|
72
|
+
- 決定事項・経緯・背景を記録する
|
ttflow-0.5.8/Makefile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Run all checks (autofix + lint + test) — same as CI
|
|
2
|
+
.PHONY: all
|
|
3
|
+
all: fmt lint test
|
|
4
|
+
|
|
5
|
+
# Run tests with verbose output
|
|
6
|
+
.PHONY: test
|
|
7
|
+
test:
|
|
8
|
+
uv run pytest -vv
|
|
9
|
+
|
|
10
|
+
# Run tests with coverage report
|
|
11
|
+
.PHONY: coverage
|
|
12
|
+
coverage:
|
|
13
|
+
uv run pytest --cov --cov-report=term-missing --cov-report=xml
|
|
14
|
+
|
|
15
|
+
# Auto-format code and apply safe lint fixes
|
|
16
|
+
.PHONY: fmt
|
|
17
|
+
fmt:
|
|
18
|
+
uv run ruff format .
|
|
19
|
+
uv run ruff check --fix .
|
|
20
|
+
|
|
21
|
+
# Check for lint errors and formatting issues (no modifications)
|
|
22
|
+
.PHONY: lint
|
|
23
|
+
lint:
|
|
24
|
+
uv run ruff check .
|
|
25
|
+
uv run ruff format --check .
|
|
26
|
+
|
|
27
|
+
# Serve docs locally with live reload
|
|
28
|
+
.PHONY: docs
|
|
29
|
+
docs:
|
|
30
|
+
uv run --group docs mkdocs serve
|
|
31
|
+
|
|
32
|
+
# Build docs for deployment
|
|
33
|
+
.PHONY: docs-build
|
|
34
|
+
docs-build:
|
|
35
|
+
uv run --group docs mkdocs build --strict
|
ttflow-0.5.8/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ttflow
|
|
3
|
+
Version: 0.5.8
|
|
4
|
+
Summary: simple workflow engine
|
|
5
|
+
Author-email: moajo <moajodev@gmail.com>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: boto3-stubs[essential]>=1.26.42
|
|
8
|
+
Requires-Dist: boto3>=1.26.42
|
|
9
|
+
Requires-Dist: dacite>=1.7.0
|
|
10
|
+
Requires-Dist: fire>=0.5.0
|
|
11
|
+
Provides-Extra: web
|
|
12
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'web'
|
|
13
|
+
Requires-Dist: uvicorn[standard]>=0.20.0; extra == 'web'
|
ttflow-0.5.8/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# ttflow
|
|
2
|
+
|
|
3
|
+
[](https://codecov.io/gh/moajo/ttflow)
|
|
4
|
+
|
|
5
|
+
軽量なPythonワークフローエンジンです。
|
|
6
|
+
|
|
7
|
+
ペライチのPythonスクリプトがそのままワークフローエンジンとして動きます。
|
|
8
|
+
サーバープロセスは不要なので、待機コストが0になります。
|
|
9
|
+
|
|
10
|
+
## 特徴
|
|
11
|
+
|
|
12
|
+
- **サーバー不要** — 常駐プロセスなし。スクリプトを実行するだけ
|
|
13
|
+
- **中断と再開** — 数日〜数週間にわたるワークフローを、途中で中断して後から再開できる
|
|
14
|
+
- **冪等な再実行** — 再開時は最初から再実行されるが、副作用は1回しか実行されない
|
|
15
|
+
- **イベント駆動** — ワークフロー間をイベントで疎結合に連携
|
|
16
|
+
- **永続化バックエンドを選択可能** — ローカルファイル、S3、DynamoDB、オンメモリ
|
|
17
|
+
|
|
18
|
+
## クイックスタート
|
|
19
|
+
|
|
20
|
+
### インストール
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install ttflow
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 最小限のワークフロー
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from ttflow import RunContext, setup
|
|
30
|
+
from ttflow.powerup.run_by_cli import run_by_cli
|
|
31
|
+
|
|
32
|
+
ttflow = setup(state_repository="local:state.json")
|
|
33
|
+
|
|
34
|
+
@ttflow.workflow()
|
|
35
|
+
def hello(c: RunContext, args: dict):
|
|
36
|
+
c.log("Hello, ttflow!")
|
|
37
|
+
|
|
38
|
+
run_by_cli(ttflow)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python hello.py run hello
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Hello, ttflow!
|
|
47
|
+
|
|
48
|
+
---------RUN SUMMARY---------
|
|
49
|
+
1件のワークフローが実行されました
|
|
50
|
+
1件目
|
|
51
|
+
ワークフロー名: hello
|
|
52
|
+
run_id: ...
|
|
53
|
+
状態: succeeded
|
|
54
|
+
ログ:
|
|
55
|
+
- Hello, ttflow!
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## コアコンセプト
|
|
59
|
+
|
|
60
|
+
### ワークフローとトリガー
|
|
61
|
+
|
|
62
|
+
`@workflow()` でワークフローを定義し、`client.run(トリガー名)` で実行します。
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
@ttflow.workflow()
|
|
66
|
+
def 処理1(c: RunContext, args: dict):
|
|
67
|
+
c.log("処理1を実行します")
|
|
68
|
+
c.event("event1", 42) # イベントを発火
|
|
69
|
+
|
|
70
|
+
@ttflow.workflow(trigger=event_trigger("event1"))
|
|
71
|
+
def 処理2(c: RunContext, args: int):
|
|
72
|
+
c.log(f"処理2も実行します: {args}")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`処理1` を実行すると `event1` が発火し、次回の `run()` で `処理2` が自動的にトリガーされます。
|
|
76
|
+
|
|
77
|
+
トリガーを省略すると、関数名がそのままトリガー名になります(`client.run("処理1")` で実行可能)。
|
|
78
|
+
|
|
79
|
+
### 中断と再開
|
|
80
|
+
|
|
81
|
+
`c.pause_once()` でワークフローを中断し、次回の `run()` で続きから再開できます。
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
@ttflow.workflow()
|
|
85
|
+
def loop(c: RunContext, args: dict):
|
|
86
|
+
count = 0
|
|
87
|
+
while True:
|
|
88
|
+
c.log(f"loop: {count}週目開始")
|
|
89
|
+
c.pause_once() # ここで中断。次回のrun()で再開
|
|
90
|
+
c.log(f"loop: {count}週目おわり")
|
|
91
|
+
count += 1
|
|
92
|
+
if count > 10:
|
|
93
|
+
break
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`c.wait_event(event_name)` を使えば、特定のイベントが発行されるまで中断することもできます。承認フローなどに便利です。
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
@ttflow.sideeffect()
|
|
100
|
+
def 承認待ち(c: RunContext):
|
|
101
|
+
c.wait_event(f"承認:{c.get_context_data().run_id}")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 冪等な再実行の仕組み
|
|
105
|
+
|
|
106
|
+
中断から再開するとき、ワークフローは**関数の最初から再実行**されます。メモリ上の状態を保存しているわけではありません。
|
|
107
|
+
|
|
108
|
+
このままでは副作用が重複して実行されてしまいますが、ttflowは以下の仕組みで冪等性を保証します。
|
|
109
|
+
|
|
110
|
+
#### `@sideeffect()` — 副作用を1回だけ実行
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
@ttflow.sideeffect()
|
|
114
|
+
def send_notification(c: RunContext, message: str):
|
|
115
|
+
# 外部APIへの通知など。再実行時はスキップされる
|
|
116
|
+
requests.post("https://...", json={"message": message})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`@sideeffect()` で修飾された関数は、同じ `run_id` で2回目以降の実行時には自動的にスキップされます。
|
|
120
|
+
|
|
121
|
+
#### `get_state()` / `set_state()` — 再実行に安全な状態管理
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
@ttflow.workflow()
|
|
125
|
+
def wf(c: RunContext, args: dict):
|
|
126
|
+
c.set_state("count", 1) # 初回のみ書き込み。再実行時はno-op
|
|
127
|
+
value = c.get_state("count") # 初回の値がキャッシュされ、再実行時も同じ値を返す
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- `set_state()` — 初回のみ実行され、再実行時には何もしない
|
|
131
|
+
- `get_state()` — 初回アクセス時の値をキャッシュし、再実行時も同じ値を返す
|
|
132
|
+
|
|
133
|
+
これらの仕組みにより、ワークフローは何度再実行されても、あたかも中断地点から再開したかのように振る舞います。
|
|
134
|
+
|
|
135
|
+
## ユースケース例
|
|
136
|
+
|
|
137
|
+
### 値段監視
|
|
138
|
+
|
|
139
|
+
商品の値段を定期的にチェックし、希望価格以下になったら通知するワークフローです。
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
@ttflow.workflow()
|
|
143
|
+
def 買いたいもの追加(c: RunContext, args: dict):
|
|
144
|
+
item_name = args["item_name"]
|
|
145
|
+
price = args["price"]
|
|
146
|
+
|
|
147
|
+
count = 1
|
|
148
|
+
while True:
|
|
149
|
+
current_price = 値段を取得(c, item_name)
|
|
150
|
+
if current_price <= price:
|
|
151
|
+
c.log(f"{count}回目: {item_name}は{current_price}円!")
|
|
152
|
+
return
|
|
153
|
+
c.log(f"{count}回目: {item_name}は{current_price}で買えませんでした")
|
|
154
|
+
count += 1
|
|
155
|
+
c.pause_once() # 次回のrun()まで待機
|
|
156
|
+
|
|
157
|
+
@ttflow.sideeffect()
|
|
158
|
+
def 値段を取得(c: RunContext, item_name: str) -> int:
|
|
159
|
+
# 実際にはここで外部APIから価格を取得する
|
|
160
|
+
return fetch_price_from_api(item_name)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
python watch_price.py run 買いたいもの追加 '{"item_name":"レモン","price":150}'
|
|
165
|
+
python watch_price.py run # 定期実行(cronなどで)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`pause_once()` のたびに中断されるので、cronやCloudWatchスケジューラで定期的に `run` を呼べば、値段が下がるまで繰り返しチェックできます。複数の商品を同時に監視することもできます。
|
|
169
|
+
|
|
170
|
+
### 承認フロー
|
|
171
|
+
|
|
172
|
+
外部からのイベントを待って再開するパターンです。
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
@ttflow.workflow()
|
|
176
|
+
def デプロイ(c: RunContext, args: dict):
|
|
177
|
+
notification(c, "デプロイを開始します")
|
|
178
|
+
承認待ち(c)
|
|
179
|
+
# ↑ 「承認」イベントが来るまで中断される
|
|
180
|
+
notification(c, "承認されました。デプロイを続行します")
|
|
181
|
+
|
|
182
|
+
@ttflow.sideeffect()
|
|
183
|
+
def 承認待ち(c: RunContext):
|
|
184
|
+
c.wait_event(f"承認:{c.get_context_data().run_id}")
|
|
185
|
+
|
|
186
|
+
@ttflow.sideeffect()
|
|
187
|
+
def notification(c: RunContext, message: str):
|
|
188
|
+
c.log(message)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
python deploy.py run デプロイ # 中断される
|
|
193
|
+
python deploy.py run 承認:<run_id> # 承認イベントを発行 → 再開
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## StateRepository(永続化バックエンド)
|
|
197
|
+
|
|
198
|
+
`setup()` の `state_repository` 引数でバックエンドを選択します。
|
|
199
|
+
|
|
200
|
+
| 指定 | 用途 |
|
|
201
|
+
| ------------------------- | ------------------------------------ |
|
|
202
|
+
| `"local:state.json"` | ローカルファイル。開発・個人利用向け |
|
|
203
|
+
| `"s3:bucket-name/prefix"` | S3。Lambda等のサーバーレス環境向け |
|
|
204
|
+
| `"dynamodb:table-name"` | DynamoDB |
|
|
205
|
+
| `"onmemory"` | オンメモリ。テスト向け |
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
ttflow = setup(state_repository="s3:my-bucket/workflows")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## 運用方法
|
|
212
|
+
|
|
213
|
+
ワークフローは実行してすぐ終了するただのスクリプトなので、様々な運用方法が考えられます。
|
|
214
|
+
|
|
215
|
+
### ローカル実行
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
python my_workflow.py run トリガー名
|
|
219
|
+
python my_workflow.py run # 中断中のワークフローを再開
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`run_by_cli()` を使うと、[python-fire](https://github.com/google/python-fire) ベースのCLIが利用できます。
|
|
223
|
+
|
|
224
|
+
### AWS Lambda
|
|
225
|
+
|
|
226
|
+
Lambda上で動かすのが最も簡単な運用方法です。
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from ttflow import setup
|
|
230
|
+
from ttflow.powerup.run_by_lambda import run_by_lambda
|
|
231
|
+
|
|
232
|
+
ttflow = setup(state_repository="s3:my-bucket/state")
|
|
233
|
+
|
|
234
|
+
# ワークフロー定義...
|
|
235
|
+
|
|
236
|
+
handler = run_by_lambda(ttflow)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- Lambda関数URLやAPI Gatewayからトリガー名を指定して実行
|
|
240
|
+
- CloudWatchスケジューラで定期的に `run()` を呼んで中断中のワークフローを再開
|
|
241
|
+
- 永続化バックエンドにはS3やDynamoDBを使用
|