spacr.data_manager¶
Workflow inputs and outputs¶
Data Manager¶
Inspect project files and disk usage. Removing derived artifacts invalidates downstream uses of those files; original images must remain recoverable.
Open: the application’s Help/tools menus.
Inputs and outputs below include conditional alternatives. The guidance and handoff notes say which route applies.
Inputs
Run history and artifacts — Project run records, settings, output paths, artifact provenance, status and logs.
Outputs
Run history and artifacts — Project run records, settings, output paths, artifact provenance, status and logs.
What a project costs in disk, and how to get it back without losing data.
An imaging screen is mostly intermediates. stack/ is the raw files
re-shaped, merged/ is stack/ with the masks concatenated on, data/
is one PNG per object, and a plate that arrives as 40 GB of TIFFs leaves 400
GB behind. A multi-terabyte project has no in-app answer to “where did the
space go?”, and no way to clear derived data that does not also risk the one
thing on the disk nobody can make again — the images off the microscope.
This module answers both, and it answers the second one conservatively.
Disk usage¶
scan_project() walks the project once and reports bytes per artifact
kind — raw images, channel stacks, merged arrays, masks, crops, databases,
model weights — measured from the filesystem, not from what the registry
happens to have recorded. Every file is reconciled against
spacr.artifacts.Registry:
a file under a registered artifact’s path is attributed to that artifact’s kind;
a file under none of them is unregistered — its bytes are reported (and labelled by the project layout, as a guess, so the number is readable) but its provenance is unknown;
a registered artifact whose path is no longer on disk is reported as missing, because a registry that claims files that are gone is a registry nobody should prune from.
Pruning¶
The safety property is the whole feature. An artifact is prunable only when it is regenerable, and “regenerable” is not a guess:
the registry knows it — anything unregistered is never a candidate;
its kind is not an original (
ORIGINAL_KINDS): nothing inspacr.portsdeclares that it produces raw images or sequencing reads, so nothing can make them again;a declared producing module exists for its kind, and the module the registry recorded is one of them;
that module’s inputs are complete — every recorded input artifact is still registered and still on disk, and
spacr.ports.check_ready()says the module could run in this project right now;it was written by a run that finished (
status == "complete");what is on disk still fingerprints to what was registered, so a folder somebody has since dropped a file into is not prunable;
it lives inside the project, reached without following a symlink;
every artifact sharing its path passes all of the above — three kinds live in
measurements.db, and deleting the file for one of them destroys the other two;no other registered artifact sits inside it or around it, because the bytes belong to the innermost one and a plan that under-reports what it deletes is the failure this module exists to prevent.
Anything that fails any of those is kept, and PrunePlan.kept says
which rule kept it. Unknown provenance always means keep.
Rule 3 is doing more work than it looks. ports.producers_of is empty for
raw-images and sequencing-reads, which is why originals are safe —
and it is empty today for channel-stacks too, so stack/ is reported
as unregistered bytes and never offered, even though it is one of the largest
intermediates. That is the correct answer while nothing declares it produces
them: the fix is a port declaration, not an exception here.
Nothing is deleted without a plan. plan_prune() returns exactly what
would go and how much it frees; prune() refuses to run unless it is
handed PrunePlan.token, a digest over that exact path set and byte
total, so a confirmation cannot authorise a deletion other than the one that
was shown.
Deleting is count first, delete second, verify. The tree is
re-fingerprinted against the plan before anything is removed; the registry
write is a count and a write on one predicate, checked for equality and
rolled back on any difference. See _verified_write() for why that is
the only property worth asserting here.
Archiving¶
plan_archive() and archive() move a project, or a subset of it,
somewhere else and leave a record: a manifest at the destination, a ledger at
the origin, and rows in the destination’s registry carrying the provenance
the artifacts arrived with. The registry still knows where everything went.
Public API¶
scan_project,ProjectUsage,KindUsage,format_usageWhere the space went.
plan_prune,PrunePlan,PruneCandidate,PruneSkip,format_prune_plan,prune,PruneResultWhat can safely go, and the deletion that is gated on it.
plan_archive,ArchivePlan,archive,ArchiveResultMoving it, with a record.
is_prunableThe predicate, on its own, for anything that wants to ask.
Exceptions¶
An archive could not be carried out, or could not be verified. |
|
A destructive call arrived without the plan's confirmation token. |
|
Anything this module refuses to do. |
|
A prune stopped before removing anything, and nothing was removed. |
|
A prune deleted some of the plan and could not finish it. |
Classes¶
One top-level entry an archive would move. |
|
What an archive would move, and where to. |
|
What an archive actually did. |
|
One registered artifact, measured on disk. |
|
What one artifact kind costs in this project. |
|
Where a project's disk went, reconciled against the registry. |
|
One thing a prune would delete. |
|
Exactly what a prune would delete, and what it would leave. |
|
What a prune actually did. |
|
One thing that was considered and kept, and why. |
Functions¶
|
Move a project somewhere else and leave a record of where it went. |
|
Render a |
|
Render a |
|
Render a byte count the way a disk report should read. |
|
Return |
|
Work out what moving a project — or part of one — would move. |
|
Work out what could be deleted, and prove it before deleting anything. |
|
Carry out a plan. Irreversible, and gated on the plan being unchanged. |
|
Measure a project and reconcile it against the artifact registry. |
Module Contents¶
- exception spacr.data_manager.ArchiveError[source]¶
Bases:
DataManagerErrorAn archive could not be carried out, or could not be verified.
Initialize self. See help(type(self)) for accurate signature.
- exception spacr.data_manager.ConfirmationRequired[source]¶
Bases:
DataManagerErrorA destructive call arrived without the plan’s confirmation token.
Initialize self. See help(type(self)) for accurate signature.
- exception spacr.data_manager.DataManagerError[source]¶
Bases:
ExceptionAnything this module refuses to do.
Initialize self. See help(type(self)) for accurate signature.
- exception spacr.data_manager.PruneAborted[source]¶
Bases:
DataManagerErrorA prune stopped before removing anything, and nothing was removed.
Raised when the tree no longer matches the plan, or when a registry write changed a different number of rows than the count that gated it. The invariant this type carries is in its name: nothing on disk was deleted.
Initialize self. See help(type(self)) for accurate signature.
- exception spacr.data_manager.PruneIncomplete[source]¶
Bases:
DataManagerErrorA prune deleted some of the plan and could not finish it.
Distinct from
PruneAbortedon purpose: that type promises nothing was removed, and a type whose promise is sometimes true is worse than no type at all.Initialize self. See help(type(self)) for accurate signature.
- class spacr.data_manager.ArchiveItem[source]¶
One top-level entry an archive would move.
- Parameters:
source – where it is now.
destination – where it would go.
size_bytes – its size.
n_files – files it holds.
kind – its kind, when an artifact claims it.
artifact_ids – the registry rows it carries.
- class spacr.data_manager.ArchivePlan[source]¶
What an archive would move, and where to.
- Parameters:
root – the project being archived from.
destination – the folder it would move into.
items – the top-level entries that would move.
total_bytes – how much moves.
total_files – how many files.
whole_project – True when every top-level entry is moving.
token – the confirmation; see
PrunePlan.token.created_utc – when the plan was made.
- class spacr.data_manager.ArchiveResult[source]¶
What an archive actually did.
- Parameters:
root – the origin.
destination – where it went.
moved –
(source, destination)per entry, in the order moved.total_bytes – bytes moved.
manifest_path – the manifest written at the destination.
ledger_path – the record left at the origin.
registered – artifacts registered in the destination’s registry.
finished_utc – when it finished.
- class spacr.data_manager.ArtifactUsage[source]¶
One registered artifact, measured on disk.
- Parameters:
path – the file or folder.
kinds – every kind registered at this path, ranked.
artifacts – the registry rows at this path, newest first.
size_bytes – bytes actually there now.
n_files – files actually there now.
exists – whether anything is there at all.
- class spacr.data_manager.KindUsage[source]¶
What one artifact kind costs in this project.
- Parameters:
kind – a
spacr.portskind, orOTHER_KIND.label – the display name.
size_bytes – bytes on disk, measured by walking the project.
n_files – files counted.
n_paths – distinct artifact paths (a folder counts once).
n_artifacts – registry rows of this kind.
registered_bytes – of
size_bytes, how much sits under a registered artifact.unregistered_bytes – the rest — bytes nobody claims.
recorded_bytes – what the registry says those artifacts weigh. A gap against
size_bytesmeans the folder changed since it was registered, which is exactly when a prune must decline.shared_paths – artifacts of this kind sitting at a path whose bytes are counted under a different kind — the three kinds inside
measurements.db. Reported so a kind showing zero bytes reads as “counted next door” rather than “gone”.
- class spacr.data_manager.ProjectUsage[source]¶
Where a project’s disk went, reconciled against the registry.
- Parameters:
root – the project root.
total_bytes – every byte under
root, symlinks excluded.total_files – every file under
root, symlinks excluded.kinds – per-kind breakdown, largest first.
artifacts – one entry per registered artifact path.
unregistered –
(path, size_bytes)for the largest unregistered top-level entries, largest first — the bytes no artifact claims.unregistered_bytes – their total.
unregistered_files – how many files that is.
missing – artifacts the registry has whose path is gone.
outside – registered artifacts whose path is not under
root.symlinks – links found and not followed. Never counted, never deleted: a link into somebody else’s storage is the one shape where a recursive delete leaves the project entirely.
errors – paths that could not be read, as strings.
scanned_utc – when the walk ran.
- __str__() str[source]¶
The full report; see
format_usage().
- artifact_at(path: str) ArtifactUsage | None[source]¶
Return the entry for one registered path, or None.
- Parameters:
path – registered artifact path to resolve absolutely and look up.
- class spacr.data_manager.PruneCandidate[source]¶
One thing a prune would delete.
- Parameters:
path – the file or folder that would go.
kind – its kind.
module – the module that would make it again.
artifact_ids – every registry row at this path.
size_bytes – what deleting it frees, measured now.
n_files – files it holds.
inventory_digest – the content fingerprint the plan was made against.
prune()re-computes it and refuses on any difference, which is how “the tree changed under us” stops a delete.sample_files – up to twenty paths, for showing a user what this is.
downstream – artifact ids derived from this one — results that would no longer be reproducible from what is left.
regenerate_with – the sentence telling a user how to get it back.
- class spacr.data_manager.PrunePlan[source]¶
Exactly what a prune would delete, and what it would leave.
Produced by
plan_prune()and consumed byprune(). Nothing else may be deleted:prune()takes a plan, not a folder.- Parameters:
root – the project.
candidates – what would go, largest first.
kept – what was considered and kept, with the rule that kept it.
total_bytes – what the prune frees.
total_files – how many files that is.
kinds – the kinds this plan was asked for.
token – the confirmation.
prune()refuses without it, and it is a digest over the exact path set and byte total, so a token taken from one plan cannot authorise a different one.unregistered_bytes – bytes in the project that no artifact claims — reported here because they are the reason a prune frees less than a user expected, and they are never candidates.
created_utc – when the plan was made.
- __str__() str[source]¶
The full report; see
format_prune_plan().
- file_list() Tuple[Tuple[str, ...], bool][source]¶
Enumerate every file this plan would delete, right now.
Walked fresh rather than stored: a plan for a project with millions of crops must not carry millions of strings, and the list a user is shown should be the list that is on disk when they look at it.
- Returns:
(paths, truncated).truncatedis True when the plan holds more thanMAX_RECORDED_FILESfiles and the list was cut short.
- class spacr.data_manager.PruneResult[source]¶
What a prune actually did.
- Parameters:
root – the project.
removed_paths – the artifact paths deleted, in plan order.
removed_files – every file removed. Empty with
files_truncatedwhen the plan held more thanMAX_RECORDED_FILES.files_truncated – the list was too long to keep.
freed_bytes – bytes freed, as counted by the plan.
n_files – files removed.
registry_rows – registry rows marked or forgotten.
forgotten – whether those rows were deleted rather than marked.
finished_utc – when it finished.
- class spacr.data_manager.PruneSkip[source]¶
One thing that was considered and kept, and why.
- Parameters:
path – the file or folder.
kind – its kind, as far as anything knows.
size_bytes – what keeping it costs.
reason – the rule that kept it, in a sentence a user can act on.
artifact_id – the registry row, when there was one.
- spacr.data_manager.archive(plan: ArchivePlan, *, confirm: str, registry: spacr.artifacts.Registry | None = None) ArchiveResult[source]¶
Move a project somewhere else and leave a record of where it went.
Three records are left, because one of them may move with the data:
a manifest at the destination naming the origin, the time, the spaCR version and every artifact that arrived, with its provenance;
a ledger at the origin — appended to, never overwritten — so a folder somebody finds nearly empty still says where its contents are;
registry rows at the destination, one per artifact, carrying the module, settings hash and inputs the artifact arrived with plus
extra['archived_from']. The destination is self-describing, andspacr.artifacts.by_project()on it answers.
The origin’s own rows are marked
archived_to— through the same counted, verified write a prune uses — unless the registry file is itself moving, in which case the marks would travel with it and say the wrong thing.Nothing is overwritten: a destination entry that already exists stops the call before anything moves.
- Parameters:
plan – from
plan_archive().confirm –
ArchivePlan.token.registry – the origin’s registry; opened from the root when omitted.
- Returns:
an
ArchiveResult.- Raises:
ConfirmationRequired – without the right token.
ArchiveError – when a destination entry exists, or when a move cannot be verified afterwards.
- spacr.data_manager.format_prune_plan(plan: PrunePlan, *, limit: int = 20) str[source]¶
Render a
PrunePlanas the text shown before a deletion.- Parameters:
plan – the plan.
limit – how many kept entries to explain.
- spacr.data_manager.format_usage(usage: ProjectUsage, *, limit: int = 8) str[source]¶
Render a
ProjectUsageas a block of text.- Parameters:
usage – the result of
scan_project().limit – how many unregistered paths to list.
- spacr.data_manager.human_bytes(size: float) str[source]¶
Render a byte count the way a disk report should read.
- Parameters:
size – bytes.
- Returns:
e.g.
"1.4 GB". Powers of 1000, because that is what the disk vendor,dfand the user’s storage quota all use.
- spacr.data_manager.is_prunable(artifact: spacr.artifacts.Artifact, *, root: Any, registry: spacr.artifacts.Registry | None = None, group: Sequence[spacr.artifacts.Artifact] = ()) str[source]¶
Return
""whenartifactmay be pruned, else the reason to keep.An empty string is the only value that means “delete this”. Every failure mode — including one this function did not anticipate — produces a sentence, which is the direction a delete predicate must fail in.
- Parameters:
artifact – a registered artifact. Something the registry has never heard of cannot be passed here at all, which is the point.
root – the project root it must live inside.
registry – the registry, for checking that its inputs survive.
group – every artifact registered at the same path. A path is prunable only when all of them are:
measurements.dbcarries three kinds, and deleting the file for one destroys the other two.
- Returns:
""or a reason.
- spacr.data_manager.plan_archive(root: Any, destination: Any, *, registry: spacr.artifacts.Registry | None = None, paths: Sequence[str] | None = None, usage: ProjectUsage | None = None) ArchivePlan[source]¶
Work out what moving a project — or part of one — would move.
Entries are top-level: a whole project archive moves every child of the root, and a subset archive moves the paths named. Moving a file out of a registered folder is not offered, because half a
merged/in two places is worse than either place having all of it.- Parameters:
root – the project root.
destination – the folder to move into. It must not already hold an entry with the same name; an archive never overwrites.
registry – an open registry, for the provenance carried along.
paths – entries to move. Defaults to everything under
root.usage – a
ProjectUsage, to save a second walk.
- Returns:
an
ArchivePlan.- Raises:
DataManagerError – when
rootis not a folder, when a named path is not inside it, or when the destination is inside the root.
- spacr.data_manager.plan_prune(root: Any, *, registry: spacr.artifacts.Registry | None = None, kinds: Sequence[str] | None = None, usage: ProjectUsage | None = None, paths: Sequence[str] | None = None) PrunePlan[source]¶
Work out what could be deleted, and prove it before deleting anything.
Every registered artifact in the project is put through
is_prunable(). What passes becomes a candidate; what does not becomes aPruneSkipcarrying the rule that kept it, so a user who expected to free 300 GB and was offered 12 GB can read why.Unregistered bytes are never candidates. They are counted and reported (
PrunePlan.unregistered_bytes), because “spaCR does not know what made this” is information, but the answer for them is always keep.- Parameters:
root – the project root.
registry – an open registry; opened from
rootwhen omitted. A project with no registry produces an empty plan, which is correct: nothing there has known provenance.kinds – kinds to consider. Defaults to
DEFAULT_PRUNABLE_KINDS. Naming aPROTECTED_KINDSmember opts it in — it still has to pass every safety rule. Naming anORIGINAL_KINDSmember does nothing: there is no path through this module that deletes an original.usage – a
ProjectUsagefromscan_project(), to save a second walk of a large project.paths – restrict to these artifact paths.
- Returns:
a
PrunePlan.
- spacr.data_manager.prune(plan: PrunePlan, *, confirm: str, registry: spacr.artifacts.Registry | None = None, forget_rows: bool = False) PruneResult[source]¶
Carry out a plan. Irreversible, and gated on the plan being unchanged.
The order is the safety story:
the confirmation must equal
PrunePlan.token, a digest over the plan’s exact paths and byte total, so a token cannot authorise a deletion other than the one it was shown for;every candidate is re-fingerprinted and must still match the plan. Any difference — a file added, a file changed, the folder gone — aborts the whole call before a single delete;
the registry write happens next, inside one transaction, counted and verified (see
_verified_write()). A mismatch rolls it back and raises with nothing on disk deleted;only then are the files removed;
every path is checked to be gone afterwards.
Step 3 commits before step 4 deliberately. A crash between them leaves the registry saying an artifact was pruned while its files are still there — recoverable by running the prune again. The other order leaves files deleted that the registry still describes as present, which is the state nobody can recover from.
- Parameters:
plan – from
plan_prune().confirm –
PrunePlan.token.registry – the project registry; opened from the plan’s root when omitted.
forget_rows – delete the registry rows instead of marking them. Off by default: the row is the recipe for making the data again.
- Returns:
a
PruneResult.- Raises:
ConfirmationRequired – without the right token.
PruneAborted – when the tree changed, or a count and its write disagreed. Nothing was deleted in either case.
PruneIncomplete – when a path survived its own deletion — a permission, a busy file. Some of the plan did go, which is why this is a different type from
PruneAborted.
- spacr.data_manager.scan_project(root: Any, *, registry: spacr.artifacts.Registry | None = None) ProjectUsage[source]¶
Measure a project and reconcile it against the artifact registry.
One walk of the tree, then every file is attributed to the registered artifact whose path contains it — longest path wins — or to nobody. The per-kind numbers come from the filesystem; the registry’s own
size_bytesis reported alongside so drift between the two is visible rather than assumed away.- Parameters:
root – the project root.
registry – an open
spacr.artifacts.Registry. Omit and one is opened forrootwhen a registry file exists — including the shared onespacr.artifacts.ARTIFACTS_DB_ENVpoints at. A project with no registry is scanned anyway, and reports every byte as unregistered, which is the correct answer and the reason nothing in it is prunable.
- Returns:
a
ProjectUsage.- Raises:
DataManagerError – when
rootis not a directory.