mpengine 0.3.0__tar.gz → 0.3.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.
- {mpengine-0.3.0/src/mpengine.egg-info → mpengine-0.3.1}/PKG-INFO +1 -1
- {mpengine-0.3.0 → mpengine-0.3.1}/pyproject.toml +1 -1
- {mpengine-0.3.0 → mpengine-0.3.1}/src/mpengine/__init__.py +1 -1
- {mpengine-0.3.0 → mpengine-0.3.1}/src/mpengine/banner.py +2 -9
- {mpengine-0.3.0 → mpengine-0.3.1}/src/mpengine/engine.py +21 -3
- {mpengine-0.3.0 → mpengine-0.3.1}/src/mpengine/orchestrator.py +162 -39
- {mpengine-0.3.0 → mpengine-0.3.1}/src/mpengine/spider.txt +2 -2
- {mpengine-0.3.0 → mpengine-0.3.1}/src/mpengine/worker_names.json +1 -1
- {mpengine-0.3.0 → mpengine-0.3.1/src/mpengine.egg-info}/PKG-INFO +1 -1
- {mpengine-0.3.0 → mpengine-0.3.1}/LICENSE +0 -0
- {mpengine-0.3.0 → mpengine-0.3.1}/README.md +0 -0
- {mpengine-0.3.0 → mpengine-0.3.1}/setup.cfg +0 -0
- {mpengine-0.3.0 → mpengine-0.3.1}/src/mpengine/partition.py +0 -0
- {mpengine-0.3.0 → mpengine-0.3.1}/src/mpengine.egg-info/SOURCES.txt +0 -0
- {mpengine-0.3.0 → mpengine-0.3.1}/src/mpengine.egg-info/dependency_links.txt +0 -0
- {mpengine-0.3.0 → mpengine-0.3.1}/src/mpengine.egg-info/requires.txt +0 -0
- {mpengine-0.3.0 → mpengine-0.3.1}/src/mpengine.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mpengine
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A small, general-purpose multiprocessing engine: dispatch any callable across processes with run manifests, per-worker logs, on-disk outputs and failure isolation
|
|
5
5
|
Author: Amandeep Singh
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mpengine"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "A small, general-purpose multiprocessing engine: dispatch any callable across processes with run manifests, per-worker logs, on-disk outputs and failure isolation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
`orchestrator.py` imports exactly one name from here and calls it once. All the
|
|
4
|
-
ASCII art, formatting and printing lives in this module, so the engine and the
|
|
5
|
-
orchestration layer stay free of decoration.
|
|
6
|
-
|
|
7
|
-
The spider is the mascot: a multiprocessing engine, and a creature famous for
|
|
8
|
-
having eight legs. The art itself lives in `spider.txt` rather than as a string
|
|
9
|
-
literal here - it is data, not code, the same call made for `worker_names.json`.
|
|
1
|
+
"""
|
|
2
|
+
The run "spider" banner - purely cosmetic, deliberately kept out of everything else.
|
|
10
3
|
"""
|
|
11
4
|
|
|
12
5
|
from __future__ import annotations
|
|
@@ -25,6 +25,15 @@ from typing import Any, Callable
|
|
|
25
25
|
import cloudpickle
|
|
26
26
|
|
|
27
27
|
_log = logging.getLogger("mpengine.engine")
|
|
28
|
+
# Lifecycle logging (dispatch start/done, worker-count clamping, progress
|
|
29
|
+
# milestones) is always captured in full by orchestrator.run()'s run.log, but
|
|
30
|
+
# is deliberately never allowed to bubble up past this logger to whatever the
|
|
31
|
+
# CALLER's own logging setup is (root, via logging.basicConfig or similar) -
|
|
32
|
+
# a caller configuring logging for their own unrelated purposes should not
|
|
33
|
+
# suddenly also see mpengine's internal chatter. Only "mpengine.summary" (the
|
|
34
|
+
# stored-here paths and the worker ranking, see orchestrator.py) is meant to
|
|
35
|
+
# surface there.
|
|
36
|
+
_log.propagate = False
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
def expand_call(kargs: dict[str, Any]) -> Any:
|
|
@@ -108,6 +117,7 @@ def process_jobs(
|
|
|
108
117
|
on_progress: Callable[[int, float, Any], None] | None = None,
|
|
109
118
|
on_job_error: Callable[[int, BaseException], None] | None = None,
|
|
110
119
|
text_progress: bool | None = None,
|
|
120
|
+
milestones: bool = True,
|
|
111
121
|
) -> list[Any]:
|
|
112
122
|
"""Ch.20 Snippet 20.9 (`processJobs`) - parallel dispatch over a process pool.
|
|
113
123
|
|
|
@@ -153,7 +163,12 @@ def process_jobs(
|
|
|
153
163
|
is running AND stderr is a real terminal. That distinction matters in
|
|
154
164
|
production - redirected to a log file, a `\\r` line per job is unreadable
|
|
155
165
|
noise, so when the text display is off this logs periodic completion
|
|
156
|
-
milestones at INFO instead
|
|
166
|
+
milestones at INFO instead - unless `milestones=False`, which a caller
|
|
167
|
+
passes when `on_progress` is itself a live visual display (a tqdm bar,
|
|
168
|
+
say) rather than merely a logging hook: the milestones would otherwise be
|
|
169
|
+
redundant with - and print right alongside - that display. `on_progress`
|
|
170
|
+
alone isn't the right signal for this, since a caller can legitimately
|
|
171
|
+
want per-job callbacks (for logging, say) with no visual display at all.
|
|
157
172
|
|
|
158
173
|
`on_job_error`, if given, is called as `on_job_error(index, exc)` for any
|
|
159
174
|
job that cannot be *serialized* for submission, and that job is skipped
|
|
@@ -210,8 +225,11 @@ def process_jobs(
|
|
|
210
225
|
if text_progress is None:
|
|
211
226
|
text_progress = on_progress is None and sys.stderr.isatty()
|
|
212
227
|
# When nothing is drawing live, log milestones so a redirected run still
|
|
213
|
-
# shows movement - roughly ten lines, whatever the job count.
|
|
214
|
-
|
|
228
|
+
# shows movement - roughly ten lines, whatever the job count. `milestones`
|
|
229
|
+
# is the caller's explicit say on this, separate from text_progress/
|
|
230
|
+
# on_progress: it's the only signal that actually means "something else
|
|
231
|
+
# is already showing a visual progress display, don't duplicate it."
|
|
232
|
+
milestone = max(1, n_submitted // 10) if (not text_progress and milestones) else 0
|
|
215
233
|
|
|
216
234
|
_log.info(
|
|
217
235
|
"dispatch start task=%s n_jobs=%d n_workers=%d text_progress=%s",
|
|
@@ -36,6 +36,7 @@ from pathlib import Path
|
|
|
36
36
|
from typing import Any, Callable, Iterator
|
|
37
37
|
|
|
38
38
|
from tqdm import tqdm
|
|
39
|
+
from tqdm.contrib.logging import logging_redirect_tqdm
|
|
39
40
|
|
|
40
41
|
from mpengine.banner import print_banner
|
|
41
42
|
from mpengine.engine import expand_call, process_jobs, process_jobs_
|
|
@@ -48,6 +49,66 @@ N_WORKERS = os.cpu_count() or 4
|
|
|
48
49
|
# who want to see this call logging.basicConfig(level=logging.INFO). The
|
|
49
50
|
# spider banner is the one deliberate exception and still prints.
|
|
50
51
|
_log = logging.getLogger("mpengine.orchestrator")
|
|
52
|
+
# See the matching note on "mpengine.engine" in engine.py: lifecycle logging
|
|
53
|
+
# is always captured in run.log but never bubbles up to the caller's own
|
|
54
|
+
# logging setup, so configuring logging for unrelated reasons doesn't also
|
|
55
|
+
# surface mpengine's internal chatter.
|
|
56
|
+
_log.propagate = False
|
|
57
|
+
|
|
58
|
+
# A separate logger, and deliberately NOT a child of "mpengine.orchestrator"
|
|
59
|
+
# (it sits directly under "mpengine" instead) - for the "what happened, where
|
|
60
|
+
# did it land" summary a person watching a terminal actually wants: the
|
|
61
|
+
# stored-here paths and the worker ranking. Kept apart from `_log` for two
|
|
62
|
+
# reasons: `_log` also carries dispatch-lifecycle chatter that would show up
|
|
63
|
+
# too if a handler were attached to it directly, and `_log.propagate = False`
|
|
64
|
+
# above would otherwise also cut this logger off from the caller's own
|
|
65
|
+
# logging setup, were it a child of `_log`. These are real `logging` calls
|
|
66
|
+
# throughout, never a print() dressed up to look like one -
|
|
67
|
+
# `_terminal_summary_handler` below is what makes them visible on an
|
|
68
|
+
# interactive terminal without the caller configuring anything, and normal
|
|
69
|
+
# propagation is what makes them visible via the caller's own setup too.
|
|
70
|
+
_summary_log = logging.getLogger("mpengine.summary")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _has_external_handler() -> bool:
|
|
74
|
+
"""Whether the caller has configured logging somewhere that would already
|
|
75
|
+
show these records - `logging.basicConfig()` (attaches to root) or a
|
|
76
|
+
handler attached directly to `mpengine`. If so, leave it alone entirely
|
|
77
|
+
rather than risk showing anything twice or fighting the caller's own
|
|
78
|
+
formatting."""
|
|
79
|
+
if logging.getLogger().handlers:
|
|
80
|
+
return True
|
|
81
|
+
return any(
|
|
82
|
+
not isinstance(h, logging.NullHandler)
|
|
83
|
+
for h in logging.getLogger("mpengine").handlers
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@contextmanager
|
|
88
|
+
def _terminal_summary_handler(interactive: bool) -> Iterator[None]:
|
|
89
|
+
"""Make `_summary_log` visible on an interactive terminal with zero
|
|
90
|
+
configuration, for the duration of one run.
|
|
91
|
+
|
|
92
|
+
Only ever active when `interactive` is True AND the caller has not
|
|
93
|
+
configured logging themselves (`_has_external_handler`) - prod stays
|
|
94
|
+
exactly as silent as the caller's own logging setup dictates, and a
|
|
95
|
+
caller who has taken control of logging is never overridden. Attached and
|
|
96
|
+
removed per run, mirroring `_run_log_file`, so nothing leaks across calls.
|
|
97
|
+
"""
|
|
98
|
+
if not interactive or _has_external_handler():
|
|
99
|
+
yield
|
|
100
|
+
return
|
|
101
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
102
|
+
handler.setFormatter(logging.Formatter("%(levelname)-7s %(name)s: %(message)s"))
|
|
103
|
+
previous_level = _summary_log.level
|
|
104
|
+
_summary_log.setLevel(logging.INFO)
|
|
105
|
+
_summary_log.addHandler(handler)
|
|
106
|
+
try:
|
|
107
|
+
yield
|
|
108
|
+
finally:
|
|
109
|
+
_summary_log.removeHandler(handler)
|
|
110
|
+
handler.close()
|
|
111
|
+
_summary_log.setLevel(previous_level)
|
|
51
112
|
|
|
52
113
|
# one FileHandler-backed logger per worker *process*, lazily created on that
|
|
53
114
|
# process's first job and reused for every job after - keyed by pid, which is
|
|
@@ -136,37 +197,46 @@ def _run_log_file(log_path: Path) -> Iterator[None]:
|
|
|
136
197
|
so one run is one self-contained, durable record. That is the audit trail
|
|
137
198
|
this library is actually differentiated on.
|
|
138
199
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
200
|
+
Attached directly to THREE loggers, not just the shared "mpengine" parent:
|
|
201
|
+
"mpengine.orchestrator" and "mpengine.engine" both set `propagate = False`
|
|
202
|
+
(so their lifecycle chatter never reaches a caller's own logging setup -
|
|
203
|
+
see the note by each `_log` definition), which means a handler placed
|
|
204
|
+
only on "mpengine" would never see their records at all. Attaching
|
|
205
|
+
directly to each stops there being any gap; "mpengine.summary" (the
|
|
206
|
+
stored-here paths and ranking) still climbs normally, so attaching to
|
|
207
|
+
"mpengine" as well is what captures those. Level handling is deliberately
|
|
208
|
+
restrained: forcing DEBUG on any of these would also push DEBUG records
|
|
209
|
+
into a caller's own handlers wherever they DO propagate (a per-job line
|
|
210
|
+
for all 10,000 jobs appearing in someone's console who asked for INFO) -
|
|
211
|
+
instead each logger's level is only ever lowered as far as INFO, and only
|
|
212
|
+
when it was coarser than that, so the file always captures the full
|
|
213
|
+
lifecycle and every failure without ever making the caller's own output
|
|
214
|
+
noisier than they configured. A caller who genuinely wants per-job DEBUG
|
|
215
|
+
detail in the file just sets DEBUG themselves. Everything is restored on
|
|
216
|
+
the way out, including if the run raises.
|
|
153
217
|
"""
|
|
154
|
-
|
|
218
|
+
loggers = [
|
|
219
|
+
logging.getLogger("mpengine"),
|
|
220
|
+
logging.getLogger("mpengine.orchestrator"),
|
|
221
|
+
logging.getLogger("mpengine.engine"),
|
|
222
|
+
]
|
|
155
223
|
handler = logging.FileHandler(log_path / "run.log", encoding="utf-8")
|
|
156
224
|
handler.setLevel(logging.DEBUG)
|
|
157
225
|
handler.setFormatter(
|
|
158
226
|
logging.Formatter("%(asctime)s %(levelname)-7s %(name)s: %(message)s")
|
|
159
227
|
)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
228
|
+
previous_levels = [lg.level for lg in loggers]
|
|
229
|
+
for lg in loggers:
|
|
230
|
+
if not lg.isEnabledFor(logging.INFO):
|
|
231
|
+
lg.setLevel(logging.INFO)
|
|
232
|
+
lg.addHandler(handler)
|
|
164
233
|
try:
|
|
165
234
|
yield
|
|
166
235
|
finally:
|
|
167
|
-
|
|
236
|
+
for lg, level in zip(loggers, previous_levels):
|
|
237
|
+
lg.removeHandler(handler)
|
|
238
|
+
lg.setLevel(level)
|
|
168
239
|
handler.close()
|
|
169
|
-
parent.setLevel(previous_level)
|
|
170
240
|
|
|
171
241
|
|
|
172
242
|
def _validate_labels(labels: list[str]) -> None:
|
|
@@ -388,15 +458,18 @@ def _progress_renderer(
|
|
|
388
458
|
def _log_worker_ranking(stats: dict[int, WorkerStats]) -> None:
|
|
389
459
|
"""Rank workers fastest-to-slowest by seconds per atom.
|
|
390
460
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
461
|
+
A real `_summary_log.info(...)` call, nothing else - visibility on an
|
|
462
|
+
interactive terminal comes entirely from `_terminal_summary_handler`
|
|
463
|
+
attaching a real handler for the run's duration, not from a second print
|
|
464
|
+
alongside the log call. This can only ever produce output when `stats` is
|
|
465
|
+
non-empty, which only happens in the first place when the terminal is
|
|
466
|
+
real (see `run()` - `stats` is populated solely by the live-display
|
|
467
|
+
renderer).
|
|
468
|
+
|
|
469
|
+
Note the caveat logged alongside: with uneven atom sizes - the normal
|
|
470
|
+
case in quant work - a low s/atom can mean small atoms rather than a
|
|
471
|
+
genuinely faster worker, so the fastest/slowest tags are a hint, not a
|
|
472
|
+
measurement.
|
|
400
473
|
"""
|
|
401
474
|
if not stats:
|
|
402
475
|
return
|
|
@@ -412,7 +485,7 @@ def _log_worker_ranking(stats: dict[int, WorkerStats]) -> None:
|
|
|
412
485
|
f"avg {s.avg_atom_s:6.2f}s/atom {s.atoms_per_s:6.2f} atoms/s{tag}"
|
|
413
486
|
)
|
|
414
487
|
lines.append(" (uneven atom sizes: a low s/atom can mean small atoms, not a faster worker)")
|
|
415
|
-
|
|
488
|
+
_summary_log.info("\n".join(lines))
|
|
416
489
|
|
|
417
490
|
|
|
418
491
|
def run(
|
|
@@ -429,6 +502,7 @@ def run(
|
|
|
429
502
|
n_workers: int = N_WORKERS,
|
|
430
503
|
debug: bool = False,
|
|
431
504
|
show_progress: bool = False,
|
|
505
|
+
text_progress: bool | None = None,
|
|
432
506
|
) -> RunSummary:
|
|
433
507
|
"""Run `func(**params)` for every dict in `param_sets`, organized.
|
|
434
508
|
|
|
@@ -455,6 +529,12 @@ def run(
|
|
|
455
529
|
moment that worker finishes an atom. It also prints a fastest-to-slowest
|
|
456
530
|
ranking at the end, and fills `RunSummary.worker_stats` (keyed by pid) so
|
|
457
531
|
the same numbers are available programmatically.
|
|
532
|
+
|
|
533
|
+
`text_progress` overrides whether the book's `\r`-overwriting stderr line
|
|
534
|
+
runs (only relevant when `show_progress=False`; the default, `None`,
|
|
535
|
+
keeps today's behaviour - on when interactive, off otherwise). Force it
|
|
536
|
+
to `False` if you're watching per-job DEBUG/INFO logging instead: the two
|
|
537
|
+
would otherwise both write to the terminal and visually clash.
|
|
458
538
|
"""
|
|
459
539
|
if base_dir is not None:
|
|
460
540
|
base = Path(base_dir)
|
|
@@ -532,13 +612,28 @@ def run(
|
|
|
532
612
|
f.write(f" {label}: {params}\n")
|
|
533
613
|
f.write("\n")
|
|
534
614
|
|
|
615
|
+
# The terminal/prod split lives here: a real terminal gets the spider, the
|
|
616
|
+
# live progress display and the worker summary logs made visible with zero
|
|
617
|
+
# configuration, since those are things worth seeing live. Redirected -
|
|
618
|
+
# piped, under a supervisor, in CI - none of that shows; only `logging`
|
|
619
|
+
# output exists, for whatever the deployment itself configures to collect it.
|
|
620
|
+
interactive = sys.stderr.isatty()
|
|
621
|
+
|
|
535
622
|
# One durable record per run: everything from here down - dispatch,
|
|
536
623
|
# milestones, every job outcome, the summary - is captured to
|
|
537
624
|
# <log_dir>/run.log as well as going to the caller's own handlers.
|
|
538
|
-
|
|
625
|
+
# `_terminal_summary_handler` is what makes the stored-here paths and the
|
|
626
|
+
# worker ranking (both real `_summary_log` calls, see above) actually
|
|
627
|
+
# visible on an interactive terminal without any caller configuration.
|
|
628
|
+
# Order matters: _terminal_summary_handler must check for an externally
|
|
629
|
+
# configured handler BEFORE _run_log_file attaches its own FileHandler to
|
|
630
|
+
# "mpengine" - reversed, it would mistake our own run.log handler for a
|
|
631
|
+
# caller-configured one and skip attaching the terminal handler entirely.
|
|
632
|
+
with _terminal_summary_handler(interactive), _run_log_file(log_path):
|
|
539
633
|
worker_stats: dict[int, WorkerStats] = {}
|
|
540
634
|
|
|
541
|
-
|
|
635
|
+
if interactive:
|
|
636
|
+
print_banner(task, n_workers, debug)
|
|
542
637
|
_log.info(
|
|
543
638
|
"run start run_id=%s task=%s func=%s n_jobs=%d n_workers=%d "
|
|
544
639
|
"debug=%s show_progress=%s",
|
|
@@ -583,7 +678,7 @@ def run(
|
|
|
583
678
|
# The live display is a terminal affordance. Interactively it is the whole
|
|
584
679
|
# point; redirected to a log file tqdm's redraws are unreadable noise, so
|
|
585
680
|
# in prod we fall back to the engine's periodic INFO milestones instead.
|
|
586
|
-
interactive
|
|
681
|
+
# (`interactive` itself was already computed above, before the banner.)
|
|
587
682
|
live_display = show_progress and not debug and interactive
|
|
588
683
|
if show_progress and not debug and not interactive:
|
|
589
684
|
_log.info(
|
|
@@ -597,7 +692,15 @@ def run(
|
|
|
597
692
|
for r in raw_results:
|
|
598
693
|
log_job(os.getpid(), 0.0, r)
|
|
599
694
|
elif live_display:
|
|
600
|
-
|
|
695
|
+
# logging_redirect_tqdm is tqdm's own answer to exactly this
|
|
696
|
+
# clash: for its duration, any logging call that would otherwise
|
|
697
|
+
# write straight to the terminal (ours - log_job's per-job
|
|
698
|
+
# DEBUG/WARNING - or a caller's own configured handler) is instead
|
|
699
|
+
# routed through tqdm.write(), which clears the active bars,
|
|
700
|
+
# prints the line cleanly above them, and redraws them - instead
|
|
701
|
+
# of corrupting their redraw or spawning duplicate bar lines.
|
|
702
|
+
with _progress_renderer(len(jobs), task, worker_stats) as render, \
|
|
703
|
+
logging_redirect_tqdm():
|
|
601
704
|
def on_progress(pid: int, atom_s: float, result: Any) -> None:
|
|
602
705
|
log_job(pid, atom_s, result)
|
|
603
706
|
render(pid, atom_s, result)
|
|
@@ -606,12 +709,24 @@ def run(
|
|
|
606
709
|
jobs, task=task, n_workers=n_workers,
|
|
607
710
|
on_progress=on_progress, on_job_error=on_job_error,
|
|
608
711
|
text_progress=False,
|
|
712
|
+
# The tqdm bar above already shows aggregate progress -
|
|
713
|
+
# the engine's own milestone logging would just repeat it.
|
|
714
|
+
milestones=False,
|
|
609
715
|
)
|
|
610
716
|
else:
|
|
717
|
+
# `text_progress=None` keeps the existing default (the book's
|
|
718
|
+
# `\r` line whenever interactive and show_progress=False);
|
|
719
|
+
# passing it explicitly - e.g. to force it off so per-job DEBUG
|
|
720
|
+
# logging isn't fighting a `\r`-rewriting line on the same
|
|
721
|
+
# stream - always wins.
|
|
722
|
+
effective_text_progress = (
|
|
723
|
+
text_progress if text_progress is not None
|
|
724
|
+
else (not show_progress) and interactive
|
|
725
|
+
)
|
|
611
726
|
raw_results = process_jobs(
|
|
612
727
|
jobs, task=task, n_workers=n_workers,
|
|
613
728
|
on_progress=log_job, on_job_error=on_job_error,
|
|
614
|
-
text_progress=
|
|
729
|
+
text_progress=effective_text_progress,
|
|
615
730
|
)
|
|
616
731
|
elapsed_s = time.perf_counter() - t0
|
|
617
732
|
|
|
@@ -647,9 +762,17 @@ def run(
|
|
|
647
762
|
run_id, task, n_ok, n_failed, elapsed_s,
|
|
648
763
|
(len(raw_results) / elapsed_s) if elapsed_s > 0 else 0.0,
|
|
649
764
|
)
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
765
|
+
# Real _summary_log.info(...) calls - reaches run.log and any handler
|
|
766
|
+
# a prod deployment configures either way. On an interactive terminal
|
|
767
|
+
# with nothing configured, `_terminal_summary_handler` (entered above)
|
|
768
|
+
# is what makes them actually appear on screen - there is no separate
|
|
769
|
+
# print() standing in for a log line here.
|
|
770
|
+
for label, value in (
|
|
771
|
+
("Logs stored here ", log_path),
|
|
772
|
+
("Output stored here ", output_path),
|
|
773
|
+
("Manifest stored here", manifest_path),
|
|
774
|
+
):
|
|
775
|
+
_summary_log.info("%s - %s", label, value)
|
|
653
776
|
|
|
654
777
|
return RunSummary(
|
|
655
778
|
run_id=run_id,
|
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
"Basilisk", "Cyclone", "Draco", "Ember", "Fenrir", "Gargoyle", "Helix", "Icarus",
|
|
7
7
|
"Juggernaut", "Karma", "Labyrinth", "Maverick", "Nexus", "Oracle", "Paradox", "Quantum",
|
|
8
8
|
"Ronin", "Sentinel", "Talisman", "Ultra", "Valkyrie", "Warden", "Xenon", "Yeti",
|
|
9
|
-
"Zodiac", "Anomaly", "Banshee", "Catalyst", "Dynamo", "Eclipse", "Frost", "Glitch"
|
|
9
|
+
"Zodiac", "Anomaly", "Banshee", "Catalyst", "Dynamo", "Eclipse", "Frost", "Glitch", "UltraLinda"
|
|
10
10
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mpengine
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A small, general-purpose multiprocessing engine: dispatch any callable across processes with run manifests, per-worker logs, on-disk outputs and failure isolation
|
|
5
5
|
Author: Amandeep Singh
|
|
6
6
|
License-Expression: MIT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|