processes 3.1.0__tar.gz → 3.1.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 (75) hide show
  1. {processes-3.1.0 → processes-3.1.1}/CHANGELOG.md +6 -0
  2. {processes-3.1.0 → processes-3.1.1}/PKG-INFO +15 -1
  3. {processes-3.1.0 → processes-3.1.1}/README.md +14 -0
  4. {processes-3.1.0 → processes-3.1.1}/docs/index.md +7 -0
  5. {processes-3.1.0 → processes-3.1.1}/src/processes/_email_internals.py +55 -6
  6. {processes-3.1.0 → processes-3.1.1}/src/processes/_error_data.py +37 -1
  7. processes-3.1.0/src/processes/_log_formatting.py → processes-3.1.1/src/processes/_logfile_formatting.py +14 -1
  8. {processes-3.1.0 → processes-3.1.1}/src/processes/_tb_utils.py +82 -4
  9. {processes-3.1.0 → processes-3.1.1}/src/processes/email_config.py +13 -3
  10. {processes-3.1.0 → processes-3.1.1}/src/processes/process.py +10 -1
  11. {processes-3.1.0 → processes-3.1.1}/src/processes/task.py +9 -2
  12. processes-3.1.0/RELEASE_NOTES.md +0 -36
  13. {processes-3.1.0 → processes-3.1.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  14. {processes-3.1.0 → processes-3.1.1}/.github/ISSUE_TEMPLATE/custom.md +0 -0
  15. {processes-3.1.0 → processes-3.1.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  16. {processes-3.1.0 → processes-3.1.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  17. {processes-3.1.0 → processes-3.1.1}/.github/workflows/docs.yml +0 -0
  18. {processes-3.1.0 → processes-3.1.1}/.github/workflows/lint-pr.yml +0 -0
  19. {processes-3.1.0 → processes-3.1.1}/.github/workflows/lint.yml +0 -0
  20. {processes-3.1.0 → processes-3.1.1}/.github/workflows/mypy.yml +0 -0
  21. {processes-3.1.0 → processes-3.1.1}/.github/workflows/publish.yml +0 -0
  22. {processes-3.1.0 → processes-3.1.1}/.github/workflows/tags.yml +0 -0
  23. {processes-3.1.0 → processes-3.1.1}/.github/workflows/tests.yml +0 -0
  24. {processes-3.1.0 → processes-3.1.1}/.gitignore +0 -0
  25. {processes-3.1.0 → processes-3.1.1}/CONTRIBUTING.md +0 -0
  26. {processes-3.1.0 → processes-3.1.1}/LICENSE +0 -0
  27. {processes-3.1.0 → processes-3.1.1}/assets/banner.svg +0 -0
  28. {processes-3.1.0 → processes-3.1.1}/assets/logo.png +0 -0
  29. {processes-3.1.0 → processes-3.1.1}/assets/logo.svg +0 -0
  30. {processes-3.1.0 → processes-3.1.1}/assets/social_banner.png +0 -0
  31. {processes-3.1.0 → processes-3.1.1}/assets/social_banner.svg +0 -0
  32. {processes-3.1.0 → processes-3.1.1}/docs/assets/favicon.svg +0 -0
  33. {processes-3.1.0 → processes-3.1.1}/docs/examples/advanced.md +0 -0
  34. {processes-3.1.0 → processes-3.1.1}/docs/examples/basic.md +0 -0
  35. {processes-3.1.0 → processes-3.1.1}/docs/examples/intro.md +0 -0
  36. {processes-3.1.0 → processes-3.1.1}/docs/reference.md +0 -0
  37. {processes-3.1.0 → processes-3.1.1}/examples/01_basic_tasks_and_dependencies/README.md +0 -0
  38. {processes-3.1.0 → processes-3.1.1}/examples/01_basic_tasks_and_dependencies/example1.py +0 -0
  39. {processes-3.1.0 → processes-3.1.1}/examples/02_task_dependencies_result_passing/README.md +0 -0
  40. {processes-3.1.0 → processes-3.1.1}/examples/02_task_dependencies_result_passing/example2.py +0 -0
  41. {processes-3.1.0 → processes-3.1.1}/examples/README.md +0 -0
  42. {processes-3.1.0 → processes-3.1.1}/mkdocs.yml +0 -0
  43. {processes-3.1.0 → processes-3.1.1}/pyproject.toml +0 -0
  44. {processes-3.1.0 → processes-3.1.1}/pytest.ini +0 -0
  45. {processes-3.1.0 → processes-3.1.1}/src/processes/__init__.py +0 -0
  46. {processes-3.1.0 → processes-3.1.1}/src/processes/exceptions.py +0 -0
  47. {processes-3.1.0 → processes-3.1.1}/src/processes/py.typed +0 -0
  48. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/languages/de.json +0 -0
  49. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/languages/en.json +0 -0
  50. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/languages/es.json +0 -0
  51. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/languages/fr.json +0 -0
  52. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/languages/it.json +0 -0
  53. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/languages/pt.json +0 -0
  54. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/palettes/catppuccin.css +0 -0
  55. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/palettes/neobones.css +0 -0
  56. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/palettes/neutral.css +0 -0
  57. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/palettes/slate.css +0 -0
  58. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/styles/classic.html +0 -0
  59. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/styles/compact.html +0 -0
  60. {processes-3.1.0 → processes-3.1.1}/src/processes/themes/styles/modern.html +0 -0
  61. {processes-3.1.0 → processes-3.1.1}/tests/__init__.py +0 -0
  62. {processes-3.1.0 → processes-3.1.1}/tests/base_test.py +0 -0
  63. {processes-3.1.0 → processes-3.1.1}/tests/manual_tests/__init__.py +0 -0
  64. {processes-3.1.0 → processes-3.1.1}/tests/manual_tests/manual_pipeline_inspect.py +0 -0
  65. {processes-3.1.0 → processes-3.1.1}/tests/manual_tests/manual_themed_tracebacks.py +0 -0
  66. {processes-3.1.0 → processes-3.1.1}/tests/test_args_kwargs.py +0 -0
  67. {processes-3.1.0 → processes-3.1.1}/tests/test_complex_dag_failures.py +0 -0
  68. {processes-3.1.0 → processes-3.1.1}/tests/test_dependencies.py +0 -0
  69. {processes-3.1.0 → processes-3.1.1}/tests/test_email_themes.py +0 -0
  70. {processes-3.1.0 → processes-3.1.1}/tests/test_logfiles.py +0 -0
  71. {processes-3.1.0 → processes-3.1.1}/tests/test_normal_run_no_errors.py +0 -0
  72. {processes-3.1.0 → processes-3.1.1}/tests/test_parallel_race_conditions.py +0 -0
  73. {processes-3.1.0 → processes-3.1.1}/tests/test_timeout_retry.py +0 -0
  74. {processes-3.1.0 → processes-3.1.1}/tests/test_unique_name.py +0 -0
  75. {processes-3.1.0 → processes-3.1.1}/uv.lock +0 -0
@@ -1,3 +1,9 @@
1
+ ## v3.1.1 (2026-06-13)
2
+
3
+ ### Refactor
4
+
5
+ - rename _log_formatting to _logfile_formatting and _TaskLogFormatter to _TaskLogfileFormatter
6
+
1
7
  ## v3.1.0 (2026-06-13)
2
8
 
3
9
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processes
3
- Version: 3.1.0
3
+ Version: 3.1.1
4
4
  Summary: Orchestrate graphs of callables in Python with automatic dependency resolution, parallel execution, retries, timeouts, and HTML email alerts on failure — zero dependencies
5
5
  Project-URL: Homepage, https://github.com/oliverm91/processes
6
6
  Project-URL: Documentation, https://oliverm91.github.io/processes/
@@ -336,6 +336,20 @@ HTMLEmailStyle(
336
336
 
337
337
  All fields are optional — omit `HTMLEmailStyle` entirely to use the defaults.
338
338
 
339
+ #### Traced Variables
340
+
341
+ On failure, the email body includes the local variables of the **outermost
342
+ user frame in the traceback** — i.e. the last frame that is not inside
343
+ `site-packages` or your virtualenv. A `file:line` reference next to the
344
+ section shows exactly where those values were captured.
345
+
346
+ `traced_vars_frame_filter` lets you point this at a different frame: set it
347
+ to a path substring (e.g. one of your own package or module names) to
348
+ capture locals from the outermost frame whose filename contains that
349
+ substring instead. This is useful for deep-debugging code that runs through
350
+ several layers of internal libraries or wrappers, where the default
351
+ outermost-user-frame would land too high up the call stack.
352
+
339
353
  </details>
340
354
 
341
355
  <details>
@@ -311,6 +311,20 @@ HTMLEmailStyle(
311
311
 
312
312
  All fields are optional — omit `HTMLEmailStyle` entirely to use the defaults.
313
313
 
314
+ #### Traced Variables
315
+
316
+ On failure, the email body includes the local variables of the **outermost
317
+ user frame in the traceback** — i.e. the last frame that is not inside
318
+ `site-packages` or your virtualenv. A `file:line` reference next to the
319
+ section shows exactly where those values were captured.
320
+
321
+ `traced_vars_frame_filter` lets you point this at a different frame: set it
322
+ to a path substring (e.g. one of your own package or module names) to
323
+ capture locals from the outermost frame whose filename contains that
324
+ substring instead. This is useful for deep-debugging code that runs through
325
+ several layers of internal libraries or wrappers, where the default
326
+ outermost-user-frame would land too high up the call stack.
327
+
314
328
  </details>
315
329
 
316
330
  <details>
@@ -184,6 +184,13 @@ in the source the listed values were captured, which is usually the
184
184
  fastest way to figure out *why* a complex task broke deep inside a
185
185
  wrapper.
186
186
 
187
+ This default can be overridden with `HTMLEmailStyle.traced_vars_frame_filter`.
188
+ Set it to a path substring (e.g. the name of one of your own packages or
189
+ modules) to capture locals from the outermost frame whose filename contains
190
+ that substring instead — useful for deep-debugging code that runs through
191
+ several layers of internal libraries or wrappers, where the default
192
+ outermost-user-frame would land too high up the call stack.
193
+
187
194
  ---
188
195
 
189
196
  ## 🛡️ Fault Tolerance & Logs
@@ -22,7 +22,18 @@ _PALETTE_MARKER = "{{__palette_css__}}"
22
22
 
23
23
 
24
24
  def _load_language_strings(language: str) -> dict[str, str]:
25
- """Load translatable strings for the given ISO 639-1 language code."""
25
+ """Load translatable strings for the given ISO 639-1 language code.
26
+
27
+ Parameters
28
+ ----------
29
+ language : str
30
+ ISO 639-1 language code, e.g. ``"en"``.
31
+
32
+ Returns
33
+ -------
34
+ dict[str, str]
35
+ Mapping of translation keys to localized strings.
36
+ """
26
37
  path = os.path.join(_LANGUAGES_DIR, f"{language}.json")
27
38
  with open(path, encoding="utf-8") as fh:
28
39
  return cast(dict[str, str], json.load(fh))
@@ -57,9 +68,20 @@ class _HTMLEmailFormatter(_ErrorContextFormatter):
57
68
  def _split_traceback_at_target(self, tb_str: str, location: str) -> tuple[str, str, str]:
58
69
  """Split *tb_str* around the frame line matching *location*.
59
70
 
60
- Returns ``(before, highlight, after)`` where ``highlight`` is the
61
- ``File "<filename>", line <lineno>, in <func>`` line for that frame.
62
- Returns ``("", "", tb_str)`` if no match is found.
71
+ Parameters
72
+ ----------
73
+ tb_str : str
74
+ Full formatted traceback to split.
75
+ location : str
76
+ ``"filename:lineno"`` of the frame to highlight, as produced by
77
+ ``_build_traced_vars_location``.
78
+
79
+ Returns
80
+ -------
81
+ tuple[str, str, str]
82
+ ``(before, highlight, after)`` where ``highlight`` is the
83
+ ``File "<filename>", line <lineno>, in <func>`` line for that
84
+ frame. Returns ``("", "", tb_str)`` if no match is found.
63
85
  """
64
86
  if not tb_str or not location:
65
87
  return ("", "", tb_str)
@@ -83,7 +105,18 @@ class _HTMLEmailFormatter(_ErrorContextFormatter):
83
105
  return rendered
84
106
 
85
107
  def format(self, record: logging.LogRecord) -> str:
86
- """Render a log record as a complete HTML email body."""
108
+ """Render a log record as a complete HTML email body.
109
+
110
+ Parameters
111
+ ----------
112
+ record : logging.LogRecord
113
+ The record being formatted.
114
+
115
+ Returns
116
+ -------
117
+ str
118
+ The fully rendered HTML email body.
119
+ """
87
120
  error = self._error_data(record)
88
121
 
89
122
  tb_before, tb_highlight, tb_after = self._split_traceback_at_target(
@@ -159,7 +192,23 @@ def _build_task_email_handler(
159
192
  style: HTMLEmailStyle,
160
193
  task_name: str,
161
194
  ) -> _HTMLEmailHandler:
162
- """Create a fully configured email handler bound to one task."""
195
+ """Create a fully configured email handler bound to one task.
196
+
197
+ Parameters
198
+ ----------
199
+ smtp_config : SMTPConfig
200
+ SMTP transport configuration for the handler.
201
+ style : HTMLEmailStyle
202
+ HTML presentation settings used by the handler's formatter.
203
+ task_name : str
204
+ Name of the task the handler is bound to, used in the email subject.
205
+
206
+ Returns
207
+ -------
208
+ _HTMLEmailHandler
209
+ A handler at ``logging.ERROR`` level, with its formatter and
210
+ localized subject configured.
211
+ """
163
212
  handler = _HTMLEmailHandler(smtp_config)
164
213
  handler.setFormatter(_HTMLEmailFormatter(style))
165
214
  handler.setLevel(logging.ERROR)
@@ -7,7 +7,29 @@ from typing import Any
7
7
 
8
8
  @dataclass(frozen=True)
9
9
  class _ErrorData:
10
- """Typed view of a task failure, extracted from ``record.task_context``."""
10
+ """Typed view of a task failure, extracted from ``record.task_context``.
11
+
12
+ Attributes
13
+ ----------
14
+ task_name : str
15
+ Name of the task that failed. Defaults to ``"?"``.
16
+ function : str
17
+ Name of the function that was executing. Defaults to ``"?"``.
18
+ args : tuple[Any, ...]
19
+ Positional arguments the function was called with. Defaults to ``()``.
20
+ kwargs : dict[str, Any]
21
+ Keyword arguments the function was called with. Defaults to ``{}``.
22
+ downstream_impact : list[str]
23
+ Names of tasks skipped as a result of this failure. Defaults to ``[]``.
24
+ exception : str
25
+ String representation of the raised exception. Defaults to ``""``.
26
+ traceback_str : str
27
+ Full formatted traceback. Defaults to ``""``.
28
+ traced_vars : str
29
+ Rendered local variables of the traced frame. Defaults to ``""``.
30
+ traced_vars_location : str
31
+ ``"filename:lineno"`` of the traced frame. Defaults to ``""``.
32
+ """
11
33
 
12
34
  task_name: str = "?"
13
35
  function: str = "?"
@@ -24,6 +46,20 @@ class _ErrorContextFormatter(logging.Formatter):
24
46
  """Base formatter providing typed access to a record's failure context."""
25
47
 
26
48
  def _error_data(self, record: logging.LogRecord) -> _ErrorData:
49
+ """Extract the failure context from a log record.
50
+
51
+ Parameters
52
+ ----------
53
+ record : logging.LogRecord
54
+ The record being formatted. May or may not carry a
55
+ ``task_context`` attribute.
56
+
57
+ Returns
58
+ -------
59
+ _ErrorData
60
+ Typed view of ``record.task_context``, with defaults filled in
61
+ for any missing fields.
62
+ """
27
63
  ctx = getattr(record, "task_context", None) or {}
28
64
  return _ErrorData(
29
65
  task_name=str(ctx.get("task_name", "?")),
@@ -7,7 +7,7 @@ from ._error_data import _ErrorContextFormatter
7
7
  _LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
8
8
 
9
9
 
10
- class _TaskLogFormatter(_ErrorContextFormatter):
10
+ class _TaskLogfileFormatter(_ErrorContextFormatter):
11
11
  """Plain-text formatter for task logfiles.
12
12
 
13
13
  On failure, appends the same failure context shown in the HTML email
@@ -19,6 +19,19 @@ class _TaskLogFormatter(_ErrorContextFormatter):
19
19
  super().__init__(_LOG_FORMAT)
20
20
 
21
21
  def format(self, record: logging.LogRecord) -> str:
22
+ """Render a log record as plain text, appending failure context if present.
23
+
24
+ Parameters
25
+ ----------
26
+ record : logging.LogRecord
27
+ The record being formatted.
28
+
29
+ Returns
30
+ -------
31
+ str
32
+ The formatted log line, with the failure context appended if
33
+ ``record.task_context`` is set.
34
+ """
22
35
  if not getattr(record, "task_context", None):
23
36
  return super().format(record)
24
37
 
@@ -8,7 +8,19 @@ from typing import Any
8
8
 
9
9
 
10
10
  def _is_library_path(filename: str) -> bool:
11
- """Return True if ``filename`` lives inside a library or stdlib."""
11
+ """Check whether a source file belongs to a library or the stdlib.
12
+
13
+ Parameters
14
+ ----------
15
+ filename : str
16
+ Path of a traceback frame's source file.
17
+
18
+ Returns
19
+ -------
20
+ bool
21
+ True if ``filename`` lives inside ``site-packages``, a ``.venv``,
22
+ or the Python install prefix, False otherwise.
23
+ """
12
24
  if "site-packages" in filename:
13
25
  return True
14
26
  if ".venv" in filename:
@@ -26,6 +38,18 @@ def _is_library_path(filename: str) -> bool:
26
38
 
27
39
 
28
40
  def _iter_tb(exc_tb: Any) -> Any:
41
+ """Iterate over a traceback's frames from outermost to innermost.
42
+
43
+ Parameters
44
+ ----------
45
+ exc_tb : types.TracebackType | None
46
+ The traceback to walk, typically ``exc.__traceback__``.
47
+
48
+ Returns
49
+ -------
50
+ Iterator[types.TracebackType]
51
+ Each traceback node from ``exc_tb`` to the innermost frame.
52
+ """
29
53
  tb = exc_tb
30
54
  while tb is not None:
31
55
  yield tb
@@ -38,6 +62,19 @@ def _resolve_target_tb(exc_tb: Any, frame_filter: str | None) -> Any:
38
62
  With ``frame_filter=None``, picks the last non-library frame.
39
63
  With a filter string, picks the last frame whose filename contains it.
40
64
  Falls back to the innermost frame when nothing matches.
65
+
66
+ Parameters
67
+ ----------
68
+ exc_tb : types.TracebackType | None
69
+ The traceback to search, typically ``exc.__traceback__``.
70
+ frame_filter : str | None
71
+ Substring used to select a frame by filename, or ``None`` to pick
72
+ the outermost non-library frame.
73
+
74
+ Returns
75
+ -------
76
+ types.TracebackType | None
77
+ The selected traceback frame, or ``None`` if ``exc_tb`` is ``None``.
41
78
  """
42
79
  frames = list(_iter_tb(exc_tb))
43
80
  if not frames:
@@ -50,7 +87,22 @@ def _resolve_target_tb(exc_tb: Any, frame_filter: str | None) -> Any:
50
87
 
51
88
 
52
89
  def _build_traced_vars_html(exc_tb: Any, frame_filter: str | None) -> str:
53
- """Return HTML-escaped ``name = repr(value)`` lines for the target frame's locals."""
90
+ """Return HTML-escaped ``name = repr(value)`` lines for the target frame's locals.
91
+
92
+ Parameters
93
+ ----------
94
+ exc_tb : types.TracebackType | None
95
+ The traceback to search, typically ``exc.__traceback__``.
96
+ frame_filter : str | None
97
+ Substring used to select a frame by filename, or ``None`` to pick
98
+ the outermost non-library frame.
99
+
100
+ Returns
101
+ -------
102
+ str
103
+ Newline-separated, HTML-escaped ``name = repr(value)`` lines for the
104
+ target frame's locals, or ``""`` if no frame is found.
105
+ """
54
106
  target = _resolve_target_tb(exc_tb, frame_filter)
55
107
  if target is None:
56
108
  return ""
@@ -66,7 +118,21 @@ def _build_traced_vars_html(exc_tb: Any, frame_filter: str | None) -> str:
66
118
 
67
119
 
68
120
  def _build_traced_vars_location(exc_tb: Any, frame_filter: str | None) -> str:
69
- """Return ``"filename:lineno"`` for the target frame, or empty string."""
121
+ """Return ``"filename:lineno"`` for the target frame, or empty string.
122
+
123
+ Parameters
124
+ ----------
125
+ exc_tb : types.TracebackType | None
126
+ The traceback to search, typically ``exc.__traceback__``.
127
+ frame_filter : str | None
128
+ Substring used to select a frame by filename, or ``None`` to pick
129
+ the outermost non-library frame.
130
+
131
+ Returns
132
+ -------
133
+ str
134
+ ``"filename:lineno"`` of the target frame, or ``""`` if no frame is found.
135
+ """
70
136
  target = _resolve_target_tb(exc_tb, frame_filter)
71
137
  if target is None:
72
138
  return ""
@@ -75,5 +141,17 @@ def _build_traced_vars_location(exc_tb: Any, frame_filter: str | None) -> str:
75
141
 
76
142
 
77
143
  def _format_traceback(exc: BaseException) -> str:
78
- """Return the full traceback of *exc* as a single string."""
144
+ """Return the full traceback of *exc* as a single string.
145
+
146
+ Parameters
147
+ ----------
148
+ exc : BaseException
149
+ The exception whose traceback should be formatted.
150
+
151
+ Returns
152
+ -------
153
+ str
154
+ The full traceback, as produced by ``traceback.format_exception``,
155
+ joined into a single string.
156
+ """
79
157
  return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
@@ -26,9 +26,11 @@ class SMTPConfig:
26
26
  Recipient email addresses.
27
27
  credentials : tuple[str, str] | None
28
28
  ``(username, password)`` for SMTP authentication. Defaults to ``None``.
29
- secure : tuple | None
30
- Security configuration. Use ``()`` for STARTTLS, ``(certfile, keyfile)``
31
- for explicit TLS, or ``None`` for no encryption. Defaults to ``None``.
29
+ secure : tuple[()] | tuple[str] | tuple[str, str] | tuple[str, str, ssl.SSLContext] | None
30
+ Security configuration. Use ``()`` for STARTTLS, ``(keyfile,)`` or
31
+ ``(keyfile, certfile)`` for explicit TLS context creation, or
32
+ ``(keyfile, certfile, ssl_context)`` to supply a pre-built
33
+ ``ssl.SSLContext``. ``None`` means no encryption. Defaults to ``None``.
32
34
  timeout : int
33
35
  Connection timeout in seconds. Defaults to ``5``.
34
36
  """
@@ -64,6 +66,14 @@ class HTMLEmailStyle:
64
66
  appear in the *Traced Variables* section. When ``None`` (default),
65
67
  the outermost user frame is used; when set, the outermost frame whose
66
68
  filename contains this substring is used instead.
69
+
70
+ Raises
71
+ ------
72
+ ValueError
73
+ If ``style``, ``palette``, or ``language`` is not one of the
74
+ supported values.
75
+ TypeError
76
+ If ``traced_vars_frame_filter`` is neither ``str`` nor ``None``.
67
77
  """
68
78
 
69
79
  style: str = _DEFAULT_STYLE
@@ -298,6 +298,8 @@ class ProcessRunner:
298
298
  Results from successfully executed tasks.
299
299
  failed_tasks : set[str]
300
300
  Names of tasks that failed during execution.
301
+ skipped_tasks : set[str]
302
+ Names of tasks that were never run because an upstream dependency failed.
301
303
  submitted_tasks : set[str]
302
304
  Names of tasks that have been submitted for execution.
303
305
  """
@@ -310,7 +312,14 @@ class ProcessRunner:
310
312
  self.submitted_tasks: set[str] = set()
311
313
 
312
314
  def _is_done(self) -> bool:
313
- """Whether every task has either passed or failed."""
315
+ """Check whether every task has either passed or failed.
316
+
317
+ Returns
318
+ -------
319
+ bool
320
+ True if every task has a recorded result (passed or failed),
321
+ False otherwise.
322
+ """
314
323
  return len(self.passed_results) + len(self.failed_tasks) >= len(self.process.tasks)
315
324
 
316
325
  def run(self, parallel: bool, max_workers: int) -> ProcessResult:
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
10
10
  import logging
11
11
 
12
12
  from ._email_internals import _build_task_email_handler
13
- from ._log_formatting import _TaskLogFormatter
13
+ from ._logfile_formatting import _TaskLogfileFormatter
14
14
  from ._tb_utils import _build_traced_vars_html, _build_traced_vars_location, _format_traceback
15
15
  from .email_config import HTMLEmailStyle, SMTPConfig
16
16
  from .exceptions import CircularDependencyError
@@ -251,7 +251,7 @@ class Task:
251
251
 
252
252
  file_handler = logging.FileHandler(self.log_path)
253
253
  file_handler.setLevel(logging.INFO)
254
- file_handler.setFormatter(_TaskLogFormatter())
254
+ file_handler.setFormatter(_TaskLogfileFormatter())
255
255
  logger.addHandler(file_handler)
256
256
 
257
257
  if smtp_config is not None:
@@ -271,6 +271,13 @@ class Task:
271
271
  """
272
272
  Validates all input parameter types.
273
273
 
274
+ Parameters
275
+ ----------
276
+ smtp_config : SMTPConfig | None
277
+ SMTP transport configuration passed to the constructor, if any.
278
+ email_style : HTMLEmailStyle | None
279
+ HTML presentation settings passed to the constructor, if any.
280
+
274
281
  Raises
275
282
  ------
276
283
  TypeError
@@ -1,36 +0,0 @@
1
- # v3.0.1 — Email API Overhaul, Task Retries & Timeouts
2
-
3
- ## ⚠️ Breaking Changes
4
-
5
- - **Removed `HTMLSMTPHandler` and `html_mail_handler`**. Email alerting is now configured with two plain dataclasses passed to `Task`:
6
- - `SMTPConfig` — SMTP transport settings (host, credentials, sender/recipients, TLS).
7
- - `HTMLEmailStyle` — presentation settings (style, color palette, language, traced-vars frame filter).
8
-
9
- ```python
10
- Task(..., smtp_config=SMTPConfig(...), email_style=HTMLEmailStyle(style="modern", palette="catppuccin"))
11
- ```
12
-
13
- ## ✨ New Features
14
-
15
- - **Per-task timeouts**: `Task(..., timeout=30)` raises `TimeoutError` if an attempt exceeds the limit.
16
- - **Automatic retries**: `Task(..., retries=3, retry_on=(ConnectionError, TimeoutError))` retries failed attempts on the specified exception types (defaults to `ConnectionError`/`TimeoutError`).
17
- - **Structured exceptions**: `DependencyNotFoundError`, `TaskNotFoundError`, and `CircularDependencyError` now carry structured attributes (`task_name`, `missing_dep`) instead of plain messages.
18
- - **Traced variables in error emails**: failure emails include a "Traced Variables" section showing local variables at the relevant stack frame, with a configurable `traced_vars_frame_filter` to target a specific module.
19
-
20
- ## 🔧 Refactoring
21
-
22
- - Centralized failure-context extraction: `Task._resolve_args` and `Task._build_failure_context` cleanly separate dependency-result injection from failure reporting.
23
- - Split traceback/frame-walking utilities (`_tb_utils.py`) from email-rendering internals (`_email_internals.py`) — each module now has a single responsibility.
24
- - Each `Task` now gets a unique per-instance logger, preventing handler leakage between tasks with the same name.
25
- - Minor cleanups in `Process`: bare `raise` for re-raised exceptions, extracted `_is_done()` helper for the parallel runner loop.
26
-
27
- ## 🐛 Bug Fixes
28
-
29
- - Fixed a logging bug where `logger.exception(...)` was called outside an `except` block, resulting in empty exception details (`None`) in failure emails. Errors now log with full `exc_info` and a structured `task_context` payload.
30
- - Fixed `Task.run()` not reporting the actual exception when retries were exhausted.
31
-
32
- ## 🧪 Testing & CI
33
-
34
- - Migrated the test suite to a shared `BaseTest` base class with consistent per-test log directories and cleanup.
35
- - Added dedicated tests for timeout and retry behavior.
36
- - Bumped GitHub Actions (`actions/checkout` → v6, `astral-sh/setup-uv` → v6) to silence deprecation warnings.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes