synology_apm_repo.sdk.presentation.progress module

Progress/ProgressMeter: the one progress contract CLI and TUI both build on, so neither grows its own rate/ETA math that disagrees with the other’s.

Four rules, implemented here rather than left to each caller to reinvent:

  1. The denominator is planned work, not logical size — a caller’s job; this module just carries whatever total it’s given (e.g. a sparse image only needs the chunks a planning pass found, not its full logical size).

  2. Rate is a windowed average over the last rate_window_seconds of real elapsed time, not a cumulative average (mis-predicts ETA right after a rate change) and not a per-call EWMA (a burst of near-zero-dt calls would drag a constant-α blend up to a spike value). ETA is suppressed until enough time/progress has accumulated (a warm-up window), and reported quantized so it doesn’t jitter.

  3. New rate samples are taken no more often than rate_sample_interval, decoupling how often the windowed average is recomputed from how often ProgressMeter.update is called — this is what makes rule 2’s fix actually hold, and keeps the sample deque bounded regardless of real call frequency.

  4. Reporting itself is rate-limited inside the SDK — calling a UI callback per chunk would cost more than the decode it measures. ProgressMeter.update is cheap to call every tick; it only forwards to the wrapped callback every min_interval seconds or min_delta units, whichever comes first.

class synology_apm_repo.sdk.presentation.progress.Progress(phase, determinate, done=0, total=None, unit='bytes', detail='', found=None)

Bases: object

One snapshot of a long-running operation’s state.

Variables:
  • phase (str) – "discovering", "planning", "reading", "assembling", or "verifying".

  • determinate (bool) – Whether total is meaningful.

  • total (int | None) – None when the total is unknown.

  • unit (str) – "bytes", "items", "chunks", or "buckets".

  • detail (str) – The file/object currently being processed, for UI display.

  • found (int | None) – Incremental result count when total is unknown.

phase: str
determinate: bool
done: int = 0
total: int | None = None
unit: str = 'bytes'
detail: str = ''
found: int | None = None
class synology_apm_repo.sdk.presentation.progress.ProgressMeter(callback=None, *, min_interval=0.1, min_delta=0, rate_window_seconds=3.0, rate_sample_interval=0.2, eta_warmup_seconds=2.0, eta_warmup_fraction=0.01, now=<built-in function monotonic>)

Bases: object

Smooths raw Progress snapshots into a stable rate/ETA (rules 2-3) and rate-limits how often the wrapped callback actually fires (rule 4). Construct one per operation; call update as often as convenient (every chunk is fine).

async update(progress)

Record progress and, if rule 4’s throttle says so, notify.

async def even though the sampling arithmetic itself is pure: the callback is Awaitable, so making this method async (rather than sync with a fire-and-forget Task) is what keeps back-pressure intact — a slow UI callback slows the producer down instead of piling up unawaited Tasks. The callback is awaited only when the throttle decides to notify, so the per-tick cost stays cheap.

property latest: Progress | None
property rate: float

The smoothed rate, in units per second.

property elapsed: timedelta
property eta: timedelta | None

None when the total is unknown, no progress has been recorded yet, or too little time/fraction has elapsed to trust a rate estimate (rule 2’s warm-up window).

synology_apm_repo.sdk.presentation.progress.reading_progress_callback(meter)

Adapts meter to the raw (done, total) shape ContentSource.export_to’s own progress callback calls, as a phase="reading" byte count — the one adapter both the CLI’s export command and the TUI’s export screen need, so a UI-facing “rate”/”ETA” can never disagree about what a raw export callback’s numbers mean.