processes 2.0.1__tar.gz → 3.0.1__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.
Files changed (85) hide show
  1. processes-3.0.1/.github/PULL_REQUEST_TEMPLATE.md +10 -0
  2. {processes-2.0.1 → processes-3.0.1}/CHANGELOG.md +21 -0
  3. processes-3.0.1/CONTRIBUTING.md +135 -0
  4. processes-3.0.1/PKG-INFO +407 -0
  5. processes-3.0.1/README.md +383 -0
  6. {processes-2.0.1 → processes-3.0.1}/docs/examples/advanced.md +17 -12
  7. {processes-2.0.1 → processes-3.0.1}/docs/index.md +56 -2
  8. {processes-2.0.1 → processes-3.0.1}/docs/reference.md +2 -1
  9. {processes-2.0.1 → processes-3.0.1}/src/processes/__init__.py +6 -7
  10. processes-3.0.1/src/processes/_email_internals.py +173 -0
  11. processes-3.0.1/src/processes/_tb_utils.py +79 -0
  12. processes-3.0.1/src/processes/email_config.py +99 -0
  13. processes-3.0.1/src/processes/exceptions.py +36 -0
  14. {processes-2.0.1 → processes-3.0.1}/src/processes/process.py +71 -51
  15. processes-3.0.1/src/processes/task.py +434 -0
  16. processes-3.0.1/tests/base_test.py +62 -0
  17. {processes-2.0.1 → processes-3.0.1}/tests/manual_tests/manual_pipeline_inspect.py +8 -8
  18. {processes-2.0.1 → processes-3.0.1}/tests/manual_tests/manual_themed_tracebacks.py +17 -13
  19. processes-3.0.1/tests/test_args_kwargs.py +125 -0
  20. {processes-2.0.1 → processes-3.0.1}/tests/test_complex_dag_failures.py +33 -99
  21. processes-3.0.1/tests/test_dependencies.py +232 -0
  22. processes-3.0.1/tests/test_email_themes.py +530 -0
  23. processes-3.0.1/tests/test_logfiles.py +84 -0
  24. processes-3.0.1/tests/test_normal_run_no_errors.py +125 -0
  25. {processes-2.0.1 → processes-3.0.1}/tests/test_parallel_race_conditions.py +21 -33
  26. processes-3.0.1/tests/test_timeout_retry.py +337 -0
  27. processes-3.0.1/tests/test_unique_name.py +40 -0
  28. processes-2.0.1/PKG-INFO +0 -234
  29. processes-2.0.1/README.md +0 -210
  30. processes-2.0.1/src/processes/exception_html_formatter.py +0 -351
  31. processes-2.0.1/src/processes/html_logging.py +0 -160
  32. processes-2.0.1/src/processes/task.py +0 -307
  33. processes-2.0.1/tests/conftest.py +0 -9
  34. processes-2.0.1/tests/log_cleaner.py +0 -8
  35. processes-2.0.1/tests/test_args_kwargs.py +0 -166
  36. processes-2.0.1/tests/test_dependencies.py +0 -266
  37. processes-2.0.1/tests/test_email_themes.py +0 -489
  38. processes-2.0.1/tests/test_examples.py +0 -48
  39. processes-2.0.1/tests/test_logfiles.py +0 -130
  40. processes-2.0.1/tests/test_normal_run_no_errors.py +0 -174
  41. processes-2.0.1/tests/test_unique_name.py +0 -45
  42. {processes-2.0.1 → processes-3.0.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  43. {processes-2.0.1 → processes-3.0.1}/.github/ISSUE_TEMPLATE/custom.md +0 -0
  44. {processes-2.0.1 → processes-3.0.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  45. {processes-2.0.1 → processes-3.0.1}/.github/workflows/docs.yml +0 -0
  46. {processes-2.0.1 → processes-3.0.1}/.github/workflows/lint-pr.yml +0 -0
  47. {processes-2.0.1 → processes-3.0.1}/.github/workflows/lint.yml +0 -0
  48. {processes-2.0.1 → processes-3.0.1}/.github/workflows/mypy.yml +0 -0
  49. {processes-2.0.1 → processes-3.0.1}/.github/workflows/publish.yml +0 -0
  50. {processes-2.0.1 → processes-3.0.1}/.github/workflows/tags.yml +0 -0
  51. {processes-2.0.1 → processes-3.0.1}/.github/workflows/tests.yml +0 -0
  52. {processes-2.0.1 → processes-3.0.1}/.gitignore +0 -0
  53. {processes-2.0.1 → processes-3.0.1}/LICENSE +0 -0
  54. {processes-2.0.1 → processes-3.0.1}/assets/banner.svg +0 -0
  55. {processes-2.0.1 → processes-3.0.1}/assets/logo.png +0 -0
  56. {processes-2.0.1 → processes-3.0.1}/assets/logo.svg +0 -0
  57. {processes-2.0.1 → processes-3.0.1}/assets/social_banner.png +0 -0
  58. {processes-2.0.1 → processes-3.0.1}/assets/social_banner.svg +0 -0
  59. {processes-2.0.1 → processes-3.0.1}/docs/assets/favicon.svg +0 -0
  60. {processes-2.0.1 → processes-3.0.1}/docs/examples/basic.md +0 -0
  61. {processes-2.0.1 → processes-3.0.1}/docs/examples/intro.md +0 -0
  62. {processes-2.0.1 → processes-3.0.1}/examples/01_basic_tasks_and_dependencies/README.md +0 -0
  63. {processes-2.0.1 → processes-3.0.1}/examples/01_basic_tasks_and_dependencies/example1.py +0 -0
  64. {processes-2.0.1 → processes-3.0.1}/examples/02_task_dependencies_result_passing/README.md +0 -0
  65. {processes-2.0.1 → processes-3.0.1}/examples/02_task_dependencies_result_passing/example2.py +0 -0
  66. {processes-2.0.1 → processes-3.0.1}/examples/README.md +0 -0
  67. {processes-2.0.1 → processes-3.0.1}/mkdocs.yml +0 -0
  68. {processes-2.0.1 → processes-3.0.1}/pyproject.toml +0 -0
  69. {processes-2.0.1 → processes-3.0.1}/pytest.ini +0 -0
  70. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/languages/de.json +0 -0
  71. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/languages/en.json +0 -0
  72. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/languages/es.json +0 -0
  73. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/languages/fr.json +0 -0
  74. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/languages/it.json +0 -0
  75. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/languages/pt.json +0 -0
  76. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/palettes/catppuccin.css +0 -0
  77. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/palettes/neobones.css +0 -0
  78. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/palettes/neutral.css +0 -0
  79. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/palettes/slate.css +0 -0
  80. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/styles/classic.html +0 -0
  81. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/styles/compact.html +0 -0
  82. {processes-2.0.1 → processes-3.0.1}/src/processes/themes/styles/modern.html +0 -0
  83. {processes-2.0.1 → processes-3.0.1}/tests/__init__.py +0 -0
  84. {processes-2.0.1 → processes-3.0.1}/tests/manual_tests/__init__.py +0 -0
  85. {processes-2.0.1 → processes-3.0.1}/uv.lock +0 -0
@@ -0,0 +1,10 @@
1
+ ## What and why
2
+ <!-- Describe the change and the motivation behind it -->
3
+
4
+ ## Type of change
5
+ - [ ] `feat` — new feature
6
+ - [ ] `fix` — bug fix
7
+ - [ ] `refactor` — no behavior change
8
+ - [ ] `docs` — documentation only
9
+ - [ ] `test` — tests only
10
+ - [ ] `chore` / `ci` / `build`
@@ -1,3 +1,24 @@
1
+ ## v3.0.1 (2026-06-13)
2
+
3
+ ### Refactor
4
+
5
+ - **process**: use bare raise and extract _is_done helper
6
+
7
+ ## v3.0.0 (2026-06-13)
8
+
9
+ ### BREAKING CHANGE
10
+
11
+ - html_mail_handler parameter removed from Task; use
12
+ smtp_config and email_style instead. HTMLSMTPHandler removed from public API.
13
+
14
+ ### Feat
15
+
16
+ - **html_logging**: expose last_path_traced_vars on HTMLSMTPHandler
17
+
18
+ ### Refactor
19
+
20
+ - **email**: replace HTMLSMTPHandler with SMTPConfig/HTMLEmailStyle
21
+
1
22
  ## v2.0.1 (2026-06-12)
2
23
 
3
24
  ### Fix
@@ -0,0 +1,135 @@
1
+ # Contributing to Processes
2
+
3
+ Thanks for your interest in contributing! This document covers the two
4
+ contributor paths, project conventions, and how to submit a change.
5
+
6
+ ## Contributor paths
7
+
8
+ **Maintainer** (direct push access): clone the repository directly.
9
+
10
+ ```bash
11
+ git clone https://github.com/oliverm91/processes.git
12
+ cd processes
13
+ uv sync
14
+ ```
15
+
16
+ **External contributor** (everyone else): fork first, then clone your fork.
17
+
18
+ ```bash
19
+ # 1. Fork on GitHub (button top-right of the repo page)
20
+ # 2. Clone your fork
21
+ git clone https://github.com/<your-username>/processes.git
22
+ cd processes
23
+ uv sync
24
+ ```
25
+
26
+ Both paths use [uv](https://docs.astral.sh/uv/) for dependency management.
27
+ `uv sync` installs all dev dependencies (pytest, mypy, ruff, mkdocs, commitizen).
28
+
29
+ ## Project layout
30
+
31
+ ```
32
+ src/processes/ # library source (the public package)
33
+ tests/ # pytest test suite
34
+ examples/ # runnable usage examples
35
+ docs/ # mkdocs documentation source
36
+ ```
37
+
38
+ Keep new public symbols in `src/processes/` and re-export them from
39
+ `src/processes/__init__.py` if they are part of the stable API.
40
+
41
+ ## Running checks locally
42
+
43
+ Before opening a pull request, make sure the following pass:
44
+
45
+ ```bash
46
+ uv run pytest # test suite
47
+ uv run mypy # strict type checking
48
+ uv run ruff check . # linting
49
+ uv run ruff format . # formatting
50
+ ```
51
+
52
+ CI runs the same checks on every push and pull request.
53
+
54
+ ## Style guide
55
+
56
+ - Python >= 3.10 syntax.
57
+ - Type hints on all new code. `mypy` is configured with `strict = true` and
58
+ `disallow_untyped_defs = true`.
59
+ - Ruff rules: `F`, `E`, `W`, `I`, `B`, `UP`. Line length is 100.
60
+ - Prefer the standard library — the library has zero runtime dependencies.
61
+ - No comments that just restate the code. A comment is welcome only when it
62
+ captures a non-obvious *why*.
63
+
64
+ ## Tests
65
+
66
+ - Add or update tests in `tests/` for any behavior change.
67
+ - Keep tests focused and deterministic; avoid sleeps where possible.
68
+ - If you fix a bug, add a regression test that fails before the fix.
69
+
70
+ ## Commit messages
71
+
72
+ This project follows [Conventional Commits](https://www.conventionalcommits.org/),
73
+ enforced by [commitizen](https://commitizen-tools.github.io/commitizen/). Use
74
+ `uv run cz commit` to be prompted through an interactive commit, or write the
75
+ message by hand:
76
+
77
+ ```
78
+ <type>(<scope>): <short description>
79
+
80
+ <optional body>
81
+
82
+ <optional footer>
83
+ ```
84
+
85
+ Common types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`,
86
+ `build`, `ci`.
87
+
88
+ ## Submitting a pull request
89
+
90
+ 1. Create a feature branch: `git checkout -b feat/my-change`.
91
+ 2. Make your changes, add tests, and confirm all checks pass.
92
+ 3. Push the branch to your fork (or directly, if you're a maintainer) and open
93
+ a PR against `main` on the upstream repository.
94
+ 4. Fill in a clear description of the *what* and the *why*.
95
+ 5. Make sure CI is green and the changelog preview looks reasonable for
96
+ user-visible changes.
97
+ 6. Be ready to revise — code review is a normal part of the process.
98
+
99
+ Prefer several small, focused PRs over one large one.
100
+
101
+ ## Documentation
102
+
103
+ User-facing changes should be reflected in the docs under `docs/`. To preview
104
+ locally:
105
+
106
+ ```bash
107
+ uv run mkdocs serve
108
+ ```
109
+
110
+ ## Versioning and releases
111
+
112
+ Versions are derived from git tags via `hatch-vcs` (`v$version`, e.g. `v2.0.1`).
113
+ Do not edit the version in `pyproject.toml` manually. To cut a release, the
114
+ maintainer runs:
115
+
116
+ ```bash
117
+ uv run cz bump
118
+ ```
119
+
120
+ This updates the version, regenerates the changelog, creates a tag, and
121
+ triggers the `publish.yml` workflow to push the release to PyPI.
122
+
123
+ ## Reporting issues
124
+
125
+ Use the GitHub issue tracker. Please include:
126
+
127
+ - A minimal, self-contained reproduction.
128
+ - The Python version, library version, and operating system.
129
+ - The full traceback for crashes, and the smallest possible task graph that
130
+ triggers the problem.
131
+
132
+ ## License
133
+
134
+ By contributing, you agree that your contributions will be licensed under the
135
+ MIT License.
@@ -0,0 +1,407 @@
1
+ Metadata-Version: 2.4
2
+ Name: processes
3
+ Version: 3.0.1
4
+ Summary: A Python library for managing and executing dependent tasks in parallel or sequential order with automatic dependency resolution and topological sorting
5
+ Project-URL: Homepage, https://github.com/oliverm91/processes
6
+ Project-URL: Documentation, https://oliverm91.github.io/processes/
7
+ Project-URL: Repository, https://github.com/oliverm91/processes
8
+ Project-URL: Issues, https://github.com/oliverm91/processes/issues
9
+ Project-URL: Changelog, https://github.com/oliverm91/processes/blob/main/CHANGELOG.md
10
+ Author-email: Oliver Mohr Bonometti <oliver.mohr.b@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: dag,dependencies,etl,parallel,process,tasks,topological-sort,workflow
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+
25
+ <div align="center">
26
+ <img src="https://raw.githubusercontent.com/oliverm91/processes/refs/heads/main/assets/banner.svg" width="100%" alt="Processes - Smart Task Orchestration">
27
+ </div>
28
+
29
+ # Processes: Smart Task Orchestration
30
+
31
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
32
+ ![Fast & Lightweight](https://img.shields.io/badge/Library-Pure%20Python-green.svg)
33
+ [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg)](https://oliverm91.github.io/processes/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
35
+ [![PyPI version](https://img.shields.io/pypi/v/processes.svg)](https://pypi.org/project/processes/)
36
+
37
+ [![Python Tests Status](https://github.com/oliverm91/processes/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/oliverm91/processes/actions/workflows/tests.yml)
38
+ [![Ruff Lint Status](https://github.com/oliverm91/processes/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/oliverm91/processes/actions/workflows/lint.yml)
39
+ [![mypy-check](https://github.com/oliverm91/processes/actions/workflows/mypy.yml/badge.svg)](https://github.com/oliverm91/processes/actions/workflows/mypy.yml)
40
+
41
+ ---
42
+
43
+ **Run a list of Python callables that depend on each other — in parallel when possible, with per-task log files and optional HTML email notification on failure. Zero dependencies. Pure Python 3.10+.**
44
+
45
+ ---
46
+
47
+ ## ✨ Why Processes?
48
+
49
+ - 🔗 **Declare what depends on what** — write your tasks in any order; the runtime sorts them so every dependency runs first.
50
+ - ⚡ **Run in parallel when you can** — independent tasks run together on a thread pool; the runtime switches on automatically for jobs with 10+ tasks.
51
+ - 🛡️ **One failure doesn't stop the rest** — a failed task skips only the jobs that depend on it, and **every other part of the workflow keeps running**.
52
+ - 📝 **One log file per task** — share a single log across the whole run, or keep them separate for easier debugging.
53
+ - 📧 **Email alerts when something breaks** — pass an `SMTPConfig` to a task and get a styled HTML email (with traceback, task context, and the list of jobs that were skipped) the instant it raises.
54
+ - 🧰 **Modern, strictly-typed Python 3.10+** — `from __future__ import annotations`, full `mypy --strict` clean, `dict[str, TaskResult]`, `set[str]`, `|` unions.
55
+
56
+ ---
57
+
58
+ ## ⚙️ How it works
59
+
60
+ A `Process` holds a list of `Task`s. At construction it validates names, types, dependency references, and detects cycles — raising before anything runs.
61
+
62
+ When you call `process.run()`, tasks are topologically sorted and scheduled: dependencies first, independent tasks in parallel.
63
+
64
+ A `TaskDependency` can forward an upstream result directly into a downstream function, as a positional or keyword argument. The result is a `ProcessResult` with `passed_tasks_results` and `failed_tasks` for inspection.
65
+
66
+ ---
67
+
68
+ ## 🚀 Quick start
69
+
70
+ A 15-line "hello pipeline" — one upstream task feeding a downstream one, run in parallel.
71
+
72
+ ```python
73
+ from processes import Process, Task, TaskDependency
74
+
75
+
76
+ def load_users() -> list[dict]:
77
+ return [{"id": 1}, {"id": 2}, {"id": 3}]
78
+
79
+
80
+ def enrich(users: list[dict]) -> list[dict]:
81
+ return [{**u, "name": f"user-{u['id']}"} for u in users]
82
+
83
+
84
+ tasks = [
85
+ Task("load", "run.log", load_users),
86
+ Task(
87
+ "enrich",
88
+ "run.log",
89
+ enrich,
90
+ dependencies=[TaskDependency("load", use_result_as_additional_args=True)],
91
+ ),
92
+ ]
93
+
94
+ with Process(tasks) as p:
95
+ result = p.run(parallel=True)
96
+
97
+ print(result.passed_tasks_results["enrich"].result)
98
+ # [{'id': 1, 'name': 'user-1'}, {'id': 2, 'name': 'user-2'}, {'id': 3, 'name': 'user-3'}]
99
+ ```
100
+
101
+ ---
102
+
103
+ ## 🧪 End-to-end example
104
+
105
+ A realistic mini-pipeline: fetch two sources **in parallel**, transform them, aggregate, and notify — with per-task log files, result piping, and one task deliberately failing to show fault isolation.
106
+
107
+ <details>
108
+ <summary>Show the full end-to-end example</summary>
109
+
110
+ ```python
111
+ import logging
112
+ from pathlib import Path
113
+
114
+ from processes import HTMLEmailStyle, Process, SMTPConfig, Task, TaskDependency
115
+
116
+ LOG_DIR = Path("logs")
117
+ LOG_DIR.mkdir(exist_ok=True)
118
+
119
+
120
+ # --- 1. Two independent "fetch" tasks that run in parallel -----------------
121
+ def fetch_orders() -> list[dict]:
122
+ logging.info("querying orders API")
123
+ return [{"order_id": 1, "amount": 42.0}, {"order_id": 2, "amount": 17.5}]
124
+
125
+
126
+ def fetch_inventory() -> list[dict]:
127
+ logging.info("querying inventory API")
128
+ return [{"sku": "A-1", "qty": 12}, {"sku": "B-2", "qty": 3}]
129
+
130
+
131
+ # --- 2. Two transforms that consume the upstream results -------------------
132
+ def total_revenue(orders: list[dict]) -> float:
133
+ total = sum(o["amount"] for o in orders)
134
+ logging.info("revenue computed: %s", total)
135
+ return total
136
+
137
+
138
+ def stock_value(inventory: list[dict], *, price_per_unit: float = 10.0) -> float:
139
+ value = sum(i["qty"] for i in inventory) * price_per_unit
140
+ logging.info("stock value: %s", value)
141
+ return value
142
+
143
+
144
+ # --- 3. An aggregator that joins the two branches --------------------------
145
+ def build_report(*, revenue: float, stock: float) -> str:
146
+ return f"daily-report | revenue={revenue:.2f} stock={stock:.2f}"
147
+
148
+
149
+ # --- 4. A flaky notifier that ALWAYS fails — to show fault isolation -------
150
+ def notify_slack(report: str) -> None:
151
+ raise RuntimeError("slack webhook returned 503")
152
+
153
+
154
+ # --- 5. A sibling task that does NOT depend on notify and still runs -------
155
+ def archive_report(report: str) -> str:
156
+ out = LOG_DIR / "report.txt"
157
+ out.write_text(report)
158
+ return str(out)
159
+
160
+
161
+ # --- 6. Optional: SMTP config so failures page on-call --------------------
162
+ smtp = SMTPConfig(
163
+ mailhost=("smtp.example.com", 587),
164
+ fromaddr="alerts@example.com",
165
+ toaddrs=["oncall@example.com"],
166
+ credentials=("user", "pass"),
167
+ secure=(),
168
+ )
169
+
170
+ tasks = [
171
+ Task("fetch_orders", LOG_DIR / "fetch_orders.log", fetch_orders),
172
+ Task("fetch_inventory", LOG_DIR / "fetch_inventory.log", fetch_inventory),
173
+
174
+ Task(
175
+ "compute_revenue",
176
+ LOG_DIR / "compute_revenue.log",
177
+ total_revenue,
178
+ dependencies=[TaskDependency("fetch_orders", use_result_as_additional_args=True)],
179
+ ),
180
+ Task(
181
+ "compute_stock",
182
+ LOG_DIR / "compute_stock.log",
183
+ stock_value,
184
+ kwargs={"price_per_unit": 7.25},
185
+ dependencies=[
186
+ TaskDependency(
187
+ "fetch_inventory",
188
+ use_result_as_additional_kwargs=True,
189
+ additional_kwarg_name="inventory",
190
+ )
191
+ ],
192
+ ),
193
+
194
+ Task(
195
+ "build_report",
196
+ LOG_DIR / "build_report.log",
197
+ build_report,
198
+ dependencies=[
199
+ TaskDependency("compute_revenue", use_result_as_additional_kwargs=True,
200
+ additional_kwarg_name="revenue"),
201
+ TaskDependency("compute_stock", use_result_as_additional_kwargs=True,
202
+ additional_kwarg_name="stock"),
203
+ ],
204
+ ),
205
+
206
+ # notify_slack fails on purpose. archive_report is a *sibling*
207
+ # of notify_slack (both depend on build_report), so it has no
208
+ # dependency on the failed task and runs normally — the rest of
209
+ # the workflow is not blackholed by one broken step.
210
+ Task(
211
+ "notify_slack",
212
+ LOG_DIR / "notify_slack.log",
213
+ notify_slack,
214
+ dependencies=[TaskDependency("build_report", use_result_as_additional_args=True)],
215
+ smtp_config=smtp,
216
+ ),
217
+ Task(
218
+ "archive_report",
219
+ LOG_DIR / "archive_report.log",
220
+ archive_report,
221
+ dependencies=[TaskDependency("build_report", use_result_as_additional_args=True)],
222
+ ),
223
+ ]
224
+
225
+ with Process(tasks) as process:
226
+ result = process.run(parallel=True)
227
+
228
+ print("passed:", sorted(result.passed_tasks_results))
229
+ # archive_report, build_report, compute_revenue, compute_stock, fetch_inventory, fetch_orders
230
+ print("failed:", sorted(result.failed_tasks))
231
+ # notify_slack
232
+ print("report:", result.passed_tasks_results["build_report"].result)
233
+ # daily-report | revenue=59.50 stock=262.50
234
+ ```
235
+
236
+ The failing `notify_slack` task does **not** abort the run. `archive_report` is a sibling of the failed task (both depend on the successful `build_report`), so it runs unaffected — the rest of the workflow is not blackholed by one broken step. The HTML email handler also fires on the `notify_slack` task, paging on-call with the full traceback and the list of downstream tasks that were skipped because of it.
237
+
238
+ </details>
239
+
240
+ ---
241
+
242
+ ## 📚 API Reference
243
+
244
+ <details>
245
+ <summary>Show API reference</summary>
246
+
247
+ ### `Task`
248
+
249
+ ```python
250
+ Task(
251
+ name: str,
252
+ log_path: str | os.PathLike,
253
+ func: Callable[..., Any],
254
+ args: tuple = (),
255
+ kwargs: dict | None = None,
256
+ dependencies: list[TaskDependency] | None = None,
257
+ smtp_config: SMTPConfig | None = None,
258
+ email_style: HTMLEmailStyle | None = None,
259
+ timeout: float | None = None,
260
+ retries: int | None = 0,
261
+ retry_on: tuple[type[Exception], ...] | None = None,
262
+ )
263
+ ```
264
+
265
+ - `name` — unique within the `Process`; no spaces.
266
+ - `log_path` — the file this task logs to (INFO level, format `%(asctime)s - %(name)s - %(levelname)s - %(message)s`).
267
+ - `func` — the callable; receives `func(*args, **kwargs)` after result-injection.
268
+ - `smtp_config` — when set, fires an HTML email on `logging.ERROR`; body includes `task_name`, `function`, `args`, `kwargs`, and `downstream_impact`.
269
+ - `email_style` — optional presentation override; defaults to `HTMLEmailStyle()` (modern, neutral, English) when `smtp_config` is set.
270
+ - `timeout` — seconds allowed per attempt; `None` means no limit. When the timeout fires the underlying thread is detached (Python threading limitation).
271
+ - `retries` — additional attempts after the first failure; `0` or `None` means a single attempt. Defaults to `0`.
272
+ - `retry_on` — tuple of exception types that trigger a retry. When `retries >= 1` and `retry_on` is `None`, defaults to `(ConnectionError, TimeoutError)` at call time.
273
+
274
+ ### `TaskDependency`
275
+
276
+ ```python
277
+ TaskDependency(
278
+ task_name: str,
279
+ use_result_as_additional_args: bool = False,
280
+ use_result_as_additional_kwargs: bool = False,
281
+ additional_kwarg_name: str = "",
282
+ )
283
+ ```
284
+
285
+ - `use_result_as_additional_args=True` — upstream result appended as the next **positional** arg.
286
+ - `use_result_as_additional_kwargs=True` with a non-empty `additional_kwarg_name` — upstream result injected as a **keyword** arg.
287
+ - Both flags can be combined (positional first, then kwarg).
288
+
289
+ ### `Process`
290
+
291
+ ```python
292
+ Process(tasks: list[Task]) # validates types, names, deps, cycles
293
+
294
+ process.run(parallel: bool | None = None, max_workers: int = 4) -> ProcessResult
295
+ ```
296
+
297
+ - Raises `DependencyNotFoundError`, `CircularDependencyError`, `TypeError`, `ValueError` on construction if the workflow is malformed.
298
+ - `parallel=None` auto-parallelises when `len(tasks) >= 10`; `max_workers=1` is always sequential.
299
+ - Use as a context manager — it cleans up `FileHandler`s on exit.
300
+
301
+ ### `ProcessResult`
302
+
303
+ ```python
304
+ result.passed_tasks_results # dict[str, TaskResult] — name → TaskResult for every task that succeeded
305
+ result.failed_tasks # set[str] — all tasks that did not produce a result (errored + skipped)
306
+ result.errored_tasks # set[str] — tasks whose function actually raised
307
+ result.skipped_tasks # set[str] — tasks skipped because an upstream dependency failed
308
+
309
+ TaskResult(worked: bool, result: Any, exception: Exception | None)
310
+ ```
311
+
312
+ ### `SMTPConfig`
313
+
314
+ ```python
315
+ SMTPConfig(
316
+ mailhost, # (host, port)
317
+ fromaddr,
318
+ toaddrs, # list[str]
319
+ credentials=None, # (username, password) | None
320
+ secure=None, # () = STARTTLS; omit for no encryption
321
+ timeout=5,
322
+ )
323
+ ```
324
+
325
+ ### `HTMLEmailStyle`
326
+
327
+ ```python
328
+ HTMLEmailStyle(
329
+ style="modern", # classic | modern | compact
330
+ palette="neutral", # neutral | catppuccin | neobones | slate
331
+ language="en", # en | es | pt | fr | de | it
332
+ traced_vars_frame_filter=None, # substring to pick the traced frame | None
333
+ )
334
+ ```
335
+
336
+ All fields are optional — omit `HTMLEmailStyle` entirely to use the defaults.
337
+
338
+ </details>
339
+
340
+ <details>
341
+ <summary>Show fault-tolerance rules in detail</summary>
342
+
343
+ When a task raises:
344
+
345
+ 1. The exception is caught and stored in `TaskResult.exception`; the task name goes into `failed_tasks` and `errored_tasks`.
346
+ 2. **Every task that depends on it (directly or indirectly) is skipped** — added to `failed_tasks` and `skipped_tasks` without running.
347
+ 3. **Every other independent part of the workflow keeps running.** With `parallel=True` they keep running concurrently on the worker pool.
348
+ 4. After `run()` returns, `ProcessResult.errored_tasks` and `ProcessResult.skipped_tasks` let you distinguish root failures from cascade skips for triage or alerting.
349
+
350
+ When a task has `retries >= 1`, a failure matching `retry_on` triggers another attempt before the task is declared failed and its dependants are skipped. This gives transient errors (network blips, connection resets) a chance to resolve without aborting downstream work.
351
+
352
+ This makes the library a good fit for fan-out / fan-in pipelines, "best-effort" notifications, and any workflow where one broken step should not blackhole the rest.
353
+
354
+ </details>
355
+
356
+ <details>
357
+ <summary>Show comparison with other libraries</summary>
358
+
359
+ | | **Processes** | Airflow | Celery | Luigi |
360
+ |---|---|---|---|---|
361
+ | External dependencies | **None** | many | broker (Redis/RabbitMQ) | few |
362
+ | Setup cost | `pip install` | cluster | broker + workers | task + config |
363
+ | Parallelism | built-in | via executors | via workers | via workers |
364
+ | Per-task file logs | **yes (built-in)** | via handlers | via signals | partial |
365
+ | HTML email on failure | **yes (built-in)** | via callbacks | via signals | manual |
366
+ | DAG validation at construction | **yes** | yes (DAG file) | n/a | partial |
367
+ | Strict typing (`mypy --strict`) | **yes** | partial | partial | no |
368
+
369
+ `Processes` is **not** a distributed scheduler — there are no workers on remote machines, no SLA monitoring, no web UI. If you need any of those, you need Airflow or a similar orchestrator. If you want a small, fast, dependency-aware pipeline that *just runs* in a single process, this is it.
370
+
371
+ </details>
372
+
373
+ <details>
374
+ <summary>Show advanced configuration</summary>
375
+
376
+ - **Shared log file** — pass the same `log_path` to every `Task` for a single combined run.log; pass distinct paths for per-task isolation.
377
+ - **Auto-parallel** — `Process.run()` with no argument runs sequentially for small workflows and switches to parallel for `len(tasks) >= 10`. Pass `parallel=True` or `parallel=False` to force the mode.
378
+ - **Result inspection** — iterate `result.passed_tasks_results.items()` to log or post-process every successful task; iterate `result.failed_tasks` for triage.
379
+ - **Re-raising** — wrap `process.run()` in `try/except` if you need a non-zero exit code on any failure; the library itself does not raise on partial failure.
380
+
381
+ </details>
382
+
383
+ ---
384
+
385
+ ## 📦 Installation
386
+
387
+ From PyPI:
388
+
389
+ ```bash
390
+ pip install processes
391
+ ```
392
+
393
+ Or straight from the repository (pure Python, no build step):
394
+
395
+ ```bash
396
+ pip install git+https://github.com/oliverm91/processes.git
397
+ ```
398
+
399
+ Requires **Python 3.10+**.
400
+
401
+ ---
402
+
403
+ ## 📄 License & contributing
404
+
405
+ Released under the **MIT License** — see [docs](https://oliverm91.github.io/processes/) for full API details.
406
+
407
+ Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow, style, and commit-message conventions used by this project.