Writing results safely¶
Two small modules that everything else depends on being correct. Neither is part of the user interface; both exist because of failure modes that are invisible until they have already cost someone their results.
Atomic writes¶
Analysis steps stream output row by row, and steps skip work whose output already exists — that is what makes a long pipeline resumable. Together those two facts make an interrupted run dangerous: a half-written file keeps its final name, and the next run accepts it as finished.
Writing under a scratch name and renaming at the end removes that. A rename is indivisible as far as the filesystem is concerned, so the real name only ever refers to a complete file.
taters.helpers.atomic ¶
Write a file under a scratch name, and give it its real name only when finished.
Analysis steps stream their output row by row, which means the output file exists -- with its final name -- from the first row onwards. Anything that interrupts the run leaves that half-written file behind looking exactly like a completed one.
That would be survivable on its own. What makes it a real hazard is the other half of the design: steps skip work whose output already exists, because that is what makes a long pipeline resumable. So a truncated file is not merely wrong, it is sticky -- the next run sees it, decides the step is done, and returns it. You get 3 rows where you asked for 300,000, with no error.
The fix is to write to <name>.part and rename it at the end. A rename is
indivisible as far as the filesystem is concerned: there is no moment at which
the destination exists half-renamed. Every other part of the write can be
interrupted; that step cannot. So the real name only ever refers to a complete
file, and an interrupted run leaves nothing for the next one to mistake for
finished work.
This covers interruption generally -- a cancelled run, a full disk, a power cut, Ctrl-C -- not just any one of them.
atomic_write ¶
atomic_write(path, mode='w', **open_kwargs)
Open a file for writing that only appears at path once complete.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str or Path
|
Where the finished file should end up. Parent directories are created. |
required |
mode
|
str
|
As :func: |
"w"
|
**open_kwargs
|
Any
|
Passed straight through to :func: |
{}
|
Yields:
| Type | Description |
|---|---|
IO
|
The handle to write to. It refers to the scratch file, not to |
Notes
On an exception the scratch file is removed and path is left exactly as
it was -- which for a first run means absent, so the step is retried rather
than resumed from a partial file.
Not safe for two processes writing the same destination at once: they would share a scratch name. Nothing in Taters does that, since a GLOBAL step runs once and ITEM steps write per-input paths.
Source code in src\taters\helpers\atomic.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | |
Progress reporting¶
A GLOBAL pipeline step is a single call, so the runner cannot count it from
outside. A step that can count itself says so by declaring an on_progress
parameter, which the runner then injects automatically — a signature check
rather than a registry, so a new analyzer opts in without anything else needing
to know.
taters.helpers.progress ¶
Row-level progress reporting for analysis steps.
A GLOBAL pipeline step is a single call: the runner hands over and gets control
back at the end, so it cannot count anything from outside. A step that can
count itself says so by declaring an on_progress parameter, which the
pipeline runner then injects automatically.
The contract
on_progress(done, total, message=None, unit=None)
totalis an int once the size of the job is known, anddoneis a position within it.totalisNonewhile the size is still being worked out.doneis then a running tally, andmessagesays which pass is running -- reading the input, counting its rows. On a large file those passes take long enough to look like a hang, and a number climbing is the only visible proof that anything is happening.unitnames what is being counted when it is not rows. The only value with a meaning today is"seconds", used by transcription, where the numbers are a position in the recording:252/771would be read as segments, which is not what it is, while4:12/12:51cannot be misread.
The last two arguments are optional in both directions -- a sink that predates them still works, because nothing is obliged to send them.
This module exists so the five text analyzers share one implementation of that pattern rather than five copies that drift.
Ticker ¶
Ticker(on_progress, total=0)
Counts work as it happens and reports it.
Safe to use unconditionally: with no on_progress every method is a
no-op, so the analysis code reads the same whether anyone is watching.
Source code in src\taters\helpers\progress.py
101 102 103 104 105 106 | |
tick ¶
tick(n=1, message=None)
Record n more units of work done.
message names the unit -- "scoring
Source code in src\taters\helpers\progress.py
108 109 110 111 112 113 114 115 116 117 118 119 120 | |
FlightReporter ¶
FlightReporter(on_progress, total, message)
Progress for a phase whose work is spread over parallel workers.
Reports the usual (done, total, message) plus -- to sinks that accept it
-- inflight: the names currently being worked on, so a display can
show one sub-bar per file the way item steps do for ffmpeg or Whisper.
start/finish may be called from executor threads; consumed
is the parent's in-order tally. All three re-report immediately.
Source code in src\taters\helpers\progress.py
156 157 158 159 160 161 162 163 164 165 166 167 168 | |
announce ¶
announce(on_progress, message)
Name the phase that is about to run, with no size known yet.
Source code in src\taters\helpers\progress.py
45 46 47 48 | |
count_rows ¶
count_rows(
path,
*,
on_progress=None,
encoding="utf-8-sig",
every=1000
)
Count the data records in a CSV, reporting progress as it goes.
Records, not lines: document text carries embedded newlines inside its quoted field, and counting physical lines told a 2,300-paper run it had 3.2 million rows to do -- a denominator so wrong the bar read as broken.
Always counts, and only reports when something is watching. It used
to return 0 with no sink, on the theory that the count was only ever a
progress bar's denominator -- but :func:taters.helpers.row_map.map_text_rows
sizes its worker pool from it, so every direct API call (no sink) ran
on one worker while the same call from the wizard ran on twelve. One
csv.reader sweep is far cheaper than the work it sizes.
The header is not counted. A file that cannot be read comes back as 0 rather than raising -- a progress bar is not worth failing a run over.
Source code in src\taters\helpers\progress.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | |