PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
Loading...
Searching...
No Matches
Data Structures | Functions | Variables
picurv_cli.storage Namespace Reference

Data Structures

class  StorageError
 User-facing storage workflow failure. More...
 

Functions

str _utc_now ()
 Return a stable UTC timestamp for storage metadata.
 
str _human_bytes (int value)
 Format a byte count for concise command output.
 
str _sha256_file (str path)
 Calculate SHA-256 without loading a potentially large file into memory.
 
None _atomic_write_json (str path, dict payload)
 Atomically replace a JSON state or manifest file.
 
 _read_json (str path)
 Read a JSON mapping when present, otherwise return None.
 
 _find_upwards (str start, str filename)
 Find the nearest named file at or above a filesystem anchor.
 
str resolve_storage_config_path (str explicit_path=None, bool require=True)
 Resolve an explicit or nearest workspace storage configuration.
 
dict load_storage_profile (str profile_name=None, str config_path=None)
 Load and validate one non-secret rclone storage profile.
 
str _remote_join (str remote, *str parts)
 Join path components without corrupting rclone remote syntax.
 
str _object_remote (dict profile, str archive_id, *str parts)
 Return the remote path for one immutable archive object.
 
subprocess.CompletedProcess _run_rclone (list arguments, bool check=True)
 Invoke rclone through the same argv-based subprocess boundary as other PICurv tools.
 
str _remote_sha256 (str remote_path)
 Ask rclone to calculate or retrieve the SHA-256 of one remote object.
 
dict _upload_verified (str local_path, str remote_path)
 Upload one file, then verify its remote SHA-256.
 
bytes _read_remote_bytes (str remote_path)
 Read a small remote catalog object through rclone.
 
dict _load_remote_manifest (dict profile, str archive_id, bool require_complete=True)
 Fetch and validate one versioned remote storage manifest.
 
str _state_path (str root_path)
 Return the local storage state marker path for an artifact root.
 
 read_storage_state (str root_path)
 Read the nearest applicable storage marker for a run, study, or study member.
 
bool is_artifact_cold (str root_path)
 Return whether a local artifact marker says payload data was pruned.
 
list cold_study_members (str study_path)
 Return numbered study members whose local payload was pruned.
 
None require_storage_payload_local (str root_path, str operation, int checkpoint=None, checkpoints=None)
 Reject a workflow that requires payload currently held in cold storage.
 
dict storage_state_summary (str root_path)
 Return a compact storage status for summarize and status commands.
 
str _validate_case_id (str case_id)
 Validate a canonical numbered study-member identifier.
 
list resolve_local_storage_targets (str run_dir=None, str study_dir=None, case_ids=None)
 Resolve explicit run/study selectors into concrete artifact descriptions.
 
bool _path_is_within (str root, str candidate)
 Return whether an absolute candidate remains within a root directory.
 
str _resolve_configured_path (str root, str value)
 Resolve runtime-directory syntax using the run directory as working directory.
 
list _artifact_runtime_roots (str root)
 Return run-like roots contained by a standalone run or whole study.
 
list _discover_external_paths (str root)
 Report configured data paths that escape the archived directory boundary.
 
list _discover_dependencies (str root)
 Discover absolute restart/source paths embedded in generated controls.
 
list _walk_archive_entries (str root)
 Enumerate archive entries without following symlinks.
 
 _checkpoint_component (str relative_path)
 Return a checkpoint component token for a path inside a committed step bundle.
 
str _classify_component (str relative_path)
 Classify one artifact path for packaging and local retention.
 
list _checkpoint_steps (list entries)
 Return committed checkpoint steps represented in an inventory.
 
list _find_incomplete_checkpoints (list entries)
 Return incomplete checkpoint paths that make archival unsafe.
 
bool _lock_owner_active (str metadata_path)
 Conservatively determine whether a solver/post/storage owner marker is active.
 
set _collect_job_ids (payload)
 Recursively collect submitted Slurm job IDs from scheduler metadata.
 
dict _slurm_activity (str root)
 Query live Slurm state for every job recorded below an artifact scheduler directory.
 
dict inspect_artifact (dict target, bool query_scheduler=True)
 Build a read-only inventory and lifecycle assessment for one artifact.
 
None _assert_archive_safe (dict inventory)
 Refuse to package a changing or scheduler-ambiguous artifact.
 
 storage_operation_lock (str root_path, str operation)
 Hold an exclusive local storage-operation marker for one artifact.
 
 runtime_stage_lock (str root_path, str stage)
 Mark a locally executed solver/post stage as active for storage safety.
 
str _select_compression (str requested, int total_bytes, dict profile)
 Resolve automatic or configured compression policy.
 
str _chunk_extension (str compression)
 Return the archive suffix for a compression policy.
 
list _build_chunk_specs (dict inventory, int chunk_size_bytes)
 Group archive entries into independently transferable component chunks.
 
str _safe_component_name (str component)
 Convert a component token into a portable archive filename fragment.
 
None _write_tar_chunk (str root, dict spec, str destination, str compression)
 Package explicitly inventoried entries without following symlinks.
 
list _capture_study_context (dict target)
 Embed small study control-plane files with an individually archived member.
 
dict _config_fingerprints (str root)
 Hash small canonical YAML inputs stored under an artifact.
 
dict _git_provenance (str root)
 Record best-effort current source revision and dirty state.
 
dict _parse_tags (raw_tags)
 Parse repeatable KEY=VALUE tags into deterministic metadata.
 
dict build_storage_plan (dict target, dict profile, str compression=None)
 Build the read-only plan consumed by protect and offload.
 
None _render_plan (dict plan)
 Print a concise archive/offload plan.
 
dict archive_artifact (dict target, dict profile, str label=None, tags=None, str compression=None, bool prune_local=False)
 Package, upload, verify, register, and optionally prune one artifact.
 
None _prune_archived_payload (str root, dict inventory)
 Remove only verified heavy payload while retaining control-plane files.
 
list list_remote_manifests (dict profile)
 Enumerate completed archive manifests from the remote catalog.
 
dict verify_remote_archive (dict profile, str archive_id)
 Verify the completion marker and every stored chunk checksum.
 
None _validate_tar_members (tarfile.TarFile archive)
 Reject archive members that could escape the restore destination.
 
None _extract_chunk (str path, str destination)
 Safely extract one verified tar chunk into a staging tree.
 
None _restore_study_context (dict manifest, str case_destination)
 Recreate missing study control-plane files around a restored member.
 
None _merge_tree (str source, str destination)
 Merge a verified restore tree into a known cold artifact skeleton.
 
list _rebase_restored_text_paths (str root, list replacements)
 Rebase known generated text artifacts after an explicit relocated restore.
 
dict restore_archive (dict profile, str archive_id, str destination=None, checkpoints=None, bool force=False)
 Download, verify, extract, and materialize an archive or selected checkpoints.
 
str _resolve_archive_id_from_args (args)
 Resolve an explicit archive ID or a local artifact marker.
 
None _render_status (dict inventory)
 Render one artifact status row and safety details.
 
None storage_setup_workflow (args)
 Create or update a non-secret workspace storage profile.
 
None storage_status_workflow (args)
 Print local storage and lifecycle status for selected artifacts.
 
None storage_plan_workflow (args)
 Render a read-only packaging and safety plan.
 
None storage_archive_workflow (args, bool prune_local)
 Execute protect or offload for one or more explicit local targets.
 
None storage_restore_workflow (args)
 Restore a remote archive by globally unique ID or local marker.
 
None storage_verify_workflow (args)
 Verify all remote chunks for one archive.
 
None storage_list_workflow (args)
 Search the remote manifest catalog without local artifact state.
 
None storage_show_workflow (args)
 Show one complete remote archive manifest.
 
None storage_workflow (args)
 Dispatch nested storage actions using existing PICurv workflow conventions.
 
argparse.ArgumentParser add_storage_parser (subparsers)
 Attach the nested storage command parser to PICurv's top-level parser.
 

Variables

str STORAGE_CONFIG_FILENAME = ".picurv-storage.yml"
 
str STORAGE_STATE_FILENAME = ".picurv-storage.json"
 
str STORAGE_LOCK_FILENAME = ".picurv-storage.lock.json"
 
int STORAGE_SCHEMA_VERSION = 1
 
str REMOTE_OBJECTS_DIRECTORY = "objects"
 
str REMOTE_MANIFEST_FILENAME = "manifest.json"
 
str REMOTE_COMPLETE_FILENAME = "COMPLETE"
 
str DEFAULT_PROFILE_NAME = "archive"
 
float DEFAULT_CHUNK_SIZE_GIB = 8.0
 
int AUTO_NO_COMPRESSION_BYTES = 256 * 1024 * 1024
 
int AUTO_MAXIMUM_COMPRESSION_BYTES = 20 * 1024 * 1024 * 1024
 
 CHECKPOINT_DIRECTORY_PATTERN = re.compile(r"^step_(\d{12})$")
 
 INCOMPLETE_CHECKPOINT_PATTERN = re.compile(r"^\.step_\d{12}\.incomplete\.")
 
 ARCHIVE_ID_PATTERN = re.compile(r"^[0-9a-f]{32}$")
 
int KNOWN_CHECKPOINT_VERSION = 1
 

Function Documentation

◆ _utc_now()

str picurv_cli.storage._utc_now ( )
protected

Return a stable UTC timestamp for storage metadata.

Returns
Result produced by this operation.

Definition at line 54 of file storage.py.

54def _utc_now() -> str:
55 """!
56 @brief Return a stable UTC timestamp for storage metadata.
57 @return Result produced by this operation.
58 """
59 return datetime.datetime.now(datetime.timezone.utc).isoformat()
60
61
Here is the caller graph for this function:

◆ _human_bytes()

str picurv_cli.storage._human_bytes ( int  value)
protected

Format a byte count for concise command output.

Parameters
[in]valueValue supplied through the value argument.
Returns
Result produced by this operation.

Definition at line 62 of file storage.py.

62def _human_bytes(value: int) -> str:
63 """!
64 @brief Format a byte count for concise command output.
65 @param[in] value Value supplied through the `value` argument.
66 @return Result produced by this operation.
67 """
68 size = float(value)
69 for suffix in ("B", "KiB", "MiB", "GiB", "TiB", "PiB"):
70 if size < 1024.0 or suffix == "PiB":
71 return f"{size:.1f} {suffix}" if suffix != "B" else f"{int(size)} B"
72 size /= 1024.0
73 return f"{int(value)} B"
74
75

◆ _sha256_file()

str picurv_cli.storage._sha256_file ( str  path)
protected

Calculate SHA-256 without loading a potentially large file into memory.

Parameters
[in]pathValue supplied through the path argument.
Returns
Result produced by this operation.

Definition at line 76 of file storage.py.

76def _sha256_file(path: str) -> str:
77 """!
78 @brief Calculate SHA-256 without loading a potentially large file into memory.
79 @param[in] path Value supplied through the `path` argument.
80 @return Result produced by this operation.
81 """
82 digest = hashlib.sha256()
83 with open(path, "rb") as stream:
84 while True:
85 block = stream.read(8 * 1024 * 1024)
86 if not block:
87 break
88 digest.update(block)
89 return digest.hexdigest()
90
91
Here is the caller graph for this function:

◆ _atomic_write_json()

None picurv_cli.storage._atomic_write_json ( str  path,
dict  payload 
)
protected

Atomically replace a JSON state or manifest file.

Parameters
[in]pathValue supplied through the path argument.
[in]payloadValue supplied through the payload argument.

Definition at line 92 of file storage.py.

92def _atomic_write_json(path: str, payload: dict) -> None:
93 """!
94 @brief Atomically replace a JSON state or manifest file.
95 @param[in] path Value supplied through the `path` argument.
96 @param[in] payload Value supplied through the `payload` argument.
97 """
98 path_abs = os.path.abspath(path)
99 os.makedirs(os.path.dirname(path_abs), exist_ok=True)
100 temporary = f"{path_abs}.tmp.{os.getpid()}"
101 with open(temporary, "w", encoding="utf-8") as stream:
102 json.dump(payload, stream, indent=2, sort_keys=True)
103 stream.write("\n")
104 stream.flush()
105 os.fsync(stream.fileno())
106 os.replace(temporary, path_abs)
107
108
Here is the caller graph for this function:

◆ _read_json()

picurv_cli.storage._read_json ( str  path)
protected

Read a JSON mapping when present, otherwise return None.

Parameters
[in]pathValue supplied through the path argument.
Returns
Result produced by this operation.

Definition at line 109 of file storage.py.

109def _read_json(path: str):
110 """!
111 @brief Read a JSON mapping when present, otherwise return None.
112 @param[in] path Value supplied through the `path` argument.
113 @return Result produced by this operation.
114 """
115 try:
116 with open(path, "r", encoding="utf-8") as stream:
117 payload = json.load(stream)
118 except (OSError, ValueError):
119 return None
120 return payload if isinstance(payload, dict) else None
121
122
Here is the caller graph for this function:

◆ _find_upwards()

picurv_cli.storage._find_upwards ( str  start,
str  filename 
)
protected

Find the nearest named file at or above a filesystem anchor.

Parameters
[in]startValue supplied through the start argument.
[in]filenameValue supplied through the filename argument.
Returns
Result produced by this operation.

Definition at line 123 of file storage.py.

123def _find_upwards(start: str, filename: str):
124 """!
125 @brief Find the nearest named file at or above a filesystem anchor.
126 @param[in] start Value supplied through the `start` argument.
127 @param[in] filename Value supplied through the `filename` argument.
128 @return Result produced by this operation.
129 """
130 current = os.path.abspath(start)
131 if os.path.isfile(current):
132 current = os.path.dirname(current)
133 while True:
134 candidate = os.path.join(current, filename)
135 if os.path.isfile(candidate):
136 return candidate
137 parent = os.path.dirname(current)
138 if parent == current:
139 return None
140 current = parent
141
142
Here is the caller graph for this function:

◆ resolve_storage_config_path()

str picurv_cli.storage.resolve_storage_config_path ( str   explicit_path = None,
bool   require = True 
)

Resolve an explicit or nearest workspace storage configuration.

Parameters
[in]explicit_pathOptional user-selected YAML path.
[in]requireWhether a missing configuration is an error.
Returns
Result produced by this operation.

Definition at line 143 of file storage.py.

143def resolve_storage_config_path(explicit_path: str = None, require: bool = True) -> str:
144 """!
145 @brief Resolve an explicit or nearest workspace storage configuration.
146 @param[in] explicit_path Optional user-selected YAML path.
147 @param[in] require Whether a missing configuration is an error.
148 @return Result produced by this operation.
149 """
150 if explicit_path:
151 result = os.path.abspath(explicit_path)
152 else:
153 result = _find_upwards(os.getcwd(), STORAGE_CONFIG_FILENAME)
154 if result and os.path.isfile(result):
155 return result
156 if require:
157 raise StorageError(
158 "No PICurv storage configuration was found. Run "
159 "'picurv storage setup --remote <rclone-remote:path>' first or pass --storage-config."
160 )
161 return os.path.abspath(explicit_path or os.path.join(os.getcwd(), STORAGE_CONFIG_FILENAME))
162
163
Here is the call graph for this function:
Here is the caller graph for this function:

◆ load_storage_profile()

dict picurv_cli.storage.load_storage_profile ( str   profile_name = None,
str   config_path = None 
)

Load and validate one non-secret rclone storage profile.

Parameters
[in]profile_nameValue supplied through the profile_name argument.
[in]config_pathValue supplied through the config_path argument.
Returns
Result produced by this operation.

Definition at line 164 of file storage.py.

164def load_storage_profile(profile_name: str = None, config_path: str = None) -> dict:
165 """!
166 @brief Load and validate one non-secret rclone storage profile.
167 @param[in] profile_name Value supplied through the `profile_name` argument.
168 @param[in] config_path Value supplied through the `config_path` argument.
169 @return Result produced by this operation.
170 """
171 resolved_config = resolve_storage_config_path(config_path)
172 with open(resolved_config, "r", encoding="utf-8") as stream:
173 payload = yaml.safe_load(stream) or {}
174 profiles = payload.get("profiles")
175 if not isinstance(profiles, dict):
176 raise StorageError(f"Storage config has no 'profiles' mapping: {resolved_config}")
177 selected = profile_name or payload.get("default_profile") or DEFAULT_PROFILE_NAME
178 profile = profiles.get(selected)
179 if not isinstance(profile, dict):
180 raise StorageError(f"Storage profile '{selected}' does not exist in {resolved_config}.")
181 remote = profile.get("remote")
182 if not isinstance(remote, str) or not remote.strip():
183 raise StorageError(f"Storage profile '{selected}' requires a non-empty remote.")
184 result = dict(profile)
185 result["name"] = selected
186 result["remote"] = remote.rstrip("/")
187 result["config_path"] = resolved_config
188 try:
189 chunk_size_gib = float(result.get("chunk_size_gib", DEFAULT_CHUNK_SIZE_GIB))
190 except (TypeError, ValueError) as exc:
191 raise StorageError(f"Storage profile '{selected}' chunk_size_gib must be numeric.") from exc
192 if chunk_size_gib <= 0.0:
193 raise StorageError(f"Storage profile '{selected}' chunk_size_gib must be positive.")
194 result["chunk_size_bytes"] = int(chunk_size_gib * 1024 ** 3)
195 return result
196
197
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _remote_join()

str picurv_cli.storage._remote_join ( str  remote,
*str  parts 
)
protected

Join path components without corrupting rclone remote syntax.

Parameters
[in]remoteValue supplied through the remote argument.
[in]partsValue supplied through the parts argument.
Returns
Result produced by this operation.

Definition at line 198 of file storage.py.

198def _remote_join(remote: str, *parts: str) -> str:
199 """!
200 @brief Join path components without corrupting rclone remote syntax.
201 @param[in] remote Value supplied through the `remote` argument.
202 @param[in] parts Value supplied through the `parts` argument.
203 @return Result produced by this operation.
204 """
205 clean_parts = [str(part).strip("/") for part in parts if str(part).strip("/")]
206 suffix = "/".join(clean_parts)
207 if not suffix:
208 return remote
209 if remote.endswith(":"):
210 return remote + suffix
211 return remote.rstrip("/") + "/" + suffix
212
213
Here is the caller graph for this function:

◆ _object_remote()

str picurv_cli.storage._object_remote ( dict  profile,
str  archive_id,
*str  parts 
)
protected

Return the remote path for one immutable archive object.

Parameters
[in]profileValue supplied through the profile argument.
[in]archive_idValue supplied through the archive_id argument.
[in]partsValue supplied through the parts argument.
Returns
Result produced by this operation.

Definition at line 214 of file storage.py.

214def _object_remote(profile: dict, archive_id: str, *parts: str) -> str:
215 """!
216 @brief Return the remote path for one immutable archive object.
217 @param[in] profile Value supplied through the `profile` argument.
218 @param[in] archive_id Value supplied through the `archive_id` argument.
219 @param[in] parts Value supplied through the `parts` argument.
220 @return Result produced by this operation.
221 """
222 return _remote_join(profile["remote"], REMOTE_OBJECTS_DIRECTORY, archive_id, *parts)
223
224
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _run_rclone()

subprocess.CompletedProcess picurv_cli.storage._run_rclone ( list  arguments,
bool   check = True 
)
protected

Invoke rclone through the same argv-based subprocess boundary as other PICurv tools.

Parameters
[in]argumentsValue supplied through the arguments argument.
[in]checkValue supplied through the check argument.
Returns
Result produced by this operation.

Definition at line 225 of file storage.py.

225def _run_rclone(arguments: list, check: bool = True) -> subprocess.CompletedProcess:
226 """!
227 @brief Invoke rclone through the same argv-based subprocess boundary as other PICurv tools.
228 @param[in] arguments Value supplied through the `arguments` argument.
229 @param[in] check Value supplied through the `check` argument.
230 @return Result produced by this operation.
231 """
232 executable = shutil.which("rclone")
233 if not executable:
234 raise StorageError("rclone was not found on PATH. Install/configure rclone before using PICurv storage.")
235 result = subprocess.run(
236 [executable] + [str(item) for item in arguments],
237 text=True,
238 capture_output=True,
239 check=False,
240 )
241 if check and result.returncode != 0:
242 detail = (result.stderr or result.stdout or "unknown rclone error").strip()
243 raise StorageError(f"rclone {' '.join(str(item) for item in arguments[:2])} failed: {detail}")
244 return result
245
246
Here is the caller graph for this function:

◆ _remote_sha256()

str picurv_cli.storage._remote_sha256 ( str  remote_path)
protected

Ask rclone to calculate or retrieve the SHA-256 of one remote object.

Parameters
[in]remote_pathValue supplied through the remote_path argument.
Returns
Result produced by this operation.

Definition at line 247 of file storage.py.

247def _remote_sha256(remote_path: str) -> str:
248 """!
249 @brief Ask rclone to calculate or retrieve the SHA-256 of one remote object.
250 @param[in] remote_path Value supplied through the `remote_path` argument.
251 @return Result produced by this operation.
252 """
253 result = _run_rclone(["hashsum", "SHA-256", remote_path])
254 for line in result.stdout.splitlines():
255 token = line.strip().split(None, 1)[0] if line.strip() else ""
256 if re.fullmatch(r"[0-9a-fA-F]{64}", token):
257 return token.lower()
258 raise StorageError(f"rclone did not return a SHA-256 for {remote_path}.")
259
260
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _upload_verified()

dict picurv_cli.storage._upload_verified ( str  local_path,
str  remote_path 
)
protected

Upload one file, then verify its remote SHA-256.

Parameters
[in]local_pathValue supplied through the local_path argument.
[in]remote_pathValue supplied through the remote_path argument.
Returns
Result produced by this operation.

Definition at line 261 of file storage.py.

261def _upload_verified(local_path: str, remote_path: str) -> dict:
262 """!
263 @brief Upload one file, then verify its remote SHA-256.
264 @param[in] local_path Value supplied through the `local_path` argument.
265 @param[in] remote_path Value supplied through the `remote_path` argument.
266 @return Result produced by this operation.
267 """
268 local_digest = _sha256_file(local_path)
269 _run_rclone(["copyto", local_path, remote_path])
270 remote_digest = _remote_sha256(remote_path)
271 if remote_digest != local_digest:
272 raise StorageError(
273 f"Remote checksum mismatch after upload: {remote_path} "
274 f"(local {local_digest}, remote {remote_digest})."
275 )
276 return {"sha256": local_digest, "stored_bytes": os.path.getsize(local_path)}
277
278
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _read_remote_bytes()

bytes picurv_cli.storage._read_remote_bytes ( str  remote_path)
protected

Read a small remote catalog object through rclone.

Parameters
[in]remote_pathValue supplied through the remote_path argument.
Returns
Result produced by this operation.

Definition at line 279 of file storage.py.

279def _read_remote_bytes(remote_path: str) -> bytes:
280 """!
281 @brief Read a small remote catalog object through rclone.
282 @param[in] remote_path Value supplied through the `remote_path` argument.
283 @return Result produced by this operation.
284 """
285 executable = shutil.which("rclone")
286 if not executable:
287 raise StorageError("rclone was not found on PATH.")
288 result = subprocess.run(
289 [executable, "cat", remote_path], capture_output=True, check=False
290 )
291 if result.returncode != 0:
292 detail = (result.stderr or result.stdout or b"unknown rclone error").decode("utf-8", "replace").strip()
293 raise StorageError(f"Unable to read remote object {remote_path}: {detail}")
294 return result.stdout
295
296
Here is the caller graph for this function:

◆ _load_remote_manifest()

dict picurv_cli.storage._load_remote_manifest ( dict  profile,
str  archive_id,
bool   require_complete = True 
)
protected

Fetch and validate one versioned remote storage manifest.

Parameters
[in]profileValue supplied through the profile argument.
[in]archive_idValue supplied through the archive_id argument.
[in]require_completeValue supplied through the require_complete argument.
Returns
Result produced by this operation.

Definition at line 297 of file storage.py.

297def _load_remote_manifest(profile: dict, archive_id: str, require_complete: bool = True) -> dict:
298 """!
299 @brief Fetch and validate one versioned remote storage manifest.
300 @param[in] profile Value supplied through the `profile` argument.
301 @param[in] archive_id Value supplied through the `archive_id` argument.
302 @param[in] require_complete Value supplied through the `require_complete` argument.
303 @return Result produced by this operation.
304 """
305 if not ARCHIVE_ID_PATTERN.fullmatch(str(archive_id)):
306 raise StorageError(f"Invalid archive ID: {archive_id!r}.")
307 manifest_bytes = _read_remote_bytes(_object_remote(profile, archive_id, REMOTE_MANIFEST_FILENAME))
308 if require_complete:
309 complete = _read_remote_bytes(_object_remote(profile, archive_id, REMOTE_COMPLETE_FILENAME))
310 recorded = complete.decode("ascii", "replace").strip().lower()
311 actual = hashlib.sha256(manifest_bytes).hexdigest()
312 if recorded != actual:
313 raise StorageError(f"Archive {archive_id} has no valid completion marker.")
314 try:
315 manifest = json.loads(manifest_bytes.decode("utf-8"))
316 except (UnicodeDecodeError, ValueError) as exc:
317 raise StorageError(f"Archive {archive_id} has an invalid manifest.") from exc
318 if not isinstance(manifest, dict) or manifest.get("storage_schema_version") != STORAGE_SCHEMA_VERSION:
319 raise StorageError(
320 f"Archive {archive_id} uses unsupported storage schema "
321 f"{manifest.get('storage_schema_version') if isinstance(manifest, dict) else 'unknown'}."
322 )
323 return manifest
324
325
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _state_path()

str picurv_cli.storage._state_path ( str  root_path)
protected

Return the local storage state marker path for an artifact root.

Parameters
[in]root_pathValue supplied through the root_path argument.
Returns
Result produced by this operation.

Definition at line 326 of file storage.py.

326def _state_path(root_path: str) -> str:
327 """!
328 @brief Return the local storage state marker path for an artifact root.
329 @param[in] root_path Value supplied through the `root_path` argument.
330 @return Result produced by this operation.
331 """
332 return os.path.join(os.path.abspath(root_path), STORAGE_STATE_FILENAME)
333
334
Here is the caller graph for this function:

◆ read_storage_state()

picurv_cli.storage.read_storage_state ( str  root_path)

Read the nearest applicable storage marker for a run, study, or study member.

Parameters
[in]root_pathValue supplied through the root_path argument.
Returns
Result produced by this operation.

Definition at line 335 of file storage.py.

335def read_storage_state(root_path: str):
336 """!
337 @brief Read the nearest applicable storage marker for a run, study, or study member.
338 @param[in] root_path Value supplied through the `root_path` argument.
339 @return Result produced by this operation.
340 """
341 root = Path(os.path.abspath(root_path))
342 state = _read_json(_state_path(str(root)))
343 if state:
344 return state
345 # A whole-study archive owns every numbered member. Let ordinary run
346 # workflows see that parent state without treating unrelated ancestors as
347 # storage owners.
348 if root.parent.name == "cases":
349 return _read_json(_state_path(str(root.parent.parent)))
350 return None
351
352
Here is the call graph for this function:
Here is the caller graph for this function:

◆ is_artifact_cold()

bool picurv_cli.storage.is_artifact_cold ( str  root_path)

Return whether a local artifact marker says payload data was pruned.

Parameters
[in]root_pathValue supplied through the root_path argument.
Returns
Result produced by this operation.

Definition at line 353 of file storage.py.

353def is_artifact_cold(root_path: str) -> bool:
354 """!
355 @brief Return whether a local artifact marker says payload data was pruned.
356 @param[in] root_path Value supplied through the `root_path` argument.
357 @return Result produced by this operation.
358 """
359 state = read_storage_state(root_path)
360 return bool(state and state.get("local_pruned"))
361
362
Here is the call graph for this function:

◆ cold_study_members()

list picurv_cli.storage.cold_study_members ( str  study_path)

Return numbered study members whose local payload was pruned.

Parameters
[in]study_pathValue supplied through the study_path argument.
Returns
Result produced by this operation.

Definition at line 363 of file storage.py.

363def cold_study_members(study_path: str) -> list:
364 """!
365 @brief Return numbered study members whose local payload was pruned.
366 @param[in] study_path Value supplied through the `study_path` argument.
367 @return Result produced by this operation.
368 """
369 cases_dir = Path(os.path.abspath(study_path)) / "cases"
370 if not cases_dir.is_dir():
371 return []
372 return [
373 child.name for child in sorted(cases_dir.iterdir())
374 if child.is_dir() and is_artifact_cold(str(child))
375 ]
376
377

◆ require_storage_payload_local()

None picurv_cli.storage.require_storage_payload_local ( str  root_path,
str  operation,
int   checkpoint = None,
  checkpoints = None 
)

Reject a workflow that requires payload currently held in cold storage.

Parameters
[in]root_pathRun or study-member directory checked by an existing workflow.
[in]operationHuman-readable consuming operation.
[in]checkpointOptional single required checkpoint step.
[in]checkpointsOptional iterable of every required checkpoint step.

Definition at line 378 of file storage.py.

383) -> None:
384 """!
385 @brief Reject a workflow that requires payload currently held in cold storage.
386 @param[in] root_path Run or study-member directory checked by an existing workflow.
387 @param[in] operation Human-readable consuming operation.
388 @param[in] checkpoint Optional single required checkpoint step.
389 @param[in] checkpoints Optional iterable of every required checkpoint step.
390 """
391 state = read_storage_state(root_path)
392 if not state or not state.get("local_pruned"):
393 return
394 required_steps = set()
395 if checkpoint is not None:
396 required_steps.add(int(checkpoint))
397 if checkpoints is not None:
398 required_steps.update(int(step) for step in checkpoints)
399 restored = set(state.get("restored_components") or [])
400 missing_steps = sorted(
401 step for step in required_steps if f"checkpoint:{step}" not in restored
402 )
403 if required_steps and not missing_steps:
404 return
405 archive_id = state.get("archive_id", "<archive-id>")
406 if missing_steps and len(missing_steps) <= 8:
407 suffix = "".join(f" --checkpoint {step}" for step in missing_steps)
408 else:
409 # A full restore is clearer than printing hundreds of repeatable selectors.
410 suffix = ""
411 raise StorageError(
412 f"{operation} requires payload archived from {os.path.abspath(root_path)}. Restore it first with:\n"
413 f" picurv storage restore --archive-id {archive_id}{suffix}"
414 )
415
416
Here is the call graph for this function:

◆ storage_state_summary()

dict picurv_cli.storage.storage_state_summary ( str  root_path)

Return a compact storage status for summarize and status commands.

Parameters
[in]root_pathValue supplied through the root_path argument.
Returns
Result produced by this operation.

Definition at line 417 of file storage.py.

417def storage_state_summary(root_path: str) -> dict:
418 """!
419 @brief Return a compact storage status for summarize and status commands.
420 @param[in] root_path Value supplied through the `root_path` argument.
421 @return Result produced by this operation.
422 """
423 state = read_storage_state(root_path)
424 if not state:
425 return {"state": "LOCAL", "archive_id": None, "label": None}
426 if state.get("local_pruned"):
427 status = "PARTIAL" if state.get("restored_components") else "COLD"
428 else:
429 status = "PROTECTED"
430 return {
431 "state": status,
432 "archive_id": state.get("archive_id"),
433 "label": state.get("label"),
434 "profile": state.get("profile"),
435 "remote": state.get("remote"),
436 }
437
438
Here is the call graph for this function:

◆ _validate_case_id()

str picurv_cli.storage._validate_case_id ( str  case_id)
protected

Validate a canonical numbered study-member identifier.

Parameters
[in]case_idValue supplied through the case_id argument.
Returns
Result produced by this operation.

Definition at line 439 of file storage.py.

439def _validate_case_id(case_id: str) -> str:
440 """!
441 @brief Validate a canonical numbered study-member identifier.
442 @param[in] case_id Value supplied through the `case_id` argument.
443 @return Result produced by this operation.
444 """
445 if not re.fullmatch(r"case_\d+", str(case_id or "")):
446 raise StorageError(f"Invalid study case ID {case_id!r}; expected a value such as case_0003.")
447 return str(case_id)
448
449
Here is the caller graph for this function:

◆ resolve_local_storage_targets()

list picurv_cli.storage.resolve_local_storage_targets ( str   run_dir = None,
str   study_dir = None,
  case_ids = None 
)

Resolve explicit run/study selectors into concrete artifact descriptions.

Parameters
[in]run_dirValue supplied through the run_dir argument.
[in]study_dirValue supplied through the study_dir argument.
[in]case_idsValue supplied through the case_ids argument.
Returns
Result produced by this operation.

Definition at line 450 of file storage.py.

450def resolve_local_storage_targets(run_dir: str = None, study_dir: str = None, case_ids=None) -> list:
451 """!
452 @brief Resolve explicit run/study selectors into concrete artifact descriptions.
453 @param[in] run_dir Value supplied through the `run_dir` argument.
454 @param[in] study_dir Value supplied through the `study_dir` argument.
455 @param[in] case_ids Value supplied through the `case_ids` argument.
456 @return Result produced by this operation.
457 """
458 if bool(run_dir) == bool(study_dir):
459 raise StorageError("Select exactly one of --run-dir or --study-dir.")
460 if run_dir:
461 if case_ids:
462 raise StorageError("--case-id is valid only with --study-dir.")
463 root = os.path.abspath(run_dir)
464 if not os.path.isdir(root):
465 raise StorageError(f"Run directory not found: {root}")
466 if not os.path.isdir(os.path.join(root, "config")):
467 raise StorageError(f"Directory does not look like a PICurv run (missing config/): {root}")
468 return [{
469 "artifact_type": "run",
470 "root_path": root,
471 "original_path": root,
472 "run_id": os.path.basename(root),
473 "study_id": None,
474 "case_id": None,
475 }]
476
477 study_root = os.path.abspath(study_dir)
478 if not os.path.isdir(study_root):
479 raise StorageError(f"Study directory not found: {study_root}")
480 if not os.path.isdir(os.path.join(study_root, "cases")):
481 raise StorageError(f"Directory does not look like a PICurv study (missing cases/): {study_root}")
482 if case_ids:
483 targets = []
484 for raw_case_id in case_ids:
485 case_id = _validate_case_id(raw_case_id)
486 case_root = os.path.join(study_root, "cases", case_id)
487 if not os.path.isdir(case_root):
488 raise StorageError(f"Study member not found: {case_root}")
489 targets.append({
490 "artifact_type": "study-case",
491 "root_path": case_root,
492 "original_path": case_root,
493 "run_id": case_id,
494 "study_id": os.path.basename(study_root),
495 "case_id": case_id,
496 "study_path": study_root,
497 })
498 return targets
499 return [{
500 "artifact_type": "study",
501 "root_path": study_root,
502 "original_path": study_root,
503 "run_id": None,
504 "study_id": os.path.basename(study_root),
505 "case_id": None,
506 }]
507
508
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _path_is_within()

bool picurv_cli.storage._path_is_within ( str  root,
str  candidate 
)
protected

Return whether an absolute candidate remains within a root directory.

Parameters
[in]rootValue supplied through the root argument.
[in]candidateValue supplied through the candidate argument.
Returns
Result produced by this operation.

Definition at line 509 of file storage.py.

509def _path_is_within(root: str, candidate: str) -> bool:
510 """!
511 @brief Return whether an absolute candidate remains within a root directory.
512 @param[in] root Value supplied through the `root` argument.
513 @param[in] candidate Value supplied through the `candidate` argument.
514 @return Result produced by this operation.
515 """
516 try:
517 return os.path.commonpath([os.path.abspath(root), os.path.abspath(candidate)]) == os.path.abspath(root)
518 except ValueError:
519 return False
520
521
Here is the caller graph for this function:

◆ _resolve_configured_path()

str picurv_cli.storage._resolve_configured_path ( str  root,
str  value 
)
protected

Resolve runtime-directory syntax using the run directory as working directory.

Parameters
[in]rootValue supplied through the root argument.
[in]valueValue supplied through the value argument.
Returns
Result produced by this operation.

Definition at line 522 of file storage.py.

522def _resolve_configured_path(root: str, value: str) -> str:
523 """!
524 @brief Resolve runtime-directory syntax using the run directory as working directory.
525 @param[in] root Value supplied through the `root` argument.
526 @param[in] value Value supplied through the `value` argument.
527 @return Result produced by this operation.
528 """
529 return os.path.abspath(value if os.path.isabs(value) else os.path.join(root, value))
530
531
Here is the caller graph for this function:

◆ _artifact_runtime_roots()

list picurv_cli.storage._artifact_runtime_roots ( str  root)
protected

Return run-like roots contained by a standalone run or whole study.

Parameters
[in]rootValue supplied through the root argument.
Returns
Result produced by this operation.

Definition at line 532 of file storage.py.

532def _artifact_runtime_roots(root: str) -> list:
533 """!
534 @brief Return run-like roots contained by a standalone run or whole study.
535 @param[in] root Value supplied through the `root` argument.
536 @return Result produced by this operation.
537 """
538 root_path = Path(os.path.abspath(root))
539 case_root = root_path / "cases"
540 if case_root.is_dir():
541 return [
542 str(path) for path in sorted(case_root.glob("case_*"))
543 if path.is_dir()
544 ]
545 return [str(root_path)]
546
547
Here is the caller graph for this function:

◆ _discover_external_paths()

list picurv_cli.storage._discover_external_paths ( str  root)
protected

Report configured data paths that escape the archived directory boundary.

Parameters
[in]rootValue supplied through the root argument.
Returns
Result produced by this operation.

Definition at line 548 of file storage.py.

548def _discover_external_paths(root: str) -> list:
549 """!
550 @brief Report configured data paths that escape the archived directory boundary.
551 @param[in] root Value supplied through the `root` argument.
552 @return Result produced by this operation.
553 """
554 archive_root = os.path.abspath(root)
555 external = []
556 for runtime_root in _artifact_runtime_roots(root):
557 config_dir = os.path.join(runtime_root, "config")
558 source_prefix = os.path.relpath(runtime_root, archive_root).replace(os.sep, "/")
559 source_prefix = "" if source_prefix == "." else source_prefix + ":"
560 monitor_path = os.path.join(config_dir, "monitor.yml")
561 if os.path.isfile(monitor_path):
562 try:
563 with open(monitor_path, "r", encoding="utf-8") as stream:
564 monitor = yaml.safe_load(stream) or {}
565 directories = ((monitor.get("io") or {}).get("directories") or {})
566 if isinstance(directories, dict):
567 for key, value in directories.items():
568 if isinstance(value, str) and value.strip():
569 resolved = _resolve_configured_path(runtime_root, value.strip())
570 if not _path_is_within(archive_root, resolved):
571 external.append({
572 "source": f"{source_prefix}monitor.io.directories.{key}",
573 "path": resolved,
574 })
575 except (OSError, ValueError, TypeError):
576 pass
577 post_path = os.path.join(config_dir, "post.yml")
578 if not os.path.isfile(post_path):
579 continue
580 try:
581 with open(post_path, "r", encoding="utf-8") as stream:
582 post = yaml.safe_load(stream) or {}
583 values = [
584 ("post.io.output_directory", (post.get("io") or {}).get("output_directory")),
585 ("post.source_data.directory", (post.get("source_data") or {}).get("directory")),
586 ]
587 for source, value in values:
588 if not isinstance(value, str) or not value.strip() or value == "<solver_output_dir>":
589 continue
590 resolved = _resolve_configured_path(runtime_root, value.strip())
591 if not _path_is_within(archive_root, resolved):
592 external.append({"source": f"{source_prefix}{source}", "path": resolved})
593 except (OSError, ValueError, TypeError):
594 pass
595 return external
596
597
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _discover_dependencies()

list picurv_cli.storage._discover_dependencies ( str  root)
protected

Discover absolute restart/source paths embedded in generated controls.

Parameters
[in]rootValue supplied through the root argument.
Returns
Result produced by this operation.

Definition at line 598 of file storage.py.

598def _discover_dependencies(root: str) -> list:
599 """!
600 @brief Discover absolute restart/source paths embedded in generated controls.
601 @param[in] root Value supplied through the `root` argument.
602 @return Result produced by this operation.
603 """
604 dependencies = []
605 archive_root = os.path.abspath(root)
606 controls = []
607 for runtime_root in _artifact_runtime_roots(root):
608 controls.extend(sorted(Path(runtime_root).glob("config/*.control")))
609 for control in controls:
610 try:
611 lines = control.read_text(encoding="utf-8", errors="replace").splitlines()
612 except OSError:
613 continue
614 for line in lines:
615 stripped = line.strip()
616 if not stripped.startswith("-restart_dir "):
617 continue
618 try:
619 tokens = __import__("shlex").split(stripped)
620 except ValueError:
621 continue
622 if len(tokens) >= 2 and os.path.isabs(tokens[1]) and not _path_is_within(archive_root, tokens[1]):
623 dependencies.append({"kind": "restart", "path": os.path.abspath(tokens[1])})
624 return dependencies
625
626
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _walk_archive_entries()

list picurv_cli.storage._walk_archive_entries ( str  root)
protected

Enumerate archive entries without following symlinks.

Parameters
[in]rootValue supplied through the root argument.
Returns
Result produced by this operation.

Definition at line 627 of file storage.py.

627def _walk_archive_entries(root: str) -> list:
628 """!
629 @brief Enumerate archive entries without following symlinks.
630 @param[in] root Value supplied through the `root` argument.
631 @return Result produced by this operation.
632 """
633 root_abs = os.path.abspath(root)
634 entries = []
635
636 def visit(directory: str):
637 """!
638 @brief Recursively inventory directory entries without following symlinks.
639 @param[in] directory Value supplied through the `directory` argument.
640 """
641 try:
642 children = sorted(os.scandir(directory), key=lambda item: item.name)
643 except OSError as exc:
644 raise StorageError(f"Unable to inventory {directory}: {exc}") from exc
645 for child in children:
646 if child.name in {STORAGE_STATE_FILENAME, STORAGE_LOCK_FILENAME}:
647 continue
648 rel = os.path.relpath(child.path, root_abs).replace(os.sep, "/")
649 try:
650 stat_result = child.stat(follow_symlinks=False)
651 except OSError as exc:
652 raise StorageError(f"Unable to stat {child.path}: {exc}") from exc
653 if child.is_symlink():
654 entry_type = "symlink"
655 size = 0
656 elif child.is_dir(follow_symlinks=False):
657 entry_type = "directory"
658 size = 0
659 elif child.is_file(follow_symlinks=False):
660 entry_type = "file"
661 size = int(stat_result.st_size)
662 else:
663 raise StorageError(f"Unsupported filesystem entry in artifact: {child.path}")
664 entries.append({
665 "path": rel,
666 "type": entry_type,
667 "size": size,
668 "mode": int(stat_result.st_mode & 0o7777),
669 "mtime_ns": int(stat_result.st_mtime_ns),
670 })
671 if entry_type == "directory":
672 visit(child.path)
673
674 visit(root_abs)
675 return entries
676
677
Here is the caller graph for this function:

◆ _checkpoint_component()

picurv_cli.storage._checkpoint_component ( str  relative_path)
protected

Return a checkpoint component token for a path inside a committed step bundle.

Parameters
[in]relative_pathValue supplied through the relative_path argument.
Returns
Result produced by this operation.

Definition at line 678 of file storage.py.

678def _checkpoint_component(relative_path: str):
679 """!
680 @brief Return a checkpoint component token for a path inside a committed step bundle.
681 @param[in] relative_path Value supplied through the `relative_path` argument.
682 @return Result produced by this operation.
683 """
684 parts = relative_path.split("/")
685 for index, part in enumerate(parts):
686 match = CHECKPOINT_DIRECTORY_PATTERN.fullmatch(part)
687 if match and index > 0 and parts[index - 1] == "checkpoints":
688 return f"checkpoint:{int(match.group(1))}"
689 return None
690
691
Here is the caller graph for this function:

◆ _classify_component()

str picurv_cli.storage._classify_component ( str  relative_path)
protected

Classify one artifact path for packaging and local retention.

Parameters
[in]relative_pathValue supplied through the relative_path argument.
Returns
Result produced by this operation.

Definition at line 692 of file storage.py.

692def _classify_component(relative_path: str) -> str:
693 """!
694 @brief Classify one artifact path for packaging and local retention.
695 @param[in] relative_path Value supplied through the `relative_path` argument.
696 @return Result produced by this operation.
697 """
698 checkpoint = _checkpoint_component(relative_path)
699 if checkpoint:
700 return checkpoint
701 parts = relative_path.split("/")
702 first = parts[0]
703 base = os.path.basename(relative_path)
704 if len(parts) >= 3 and parts[0] == "cases" and re.fullmatch(r"case_\d+", parts[1]):
705 first = parts[2]
706 if first in {"config", "scheduler"} or base in {
707 "manifest.json", "study_manifest.json", "study.yml", "cluster.yml"
708 }:
709 return "metadata"
710 if first == "results":
711 return "results"
712 if first == "logs":
713 return "logs"
714 return "data"
715
716
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _checkpoint_steps()

list picurv_cli.storage._checkpoint_steps ( list  entries)
protected

Return committed checkpoint steps represented in an inventory.

Parameters
[in]entriesValue supplied through the entries argument.
Returns
Result produced by this operation.

Definition at line 717 of file storage.py.

717def _checkpoint_steps(entries: list) -> list:
718 """!
719 @brief Return committed checkpoint steps represented in an inventory.
720 @param[in] entries Value supplied through the `entries` argument.
721 @return Result produced by this operation.
722 """
723 candidates = {}
724 for entry in entries:
725 component = _checkpoint_component(entry["path"])
726 if component:
727 step = int(component.split(":", 1)[1])
728 candidates.setdefault(step, set()).add(os.path.basename(entry["path"]))
729 return sorted(step for step, names in candidates.items() if {"checkpoint.meta", "COMMITTED"} <= names)
730
731
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _find_incomplete_checkpoints()

list picurv_cli.storage._find_incomplete_checkpoints ( list  entries)
protected

Return incomplete checkpoint paths that make archival unsafe.

Parameters
[in]entriesValue supplied through the entries argument.
Returns
Result produced by this operation.

Definition at line 732 of file storage.py.

732def _find_incomplete_checkpoints(entries: list) -> list:
733 """!
734 @brief Return incomplete checkpoint paths that make archival unsafe.
735 @param[in] entries Value supplied through the `entries` argument.
736 @return Result produced by this operation.
737 """
738 return [
739 entry["path"] for entry in entries
740 if any(INCOMPLETE_CHECKPOINT_PATTERN.match(part) for part in entry["path"].split("/"))
741 ]
742
743
Here is the caller graph for this function:

◆ _lock_owner_active()

bool picurv_cli.storage._lock_owner_active ( str  metadata_path)
protected

Conservatively determine whether a solver/post/storage owner marker is active.

Parameters
[in]metadata_pathValue supplied through the metadata_path argument.
Returns
Result produced by this operation.

Definition at line 744 of file storage.py.

744def _lock_owner_active(metadata_path: str) -> bool:
745 """!
746 @brief Conservatively determine whether a solver/post/storage owner marker is active.
747 @param[in] metadata_path Value supplied through the `metadata_path` argument.
748 @return Result produced by this operation.
749 """
750 owner = _read_json(metadata_path)
751 if not owner:
752 return True
753 host = owner.get("host")
754 pid = owner.get("pid")
755 if host and host != socket.gethostname():
756 return True
757 try:
758 pid = int(pid)
759 except (TypeError, ValueError):
760 return True
761 try:
762 os.kill(pid, 0)
763 except ProcessLookupError:
764 return False
765 except (PermissionError, OSError):
766 return True
767 return True
768
769
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _collect_job_ids()

set picurv_cli.storage._collect_job_ids (   payload)
protected

Recursively collect submitted Slurm job IDs from scheduler metadata.

Parameters
[in]payloadValue supplied through the payload argument.
Returns
Result produced by this operation.

Definition at line 770 of file storage.py.

770def _collect_job_ids(payload) -> set:
771 """!
772 @brief Recursively collect submitted Slurm job IDs from scheduler metadata.
773 @param[in] payload Value supplied through the `payload` argument.
774 @return Result produced by this operation.
775 """
776 result = set()
777 if isinstance(payload, dict):
778 if payload.get("submitted") and payload.get("job_id") is not None:
779 result.add(str(payload["job_id"]).strip())
780 for value in payload.values():
781 result.update(_collect_job_ids(value))
782 elif isinstance(payload, list):
783 for value in payload:
784 result.update(_collect_job_ids(value))
785 return {item for item in result if item}
786
787
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _slurm_activity()

dict picurv_cli.storage._slurm_activity ( str  root)
protected

Query live Slurm state for every job recorded below an artifact scheduler directory.

Parameters
[in]rootValue supplied through the root argument.
Returns
Result produced by this operation.

Definition at line 788 of file storage.py.

788def _slurm_activity(root: str) -> dict:
789 """!
790 @brief Query live Slurm state for every job recorded below an artifact scheduler directory.
791 @param[in] root Value supplied through the `root` argument.
792 @return Result produced by this operation.
793 """
794 job_ids = set()
795 scheduler_dirs = [Path(root) / "scheduler"]
796 if (Path(root) / "cases").is_dir():
797 scheduler_dirs.extend((Path(root) / "cases").glob("*/scheduler"))
798 for scheduler in scheduler_dirs:
799 if not scheduler.is_dir():
800 continue
801 for path in scheduler.glob("submission*.json"):
802 job_ids.update(_collect_job_ids(_read_json(str(path))))
803 if not job_ids:
804 return {"job_ids": [], "active": [], "unknown": False}
805 squeue = shutil.which("squeue")
806 if not squeue:
807 return {"job_ids": sorted(job_ids), "active": [], "unknown": True}
808 result = subprocess.run(
809 [squeue, "-h", "-j", ",".join(sorted(job_ids)), "-o", "%i|%T"],
810 text=True,
811 capture_output=True,
812 check=False,
813 )
814 if result.returncode != 0:
815 return {"job_ids": sorted(job_ids), "active": [], "unknown": True}
816 active = []
817 for line in result.stdout.splitlines():
818 if not line.strip():
819 continue
820 job_id, _, state = line.partition("|")
821 active.append({"job_id": job_id.strip(), "state": state.strip() or "UNKNOWN"})
822 return {"job_ids": sorted(job_ids), "active": active, "unknown": False}
823
824
Here is the call graph for this function:
Here is the caller graph for this function:

◆ inspect_artifact()

dict picurv_cli.storage.inspect_artifact ( dict  target,
bool   query_scheduler = True 
)

Build a read-only inventory and lifecycle assessment for one artifact.

Parameters
[in]targetValue supplied through the target argument.
[in]query_schedulerValue supplied through the query_scheduler argument.
Returns
Result produced by this operation.

Definition at line 825 of file storage.py.

825def inspect_artifact(target: dict, query_scheduler: bool = True) -> dict:
826 """!
827 @brief Build a read-only inventory and lifecycle assessment for one artifact.
828 @param[in] target Value supplied through the `target` argument.
829 @param[in] query_scheduler Value supplied through the `query_scheduler` argument.
830 @return Result produced by this operation.
831 """
832 root = target["root_path"]
833 entries = _walk_archive_entries(root)
834 for entry in entries:
835 entry["component"] = _classify_component(entry["path"])
836 lock_paths = []
837 for name in ("post.lock.json", "solver.lock.json"):
838 for candidate in Path(root).glob(f"**/scheduler/{name}"):
839 if _lock_owner_active(str(candidate)):
840 lock_paths.append(os.path.relpath(candidate, root))
841 slurm = _slurm_activity(root) if query_scheduler else {"job_ids": [], "active": [], "unknown": False}
842 state = storage_state_summary(root)
843 return {
844 "target": dict(target),
845 "entries": entries,
846 "file_count": sum(entry["type"] in {"file", "symlink"} for entry in entries),
847 "total_bytes": sum(entry["size"] for entry in entries),
848 "checkpoint_steps": _checkpoint_steps(entries),
849 "incomplete_checkpoints": _find_incomplete_checkpoints(entries),
850 "active_locks": sorted(lock_paths),
851 "slurm": slurm,
852 "external_paths": _discover_external_paths(root),
853 "dependencies": _discover_dependencies(root),
854 "storage": state,
855 }
856
857
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _assert_archive_safe()

None picurv_cli.storage._assert_archive_safe ( dict  inventory)
protected

Refuse to package a changing or scheduler-ambiguous artifact.

Parameters
[in]inventoryValue supplied through the inventory argument.

Definition at line 858 of file storage.py.

858def _assert_archive_safe(inventory: dict) -> None:
859 """!
860 @brief Refuse to package a changing or scheduler-ambiguous artifact.
861 @param[in] inventory Value supplied through the `inventory` argument.
862 """
863 problems = []
864 if inventory["incomplete_checkpoints"]:
865 problems.append("incomplete checkpoint(s): " + ", ".join(inventory["incomplete_checkpoints"][:3]))
866 if inventory["active_locks"]:
867 problems.append("active runtime lock(s): " + ", ".join(inventory["active_locks"]))
868 if inventory["slurm"]["active"]:
869 problems.append(
870 "active Slurm job(s): " + ", ".join(
871 f"{item['job_id']} ({item['state']})" for item in inventory["slurm"]["active"]
872 )
873 )
874 if inventory["slurm"]["unknown"]:
875 problems.append(
876 "recorded Slurm job IDs could not be checked because squeue is unavailable or failed"
877 )
878 if problems:
879 raise StorageError("Artifact is not safe to archive/offload: " + "; ".join(problems) + ".")
880
881
882@contextlib.contextmanager
Here is the caller graph for this function:

◆ storage_operation_lock()

picurv_cli.storage.storage_operation_lock ( str  root_path,
str  operation 
)

Hold an exclusive local storage-operation marker for one artifact.

Parameters
[in]root_pathValue supplied through the root_path argument.
[in]operationValue supplied through the operation argument.

Definition at line 883 of file storage.py.

883def storage_operation_lock(root_path: str, operation: str):
884 """!
885 @brief Hold an exclusive local storage-operation marker for one artifact.
886 @param[in] root_path Value supplied through the `root_path` argument.
887 @param[in] operation Value supplied through the `operation` argument.
888 """
889 root = os.path.abspath(root_path)
890 lock_path = os.path.join(root, STORAGE_LOCK_FILENAME)
891 if os.path.exists(lock_path):
892 if _lock_owner_active(lock_path):
893 raise StorageError(f"Another storage operation owns {lock_path}.")
894 try:
895 os.remove(lock_path)
896 except OSError as exc:
897 raise StorageError(f"Unable to remove stale storage lock {lock_path}: {exc}") from exc
898 payload = {"operation": operation, "pid": os.getpid(), "host": socket.gethostname(), "started_at": _utc_now()}
899 flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
900 try:
901 descriptor = os.open(lock_path, flags, 0o600)
902 with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
903 json.dump(payload, stream, indent=2, sort_keys=True)
904 stream.write("\n")
905 yield
906 finally:
907 try:
908 os.remove(lock_path)
909 except FileNotFoundError:
910 pass
911
912
913@contextlib.contextmanager
Here is the call graph for this function:
Here is the caller graph for this function:

◆ runtime_stage_lock()

picurv_cli.storage.runtime_stage_lock ( str  root_path,
str  stage 
)

Mark a locally executed solver/post stage as active for storage safety.

Parameters
[in]root_pathRun directory used as the runtime working directory.
[in]stageRuntime stage label; storage currently uses this for solver execution.

Definition at line 914 of file storage.py.

914def runtime_stage_lock(root_path: str, stage: str):
915 """!
916 @brief Mark a locally executed solver/post stage as active for storage safety.
917 @param[in] root_path Run directory used as the runtime working directory.
918 @param[in] stage Runtime stage label; storage currently uses this for solver execution.
919 """
920 scheduler = os.path.join(os.path.abspath(root_path), "scheduler")
921 os.makedirs(scheduler, exist_ok=True)
922 lock_path = os.path.join(scheduler, f"{stage}.lock.json")
923 if os.path.exists(lock_path):
924 if _lock_owner_active(lock_path):
925 raise StorageError(f"A {stage} runtime stage already owns {lock_path}.")
926 os.remove(lock_path)
927 payload = {"stage": stage, "pid": os.getpid(), "host": socket.gethostname(), "started_at": _utc_now()}
928 flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
929 descriptor = os.open(lock_path, flags, 0o600)
930 with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
931 json.dump(payload, stream, indent=2, sort_keys=True)
932 stream.write("\n")
933 try:
934 yield
935 finally:
936 try:
937 os.remove(lock_path)
938 except FileNotFoundError:
939 pass
940
941
Here is the call graph for this function:

◆ _select_compression()

str picurv_cli.storage._select_compression ( str  requested,
int  total_bytes,
dict  profile 
)
protected

Resolve automatic or configured compression policy.

Parameters
[in]requestedValue supplied through the requested argument.
[in]total_bytesValue supplied through the total_bytes argument.
[in]profileValue supplied through the profile argument.
Returns
Result produced by this operation.

Definition at line 942 of file storage.py.

942def _select_compression(requested: str, total_bytes: int, profile: dict) -> str:
943 """!
944 @brief Resolve automatic or configured compression policy.
945 @param[in] requested Value supplied through the `requested` argument.
946 @param[in] total_bytes Value supplied through the `total_bytes` argument.
947 @param[in] profile Value supplied through the `profile` argument.
948 @return Result produced by this operation.
949 """
950 selected = requested or profile.get("compression", "auto")
951 selected = str(selected).strip().lower()
952 if selected not in {"auto", "none", "fast", "balanced", "maximum"}:
953 raise StorageError("Compression must be one of: auto, none, fast, balanced, maximum.")
954 if selected != "auto":
955 return selected
956 if total_bytes < AUTO_NO_COMPRESSION_BYTES:
957 return "none"
958 if total_bytes >= AUTO_MAXIMUM_COMPRESSION_BYTES:
959 return "maximum"
960 return "balanced"
961
962
Here is the caller graph for this function:

◆ _chunk_extension()

str picurv_cli.storage._chunk_extension ( str  compression)
protected

Return the archive suffix for a compression policy.

Parameters
[in]compressionValue supplied through the compression argument.
Returns
Result produced by this operation.

Definition at line 963 of file storage.py.

963def _chunk_extension(compression: str) -> str:
964 """!
965 @brief Return the archive suffix for a compression policy.
966 @param[in] compression Value supplied through the `compression` argument.
967 @return Result produced by this operation.
968 """
969 return {"none": ".tar", "fast": ".tar.gz", "balanced": ".tar.gz", "maximum": ".tar.xz"}[compression]
970
971

◆ _build_chunk_specs()

list picurv_cli.storage._build_chunk_specs ( dict  inventory,
int  chunk_size_bytes 
)
protected

Group archive entries into independently transferable component chunks.

Parameters
[in]inventoryValue supplied through the inventory argument.
[in]chunk_size_bytesValue supplied through the chunk_size_bytes argument.
Returns
Result produced by this operation.

Definition at line 972 of file storage.py.

972def _build_chunk_specs(inventory: dict, chunk_size_bytes: int) -> list:
973 """!
974 @brief Group archive entries into independently transferable component chunks.
975 @param[in] inventory Value supplied through the `inventory` argument.
976 @param[in] chunk_size_bytes Value supplied through the `chunk_size_bytes` argument.
977 @return Result produced by this operation.
978 """
979 groups = {}
980 directories = []
981 for entry in inventory["entries"]:
982 if entry["type"] == "directory":
983 directories.append(entry["path"])
984 continue
985 groups.setdefault(entry["component"], []).append(entry)
986 specs = []
987 if directories:
988 specs.append({"component": "metadata", "entries": directories, "uncompressed_bytes": 0})
989 component_order = sorted(groups, key=lambda name: (not name.startswith("checkpoint:"), name))
990 for component in component_order:
991 current = []
992 current_bytes = 0
993 for entry in groups[component]:
994 entry_size = max(1, int(entry["size"]))
995 if current and current_bytes + entry_size > chunk_size_bytes:
996 specs.append({"component": component, "entries": current, "uncompressed_bytes": current_bytes})
997 current = []
998 current_bytes = 0
999 current.append(entry["path"])
1000 current_bytes += int(entry["size"])
1001 if current:
1002 specs.append({"component": component, "entries": current, "uncompressed_bytes": current_bytes})
1003 return specs
1004
1005
Here is the caller graph for this function:

◆ _safe_component_name()

str picurv_cli.storage._safe_component_name ( str  component)
protected

Convert a component token into a portable archive filename fragment.

Parameters
[in]componentValue supplied through the component argument.
Returns
Result produced by this operation.

Definition at line 1006 of file storage.py.

1006def _safe_component_name(component: str) -> str:
1007 """!
1008 @brief Convert a component token into a portable archive filename fragment.
1009 @param[in] component Value supplied through the `component` argument.
1010 @return Result produced by this operation.
1011 """
1012 return re.sub(r"[^A-Za-z0-9_.-]+", "-", component).strip("-") or "data"
1013
1014
Here is the caller graph for this function:

◆ _write_tar_chunk()

None picurv_cli.storage._write_tar_chunk ( str  root,
dict  spec,
str  destination,
str  compression 
)
protected

Package explicitly inventoried entries without following symlinks.

Parameters
[in]rootValue supplied through the root argument.
[in]specValue supplied through the spec argument.
[in]destinationValue supplied through the destination argument.
[in]compressionValue supplied through the compression argument.

Definition at line 1015 of file storage.py.

1015def _write_tar_chunk(root: str, spec: dict, destination: str, compression: str) -> None:
1016 """!
1017 @brief Package explicitly inventoried entries without following symlinks.
1018 @param[in] root Value supplied through the `root` argument.
1019 @param[in] spec Value supplied through the `spec` argument.
1020 @param[in] destination Value supplied through the `destination` argument.
1021 @param[in] compression Value supplied through the `compression` argument.
1022 """
1023 kwargs = {}
1024 if compression == "none":
1025 mode = "w"
1026 elif compression == "fast":
1027 mode = "w:gz"
1028 kwargs["compresslevel"] = 1
1029 elif compression == "balanced":
1030 mode = "w:gz"
1031 kwargs["compresslevel"] = 6
1032 else:
1033 mode = "w:xz"
1034 kwargs["preset"] = 9
1035 with tarfile.open(destination, mode, dereference=False, **kwargs) as archive:
1036 for relative in spec["entries"]:
1037 source = os.path.join(root, *relative.split("/"))
1038 if not os.path.lexists(source):
1039 raise StorageError(f"Artifact changed during packaging; entry disappeared: {source}")
1040 archive.add(source, arcname=relative, recursive=False)
1041
1042
Here is the caller graph for this function:

◆ _capture_study_context()

list picurv_cli.storage._capture_study_context ( dict  target)
protected

Embed small study control-plane files with an individually archived member.

Parameters
[in]targetValue supplied through the target argument.
Returns
Result produced by this operation.

Definition at line 1043 of file storage.py.

1043def _capture_study_context(target: dict) -> list:
1044 """!
1045 @brief Embed small study control-plane files with an individually archived member.
1046 @param[in] target Value supplied through the `target` argument.
1047 @return Result produced by this operation.
1048 """
1049 if target["artifact_type"] != "study-case":
1050 return []
1051 study_root = Path(target["study_path"])
1052 candidates = [
1053 study_root / "study.yml",
1054 study_root / "cluster.yml",
1055 study_root / "study_manifest.json",
1056 study_root / "scheduler" / "case_index.tsv",
1057 study_root / "scheduler" / "submission.json",
1058 ]
1059 captured = []
1060 for path in candidates:
1061 if not path.is_file() or path.stat().st_size > 5 * 1024 * 1024:
1062 continue
1063 captured.append({
1064 "path": path.relative_to(study_root).as_posix(),
1065 "mode": int(path.stat().st_mode & 0o7777),
1066 "content_base64": base64.b64encode(path.read_bytes()).decode("ascii"),
1067 })
1068 return captured
1069
1070
Here is the caller graph for this function:

◆ _config_fingerprints()

dict picurv_cli.storage._config_fingerprints ( str  root)
protected

Hash small canonical YAML inputs stored under an artifact.

Parameters
[in]rootValue supplied through the root argument.
Returns
Result produced by this operation.

Definition at line 1071 of file storage.py.

1071def _config_fingerprints(root: str) -> dict:
1072 """!
1073 @brief Hash small canonical YAML inputs stored under an artifact.
1074 @param[in] root Value supplied through the `root` argument.
1075 @return Result produced by this operation.
1076 """
1077 result = {}
1078 root_path = Path(root)
1079 candidates = set(root_path.glob("config/*.yml"))
1080 candidates.update(root_path.glob("base_configs/*.yml"))
1081 candidates.update(root_path.glob("cases/case_*/config/*.yml"))
1082 for path in sorted(candidates):
1083 result[path.relative_to(root).as_posix()] = _sha256_file(str(path))
1084 for name in ("study.yml", "cluster.yml"):
1085 path = root_path / name
1086 if path.is_file():
1087 result[name] = _sha256_file(str(path))
1088 return result
1089
1090
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _git_provenance()

dict picurv_cli.storage._git_provenance ( str  root)
protected

Record best-effort current source revision and dirty state.

Parameters
[in]rootValue supplied through the root argument.
Returns
Result produced by this operation.

Definition at line 1091 of file storage.py.

1091def _git_provenance(root: str) -> dict:
1092 """!
1093 @brief Record best-effort current source revision and dirty state.
1094 @param[in] root Value supplied through the `root` argument.
1095 @return Result produced by this operation.
1096 """
1097 result = {"commit": None, "dirty": None}
1098 try:
1099 commit = subprocess.run(
1100 ["git", "rev-parse", "HEAD"], cwd=root, text=True, capture_output=True, check=False
1101 )
1102 status = subprocess.run(
1103 ["git", "status", "--porcelain"], cwd=root, text=True, capture_output=True, check=False
1104 )
1105 if commit.returncode == 0:
1106 result["commit"] = commit.stdout.strip()
1107 if status.returncode == 0:
1108 result["dirty"] = bool(status.stdout.strip())
1109 except OSError:
1110 pass
1111 return result
1112
1113
Here is the caller graph for this function:

◆ _parse_tags()

dict picurv_cli.storage._parse_tags (   raw_tags)
protected

Parse repeatable KEY=VALUE tags into deterministic metadata.

Parameters
[in]raw_tagsValue supplied through the raw_tags argument.
Returns
Result produced by this operation.

Definition at line 1114 of file storage.py.

1114def _parse_tags(raw_tags) -> dict:
1115 """!
1116 @brief Parse repeatable KEY=VALUE tags into deterministic metadata.
1117 @param[in] raw_tags Value supplied through the `raw_tags` argument.
1118 @return Result produced by this operation.
1119 """
1120 tags = {}
1121 for item in raw_tags or []:
1122 key, separator, value = str(item).partition("=")
1123 if not separator or not key.strip() or not value.strip():
1124 raise StorageError(f"Invalid tag {item!r}; expected KEY=VALUE.")
1125 tags[key.strip()] = value.strip()
1126 return tags
1127
1128
Here is the caller graph for this function:

◆ build_storage_plan()

dict picurv_cli.storage.build_storage_plan ( dict  target,
dict  profile,
str   compression = None 
)

Build the read-only plan consumed by protect and offload.

Parameters
[in]targetValue supplied through the target argument.
[in]profileValue supplied through the profile argument.
[in]compressionValue supplied through the compression argument.
Returns
Result produced by this operation.

Definition at line 1129 of file storage.py.

1129def build_storage_plan(target: dict, profile: dict, compression: str = None) -> dict:
1130 """!
1131 @brief Build the read-only plan consumed by protect and offload.
1132 @param[in] target Value supplied through the `target` argument.
1133 @param[in] profile Value supplied through the `profile` argument.
1134 @param[in] compression Value supplied through the `compression` argument.
1135 @return Result produced by this operation.
1136 """
1137 inventory = inspect_artifact(target)
1138 selected_compression = _select_compression(compression, inventory["total_bytes"], profile)
1139 specs = _build_chunk_specs(inventory, profile["chunk_size_bytes"])
1140 return {
1141 "inventory": inventory,
1142 "compression": selected_compression,
1143 "chunk_count": len(specs),
1144 "chunks": [
1145 {
1146 "component": spec["component"],
1147 "file_count": len(spec["entries"]),
1148 "uncompressed_bytes": spec["uncompressed_bytes"],
1149 }
1150 for spec in specs
1151 ],
1152 }
1153
1154
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _render_plan()

None picurv_cli.storage._render_plan ( dict  plan)
protected

Print a concise archive/offload plan.

Parameters
[in]planValue supplied through the plan argument.

Definition at line 1155 of file storage.py.

1155def _render_plan(plan: dict) -> None:
1156 """!
1157 @brief Print a concise archive/offload plan.
1158 @param[in] plan Value supplied through the `plan` argument.
1159 """
1160 inventory = plan["inventory"]
1161 target = inventory["target"]
1162 print(f"[INFO] Artifact type : {target['artifact_type']}")
1163 print(f"[INFO] Artifact path : {target['root_path']}")
1164 print(f"[INFO] Local size : {_human_bytes(inventory['total_bytes'])}")
1165 print(f"[INFO] Files : {inventory['file_count']}")
1166 print(f"[INFO] Checkpoints : {len(inventory['checkpoint_steps'])}")
1167 print(f"[INFO] Compression : {plan['compression']}")
1168 print(f"[INFO] Archive chunks: {plan['chunk_count']}")
1169 if inventory["external_paths"]:
1170 print("[WARNING] External configured paths are recorded but are not followed automatically:")
1171 for item in inventory["external_paths"]:
1172 print(f" - {item['source']}: {item['path']}")
1173 if inventory["dependencies"]:
1174 print("[WARNING] External run dependencies:")
1175 for item in inventory["dependencies"]:
1176 print(f" - {item['kind']}: {item['path']}")
1177
1178
Here is the caller graph for this function:

◆ archive_artifact()

dict picurv_cli.storage.archive_artifact ( dict  target,
dict  profile,
str   label = None,
  tags = None,
str   compression = None,
bool   prune_local = False 
)

Package, upload, verify, register, and optionally prune one artifact.

Parameters
[in]targetValue supplied through the target argument.
[in]profileValue supplied through the profile argument.
[in]labelValue supplied through the label argument.
[in]tagsValue supplied through the tags argument.
[in]compressionValue supplied through the compression argument.
[in]prune_localValue supplied through the prune_local argument.
Returns
Result produced by this operation.

Definition at line 1179 of file storage.py.

1180 compression: str = None, prune_local: bool = False) -> dict:
1181 """!
1182 @brief Package, upload, verify, register, and optionally prune one artifact.
1183 @param[in] target Value supplied through the `target` argument.
1184 @param[in] profile Value supplied through the `profile` argument.
1185 @param[in] label Value supplied through the `label` argument.
1186 @param[in] tags Value supplied through the `tags` argument.
1187 @param[in] compression Value supplied through the `compression` argument.
1188 @param[in] prune_local Value supplied through the `prune_local` argument.
1189 @return Result produced by this operation.
1190 """
1191 with storage_operation_lock(target["root_path"], "offload" if prune_local else "protect"):
1192 plan = build_storage_plan(target, profile, compression)
1193 inventory = plan["inventory"]
1194 _assert_archive_safe(inventory)
1195 archive_id = uuid.uuid4().hex
1196 specs = _build_chunk_specs(inventory, profile["chunk_size_bytes"])
1197 staging_parent = profile.get("staging_directory")
1198 if staging_parent:
1199 staging_parent = os.path.abspath(os.path.expanduser(str(staging_parent)))
1200 os.makedirs(staging_parent, exist_ok=True)
1201 print(f"[INFO] Creating archive {archive_id} for {target['root_path']}")
1202 with tempfile.TemporaryDirectory(prefix="picurv-storage-", dir=staging_parent) as staging:
1203 chunks = []
1204 for index, spec in enumerate(specs):
1205 component = _safe_component_name(spec["component"])
1206 filename = f"{index:05d}_{component}{_chunk_extension(plan['compression'])}"
1207 local_chunk = os.path.join(staging, filename)
1208 print(
1209 f"[INFO] Packaging chunk {index + 1}/{len(specs)}: "
1210 f"{spec['component']} ({_human_bytes(spec['uncompressed_bytes'])})"
1211 )
1212 _write_tar_chunk(target["root_path"], spec, local_chunk, plan["compression"])
1213 remote_path = _object_remote(profile, archive_id, "chunks", filename)
1214 verified = _upload_verified(local_chunk, remote_path)
1215 chunks.append({
1216 "name": filename,
1217 "component": spec["component"],
1218 "file_count": len(spec["entries"]),
1219 "uncompressed_bytes": spec["uncompressed_bytes"],
1220 "stored_bytes": verified["stored_bytes"],
1221 "sha256": verified["sha256"],
1222 })
1223
1224 manifest = {
1225 "storage_schema_version": STORAGE_SCHEMA_VERSION,
1226 "archive_id": archive_id,
1227 "created_at": _utc_now(),
1228 "artifact_type": target["artifact_type"],
1229 "run_id": target.get("run_id"),
1230 "study_id": target.get("study_id"),
1231 "case_id": target.get("case_id"),
1232 "label": label or os.path.basename(target["root_path"]),
1233 "tags": _parse_tags(tags),
1234 "original_path": target["original_path"],
1235 "original_study_path": target.get("study_path"),
1236 "profile": profile["name"],
1237 "remote": profile["remote"],
1238 "source_bytes": inventory["total_bytes"],
1239 "source_file_count": inventory["file_count"],
1240 "compression": plan["compression"],
1241 "checkpoint_format_version": KNOWN_CHECKPOINT_VERSION,
1242 "checkpoint_steps": inventory["checkpoint_steps"],
1243 "chunks": chunks,
1244 "config_sha256": _config_fingerprints(target["root_path"]),
1245 "git": _git_provenance(target["root_path"]),
1246 "external_paths": inventory["external_paths"],
1247 "dependencies": inventory["dependencies"],
1248 "study_context": _capture_study_context(target),
1249 "capabilities": {
1250 "restorable": True,
1251 "continuable": bool(inventory["checkpoint_steps"]),
1252 "reprocessable": bool(inventory["checkpoint_steps"]),
1253 "exact_binary_reproduction": False,
1254 },
1255 }
1256 manifest_path = os.path.join(staging, REMOTE_MANIFEST_FILENAME)
1257 _atomic_write_json(manifest_path, manifest)
1258 _upload_verified(manifest_path, _object_remote(profile, archive_id, REMOTE_MANIFEST_FILENAME))
1259 manifest_digest = _sha256_file(manifest_path)
1260 complete_path = os.path.join(staging, REMOTE_COMPLETE_FILENAME)
1261 with open(complete_path, "w", encoding="ascii") as stream:
1262 stream.write(manifest_digest + "\n")
1263 _upload_verified(complete_path, _object_remote(profile, archive_id, REMOTE_COMPLETE_FILENAME))
1264
1265 state = {
1266 "storage_schema_version": STORAGE_SCHEMA_VERSION,
1267 "archive_id": archive_id,
1268 "profile": profile["name"],
1269 "remote": profile["remote"],
1270 "label": manifest["label"],
1271 "archived_at": manifest["created_at"],
1272 "local_pruned": False,
1273 "restored_components": [],
1274 }
1275 _atomic_write_json(_state_path(target["root_path"]), state)
1276 if prune_local:
1277 _prune_archived_payload(target["root_path"], inventory)
1278 state["local_pruned"] = True
1279 state["pruned_at"] = _utc_now()
1280 _atomic_write_json(_state_path(target["root_path"]), state)
1281 print(
1282 f"[SUCCESS] {'Offloaded' if prune_local else 'Protected'} {target['root_path']} "
1283 f"as archive {archive_id}."
1284 )
1285 return manifest
1286
1287
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _prune_archived_payload()

None picurv_cli.storage._prune_archived_payload ( str  root,
dict  inventory 
)
protected

Remove only verified heavy payload while retaining control-plane files.

Parameters
[in]rootValue supplied through the root argument.
[in]inventoryValue supplied through the inventory argument.

Definition at line 1288 of file storage.py.

1288def _prune_archived_payload(root: str, inventory: dict) -> None:
1289 """!
1290 @brief Remove only verified heavy payload while retaining control-plane files.
1291 @param[in] root Value supplied through the `root` argument.
1292 @param[in] inventory Value supplied through the `inventory` argument.
1293 """
1294 removable = {"data"}
1295 removable.update(f"checkpoint:{step}" for step in inventory["checkpoint_steps"])
1296 for entry in sorted(inventory["entries"], key=lambda item: item["path"], reverse=True):
1297 if entry["component"] not in removable or entry["type"] == "directory":
1298 continue
1299 path = os.path.join(root, *entry["path"].split("/"))
1300 try:
1301 if os.path.islink(path) or os.path.isfile(path):
1302 os.remove(path)
1303 except FileNotFoundError:
1304 pass
1305 for entry in sorted(
1306 (item for item in inventory["entries"] if item["type"] == "directory"),
1307 key=lambda item: item["path"].count("/"), reverse=True,
1308 ):
1309 path = os.path.join(root, *entry["path"].split("/"))
1310 try:
1311 os.rmdir(path)
1312 except OSError:
1313 pass
1314
1315
Here is the caller graph for this function:

◆ list_remote_manifests()

list picurv_cli.storage.list_remote_manifests ( dict  profile)

Enumerate completed archive manifests from the remote catalog.

Parameters
[in]profileValue supplied through the profile argument.
Returns
Result produced by this operation.

Definition at line 1316 of file storage.py.

1316def list_remote_manifests(profile: dict) -> list:
1317 """!
1318 @brief Enumerate completed archive manifests from the remote catalog.
1319 @param[in] profile Value supplied through the `profile` argument.
1320 @return Result produced by this operation.
1321 """
1322 root = _remote_join(profile["remote"], REMOTE_OBJECTS_DIRECTORY)
1323 result = _run_rclone([
1324 "lsf", root, "--recursive", "--files-only", "--include", f"*/{REMOTE_MANIFEST_FILENAME}"
1325 ])
1326 manifests = []
1327 for relative in sorted(line.strip() for line in result.stdout.splitlines() if line.strip()):
1328 archive_id = relative.split("/", 1)[0]
1329 if not ARCHIVE_ID_PATTERN.fullmatch(archive_id):
1330 continue
1331 try:
1332 manifests.append(_load_remote_manifest(profile, archive_id))
1333 except StorageError:
1334 continue
1335 return manifests
1336
1337
Here is the call graph for this function:
Here is the caller graph for this function:

◆ verify_remote_archive()

dict picurv_cli.storage.verify_remote_archive ( dict  profile,
str  archive_id 
)

Verify the completion marker and every stored chunk checksum.

Parameters
[in]profileValue supplied through the profile argument.
[in]archive_idValue supplied through the archive_id argument.
Returns
Result produced by this operation.

Definition at line 1338 of file storage.py.

1338def verify_remote_archive(profile: dict, archive_id: str) -> dict:
1339 """!
1340 @brief Verify the completion marker and every stored chunk checksum.
1341 @param[in] profile Value supplied through the `profile` argument.
1342 @param[in] archive_id Value supplied through the `archive_id` argument.
1343 @return Result produced by this operation.
1344 """
1345 manifest = _load_remote_manifest(profile, archive_id)
1346 for chunk in manifest.get("chunks", []):
1347 remote = _object_remote(profile, archive_id, "chunks", chunk["name"])
1348 actual = _remote_sha256(remote)
1349 if actual != chunk.get("sha256"):
1350 raise StorageError(
1351 f"Archive {archive_id} chunk checksum mismatch: {chunk['name']} "
1352 f"(expected {chunk.get('sha256')}, got {actual})."
1353 )
1354 return manifest
1355
1356
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _validate_tar_members()

None picurv_cli.storage._validate_tar_members ( tarfile.TarFile  archive)
protected

Reject archive members that could escape the restore destination.

Parameters
[in]archiveValue supplied through the archive argument.

Definition at line 1357 of file storage.py.

1357def _validate_tar_members(archive: tarfile.TarFile) -> None:
1358 """!
1359 @brief Reject archive members that could escape the restore destination.
1360 @param[in] archive Value supplied through the `archive` argument.
1361 """
1362 link_paths = set()
1363 members = archive.getmembers()
1364 for member in members:
1365 normalized = os.path.normpath(member.name.replace("\\", "/"))
1366 if normalized.startswith("../") or normalized == ".." or os.path.isabs(normalized):
1367 raise StorageError(f"Unsafe archive member path: {member.name}")
1368 for link_path in link_paths:
1369 if normalized == link_path or normalized.startswith(link_path.rstrip("/") + "/"):
1370 raise StorageError(f"Archive member traverses an earlier symlink: {member.name}")
1371 if member.issym() or member.islnk():
1372 if member.islnk():
1373 link_target = os.path.normpath(member.linkname.replace("\\", "/"))
1374 if link_target.startswith("../") or link_target == ".." or os.path.isabs(link_target):
1375 raise StorageError(f"Unsafe archive hardlink target: {member.linkname}")
1376 link_paths.add(normalized)
1377
1378
Here is the caller graph for this function:

◆ _extract_chunk()

None picurv_cli.storage._extract_chunk ( str  path,
str  destination 
)
protected

Safely extract one verified tar chunk into a staging tree.

Parameters
[in]pathValue supplied through the path argument.
[in]destinationValue supplied through the destination argument.

Definition at line 1379 of file storage.py.

1379def _extract_chunk(path: str, destination: str) -> None:
1380 """!
1381 @brief Safely extract one verified tar chunk into a staging tree.
1382 @param[in] path Value supplied through the `path` argument.
1383 @param[in] destination Value supplied through the `destination` argument.
1384 """
1385 with tarfile.open(path, "r:*") as archive:
1386 _validate_tar_members(archive)
1387 try:
1388 archive.extractall(destination, filter="data")
1389 except TypeError:
1390 archive.extractall(destination)
1391
1392
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _restore_study_context()

None picurv_cli.storage._restore_study_context ( dict  manifest,
str  case_destination 
)
protected

Recreate missing study control-plane files around a restored member.

Parameters
[in]manifestValue supplied through the manifest argument.
[in]case_destinationValue supplied through the case_destination argument.

Definition at line 1393 of file storage.py.

1393def _restore_study_context(manifest: dict, case_destination: str) -> None:
1394 """!
1395 @brief Recreate missing study control-plane files around a restored member.
1396 @param[in] manifest Value supplied through the `manifest` argument.
1397 @param[in] case_destination Value supplied through the `case_destination` argument.
1398 """
1399 if manifest.get("artifact_type") != "study-case":
1400 return
1401 study_root = Path(case_destination).parent.parent
1402 for item in manifest.get("study_context", []):
1403 relative = item.get("path")
1404 if not isinstance(relative, str):
1405 continue
1406 destination = study_root.joinpath(*relative.split("/"))
1407 if destination.exists():
1408 continue
1409 destination.parent.mkdir(parents=True, exist_ok=True)
1410 destination.write_bytes(base64.b64decode(item.get("content_base64", "")))
1411 try:
1412 destination.chmod(int(item.get("mode", 0o644)))
1413 except OSError:
1414 pass
1415
1416
Here is the caller graph for this function:

◆ _merge_tree()

None picurv_cli.storage._merge_tree ( str  source,
str  destination 
)
protected

Merge a verified restore tree into a known cold artifact skeleton.

Parameters
[in]sourceValue supplied through the source argument.
[in]destinationValue supplied through the destination argument.

Definition at line 1417 of file storage.py.

1417def _merge_tree(source: str, destination: str) -> None:
1418 """!
1419 @brief Merge a verified restore tree into a known cold artifact skeleton.
1420 @param[in] source Value supplied through the `source` argument.
1421 @param[in] destination Value supplied through the `destination` argument.
1422 """
1423 os.makedirs(destination, exist_ok=True)
1424 for entry in os.scandir(source):
1425 target = os.path.join(destination, entry.name)
1426 if entry.is_symlink():
1427 if os.path.lexists(target):
1428 if os.path.isdir(target) and not os.path.islink(target):
1429 shutil.rmtree(target)
1430 else:
1431 os.remove(target)
1432 os.symlink(os.readlink(entry.path), target)
1433 elif entry.is_dir(follow_symlinks=False):
1434 _merge_tree(entry.path, target)
1435 else:
1436 os.makedirs(os.path.dirname(target), exist_ok=True)
1437 shutil.copy2(entry.path, target)
1438
1439
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _rebase_restored_text_paths()

list picurv_cli.storage._rebase_restored_text_paths ( str  root,
list  replacements 
)
protected

Rebase known generated text artifacts after an explicit relocated restore.

Parameters
[in]rootValue supplied through the root argument.
[in]replacementsValue supplied through the replacements argument.
Returns
Result produced by this operation.

Definition at line 1440 of file storage.py.

1440def _rebase_restored_text_paths(root: str, replacements: list) -> list:
1441 """!
1442 @brief Rebase known generated text artifacts after an explicit relocated restore.
1443 @param[in] root Value supplied through the `root` argument.
1444 @param[in] replacements Value supplied through the `replacements` argument.
1445 @return Result produced by this operation.
1446 """
1447 changed = []
1448 allowed_suffixes = {".control", ".run", ".sbatch", ".json", ".tsv", ".yml", ".yaml"}
1449 for path in Path(root).rglob("*"):
1450 if not path.is_file() or path.is_symlink() or path.stat().st_size > 32 * 1024 * 1024:
1451 continue
1452 if path.suffix.lower() not in allowed_suffixes:
1453 continue
1454 try:
1455 content = path.read_text(encoding="utf-8")
1456 except (OSError, UnicodeDecodeError):
1457 continue
1458 updated = content
1459 for old, new in replacements:
1460 if old and new and old != new:
1461 updated = updated.replace(old, new)
1462 if updated != content:
1463 path.write_text(updated, encoding="utf-8")
1464 changed.append(str(path))
1465 return changed
1466
1467
Here is the caller graph for this function:

◆ restore_archive()

dict picurv_cli.storage.restore_archive ( dict  profile,
str  archive_id,
str   destination = None,
  checkpoints = None,
bool   force = False 
)

Download, verify, extract, and materialize an archive or selected checkpoints.

Parameters
[in]profileValue supplied through the profile argument.
[in]archive_idValue supplied through the archive_id argument.
[in]destinationValue supplied through the destination argument.
[in]checkpointsValue supplied through the checkpoints argument.
[in]forceValue supplied through the force argument.
Returns
Result produced by this operation.

Definition at line 1468 of file storage.py.

1469 checkpoints=None, force: bool = False) -> dict:
1470 """!
1471 @brief Download, verify, extract, and materialize an archive or selected checkpoints.
1472 @param[in] profile Value supplied through the `profile` argument.
1473 @param[in] archive_id Value supplied through the `archive_id` argument.
1474 @param[in] destination Value supplied through the `destination` argument.
1475 @param[in] checkpoints Value supplied through the `checkpoints` argument.
1476 @param[in] force Value supplied through the `force` argument.
1477 @return Result produced by this operation.
1478 """
1479 manifest = _load_remote_manifest(profile, archive_id)
1480 original = os.path.abspath(manifest["original_path"])
1481 destination_abs = os.path.abspath(destination or original)
1482 selected_steps = {int(step) for step in (checkpoints or [])}
1483 chunks = []
1484 for chunk in manifest.get("chunks", []):
1485 component = str(chunk.get("component", ""))
1486 if selected_steps:
1487 if component == "metadata":
1488 chunks.append(chunk)
1489 elif component.startswith("checkpoint:") and int(component.split(":", 1)[1]) in selected_steps:
1490 chunks.append(chunk)
1491 else:
1492 chunks.append(chunk)
1493 if selected_steps:
1494 available = set(manifest.get("checkpoint_steps", []))
1495 missing = selected_steps - available
1496 if missing:
1497 raise StorageError(f"Archive {archive_id} does not contain checkpoint step(s): {sorted(missing)}")
1498 existing_state = read_storage_state(destination_abs) if os.path.isdir(destination_abs) else None
1499 if os.path.exists(destination_abs) and not force:
1500 if not existing_state or existing_state.get("archive_id") != archive_id:
1501 raise StorageError(
1502 f"Restore destination already exists and is not the matching cold artifact: {destination_abs}. "
1503 "Choose --to or use --force after verifying the destination."
1504 )
1505 parent = os.path.dirname(destination_abs)
1506 os.makedirs(parent, exist_ok=True)
1507 temporary = tempfile.mkdtemp(prefix=f".picurv-restore-{archive_id[:8]}-", dir=parent)
1508 materialized = os.path.join(temporary, "materialized")
1509 os.makedirs(materialized)
1510 try:
1511 for index, chunk in enumerate(chunks):
1512 local_chunk = os.path.join(temporary, chunk["name"])
1513 remote_chunk = _object_remote(profile, archive_id, "chunks", chunk["name"])
1514 print(f"[INFO] Restoring chunk {index + 1}/{len(chunks)}: {chunk['component']}")
1515 _run_rclone(["copyto", remote_chunk, local_chunk])
1516 actual = _sha256_file(local_chunk)
1517 if actual != chunk.get("sha256"):
1518 raise StorageError(
1519 f"Downloaded chunk checksum mismatch: {chunk['name']} "
1520 f"(expected {chunk.get('sha256')}, got {actual})."
1521 )
1522 _extract_chunk(local_chunk, materialized)
1523
1524 if os.path.isdir(destination_abs):
1525 _merge_tree(materialized, destination_abs)
1526 else:
1527 os.replace(materialized, destination_abs)
1528 _restore_study_context(manifest, destination_abs)
1529
1530 replacements = [(original, destination_abs)]
1531 original_study = manifest.get("original_study_path")
1532 if original_study and manifest.get("artifact_type") == "study-case":
1533 replacements.append((os.path.abspath(original_study), str(Path(destination_abs).parent.parent)))
1534 rebased = _rebase_restored_text_paths(destination_abs, replacements)
1535 state = {
1536 "storage_schema_version": STORAGE_SCHEMA_VERSION,
1537 "archive_id": archive_id,
1538 "profile": profile["name"],
1539 "remote": profile["remote"],
1540 "label": manifest.get("label"),
1541 "archived_at": manifest.get("created_at"),
1542 "restored_at": _utc_now(),
1543 "local_pruned": bool(selected_steps),
1544 "restored_components": [f"checkpoint:{step}" for step in sorted(selected_steps)],
1545 "relocated_from": original if original != destination_abs else None,
1546 "rebased_files": len(rebased),
1547 }
1548 _atomic_write_json(_state_path(destination_abs), state)
1549 except Exception:
1550 if os.path.isdir(temporary):
1551 shutil.rmtree(temporary, ignore_errors=True)
1552 raise
1553 if os.path.isdir(temporary):
1554 shutil.rmtree(temporary, ignore_errors=True)
1555 print(f"[SUCCESS] Restored archive {archive_id} to {destination_abs}.")
1556 if rebased:
1557 print(f"[INFO] Rebased {len(rebased)} generated text artifact(s) to the restored path.")
1558 return manifest
1559
1560
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _resolve_archive_id_from_args()

str picurv_cli.storage._resolve_archive_id_from_args (   args)
protected

Resolve an explicit archive ID or a local artifact marker.

Parameters
[in]argsValue supplied through the args argument.
Returns
Result produced by this operation.

Definition at line 1561 of file storage.py.

1561def _resolve_archive_id_from_args(args) -> str:
1562 """!
1563 @brief Resolve an explicit archive ID or a local artifact marker.
1564 @param[in] args Value supplied through the `args` argument.
1565 @return Result produced by this operation.
1566 """
1567 archive_id = getattr(args, "archive_id", None)
1568 if archive_id:
1569 return archive_id
1570 targets = resolve_local_storage_targets(
1571 getattr(args, "run_dir", None), getattr(args, "study_dir", None), getattr(args, "case_ids", None)
1572 )
1573 if len(targets) != 1:
1574 raise StorageError("Restore/verify by local marker requires exactly one target.")
1575 state = read_storage_state(targets[0]["root_path"])
1576 if not state or not state.get("archive_id"):
1577 raise StorageError(f"No storage marker with an archive ID exists under {targets[0]['root_path']}.")
1578 return state["archive_id"]
1579
1580
Here is the call graph for this function:
Here is the caller graph for this function:

◆ _render_status()

None picurv_cli.storage._render_status ( dict  inventory)
protected

Render one artifact status row and safety details.

Parameters
[in]inventoryValue supplied through the inventory argument.

Definition at line 1581 of file storage.py.

1581def _render_status(inventory: dict) -> None:
1582 """!
1583 @brief Render one artifact status row and safety details.
1584 @param[in] inventory Value supplied through the `inventory` argument.
1585 """
1586 target = inventory["target"]
1587 storage = inventory["storage"]
1588 activity = "BUSY" if inventory["active_locks"] or inventory["slurm"]["active"] else storage["state"]
1589 identity = target.get("case_id") or target.get("run_id") or target.get("study_id")
1590 print(
1591 f"{identity:<36} {target['artifact_type']:<12} {activity:<10} "
1592 f"{_human_bytes(inventory['total_bytes']):>12} {storage.get('label') or ''}"
1593 )
1594
1595
Here is the caller graph for this function:

◆ storage_setup_workflow()

None picurv_cli.storage.storage_setup_workflow (   args)

Create or update a non-secret workspace storage profile.

Parameters
[in]argsValue supplied through the args argument.

Definition at line 1596 of file storage.py.

1596def storage_setup_workflow(args) -> None:
1597 """!
1598 @brief Create or update a non-secret workspace storage profile.
1599 @param[in] args Value supplied through the `args` argument.
1600 """
1601 config_path = resolve_storage_config_path(getattr(args, "storage_config", None), require=False)
1602 payload = {}
1603 if os.path.isfile(config_path):
1604 with open(config_path, "r", encoding="utf-8") as stream:
1605 payload = yaml.safe_load(stream) or {}
1606 profiles = payload.setdefault("profiles", {})
1607 profile_name = args.profile or DEFAULT_PROFILE_NAME
1608 profile = {
1609 "remote": args.remote.rstrip("/"),
1610 "compression": args.compression,
1611 "chunk_size_gib": args.chunk_size_gib,
1612 }
1613 if args.staging_directory:
1614 profile["staging_directory"] = os.path.abspath(os.path.expanduser(args.staging_directory))
1615 payload["default_profile"] = profile_name
1616 profiles[profile_name] = profile
1617 print(f"[INFO] Storage config : {config_path}")
1618 print(f"[INFO] Profile : {profile_name}")
1619 print(f"[INFO] Remote : {profile['remote']}")
1620 if args.dry_run:
1621 print("[INFO] Dry-run only. No configuration or remote directories were changed.")
1622 return
1623 _run_rclone(["mkdir", _remote_join(profile["remote"], REMOTE_OBJECTS_DIRECTORY)])
1624 os.makedirs(os.path.dirname(config_path), exist_ok=True)
1625 temporary = f"{config_path}.tmp.{os.getpid()}"
1626 with open(temporary, "w", encoding="utf-8") as stream:
1627 yaml.safe_dump(payload, stream, sort_keys=False)
1628 os.replace(temporary, config_path)
1629 print("[SUCCESS] Storage profile configured and remote access verified.")
1630
1631
Here is the call graph for this function:
Here is the caller graph for this function:

◆ storage_status_workflow()

None picurv_cli.storage.storage_status_workflow (   args)

Print local storage and lifecycle status for selected artifacts.

Parameters
[in]argsValue supplied through the args argument.

Definition at line 1632 of file storage.py.

1632def storage_status_workflow(args) -> None:
1633 """!
1634 @brief Print local storage and lifecycle status for selected artifacts.
1635 @param[in] args Value supplied through the `args` argument.
1636 """
1637 if args.study_dir and not args.case_ids:
1638 study_root = os.path.abspath(args.study_dir)
1639 case_ids = [
1640 path.name for path in sorted((Path(study_root) / "cases").glob("case_*")) if path.is_dir()
1641 ]
1642 targets = resolve_local_storage_targets(None, study_root, case_ids) if case_ids else resolve_local_storage_targets(None, study_root)
1643 else:
1644 targets = resolve_local_storage_targets(args.run_dir, args.study_dir, args.case_ids)
1645 inventories = [inspect_artifact(target) for target in targets]
1646 if args.output_format == "json":
1647 serializable = []
1648 for item in inventories:
1649 copy_item = dict(item)
1650 copy_item.pop("entries", None)
1651 serializable.append(copy_item)
1652 print(json.dumps(serializable, indent=2, sort_keys=True))
1653 return
1654 print(f"{'ARTIFACT':<36} {'TYPE':<12} {'STATE':<10} {'LOCAL SIZE':>12} LABEL")
1655 for inventory in inventories:
1656 _render_status(inventory)
1657
1658
Here is the call graph for this function:
Here is the caller graph for this function:

◆ storage_plan_workflow()

None picurv_cli.storage.storage_plan_workflow (   args)

Render a read-only packaging and safety plan.

Parameters
[in]argsValue supplied through the args argument.

Definition at line 1659 of file storage.py.

1659def storage_plan_workflow(args) -> None:
1660 """!
1661 @brief Render a read-only packaging and safety plan.
1662 @param[in] args Value supplied through the `args` argument.
1663 """
1664 profile = load_storage_profile(args.profile, args.storage_config)
1665 targets = resolve_local_storage_targets(args.run_dir, args.study_dir, args.case_ids)
1666 for index, target in enumerate(targets):
1667 if index:
1668 print()
1669 plan = build_storage_plan(target, profile, args.compression)
1670 _render_plan(plan)
1671 _assert_archive_safe(plan["inventory"])
1672
1673
Here is the call graph for this function:
Here is the caller graph for this function:

◆ storage_archive_workflow()

None picurv_cli.storage.storage_archive_workflow (   args,
bool  prune_local 
)

Execute protect or offload for one or more explicit local targets.

Parameters
[in]argsValue supplied through the args argument.
[in]prune_localValue supplied through the prune_local argument.

Definition at line 1674 of file storage.py.

1674def storage_archive_workflow(args, prune_local: bool) -> None:
1675 """!
1676 @brief Execute protect or offload for one or more explicit local targets.
1677 @param[in] args Value supplied through the `args` argument.
1678 @param[in] prune_local Value supplied through the `prune_local` argument.
1679 """
1680 profile = load_storage_profile(args.profile, args.storage_config)
1681 targets = resolve_local_storage_targets(args.run_dir, args.study_dir, args.case_ids)
1682 for target in targets:
1683 if args.dry_run:
1684 _render_plan(build_storage_plan(target, profile, args.compression))
1685 print("[INFO] Dry-run only. No files were packaged, uploaded, or pruned.")
1686 continue
1687 archive_artifact(
1688 target,
1689 profile,
1690 label=args.label,
1691 tags=args.tags,
1692 compression=args.compression,
1693 prune_local=prune_local,
1694 )
1695
1696
Here is the call graph for this function:
Here is the caller graph for this function:

◆ storage_restore_workflow()

None picurv_cli.storage.storage_restore_workflow (   args)

Restore a remote archive by globally unique ID or local marker.

Parameters
[in]argsValue supplied through the args argument.

Definition at line 1697 of file storage.py.

1697def storage_restore_workflow(args) -> None:
1698 """!
1699 @brief Restore a remote archive by globally unique ID or local marker.
1700 @param[in] args Value supplied through the `args` argument.
1701 """
1702 profile = load_storage_profile(args.profile, args.storage_config)
1703 archive_id = _resolve_archive_id_from_args(args)
1704 restore_archive(
1705 profile,
1706 archive_id,
1707 destination=args.destination,
1708 checkpoints=args.checkpoints,
1709 force=args.force,
1710 )
1711
1712
Here is the call graph for this function:
Here is the caller graph for this function:

◆ storage_verify_workflow()

None picurv_cli.storage.storage_verify_workflow (   args)

Verify all remote chunks for one archive.

Parameters
[in]argsValue supplied through the args argument.

Definition at line 1713 of file storage.py.

1713def storage_verify_workflow(args) -> None:
1714 """!
1715 @brief Verify all remote chunks for one archive.
1716 @param[in] args Value supplied through the `args` argument.
1717 """
1718 profile = load_storage_profile(args.profile, args.storage_config)
1719 archive_id = _resolve_archive_id_from_args(args)
1720 manifest = verify_remote_archive(profile, archive_id)
1721 print(
1722 f"[SUCCESS] Archive {archive_id} is complete; verified {len(manifest.get('chunks', []))} chunk(s)."
1723 )
1724
1725
Here is the call graph for this function:
Here is the caller graph for this function:

◆ storage_list_workflow()

None picurv_cli.storage.storage_list_workflow (   args)

Search the remote manifest catalog without local artifact state.

Parameters
[in]argsValue supplied through the args argument.

Definition at line 1726 of file storage.py.

1726def storage_list_workflow(args) -> None:
1727 """!
1728 @brief Search the remote manifest catalog without local artifact state.
1729 @param[in] args Value supplied through the `args` argument.
1730 """
1731 profile = load_storage_profile(args.profile, args.storage_config)
1732 manifests = list_remote_manifests(profile)
1733 query = str(args.search or "").lower()
1734 if query:
1735 manifests = [
1736 item for item in manifests
1737 if query in " ".join(
1738 str(item.get(key, "")) for key in ("archive_id", "label", "run_id", "study_id", "case_id", "tags")
1739 ).lower()
1740 ]
1741 if args.output_format == "json":
1742 print(json.dumps(manifests, indent=2, sort_keys=True))
1743 return
1744 print(f"{'ARCHIVE ID':<34} {'TYPE':<12} {'IDENTITY':<32} LABEL")
1745 for item in manifests:
1746 identity = item.get("case_id") or item.get("run_id") or item.get("study_id") or "-"
1747 print(f"{item['archive_id']:<34} {item.get('artifact_type', '-'):<12} {identity:<32} {item.get('label', '')}")
1748
1749
Here is the call graph for this function:
Here is the caller graph for this function:

◆ storage_show_workflow()

None picurv_cli.storage.storage_show_workflow (   args)

Show one complete remote archive manifest.

Parameters
[in]argsValue supplied through the args argument.

Definition at line 1750 of file storage.py.

1750def storage_show_workflow(args) -> None:
1751 """!
1752 @brief Show one complete remote archive manifest.
1753 @param[in] args Value supplied through the `args` argument.
1754 """
1755 profile = load_storage_profile(args.profile, args.storage_config)
1756 manifest = _load_remote_manifest(profile, args.archive_id)
1757 print(json.dumps(manifest, indent=2, sort_keys=True))
1758
1759
Here is the call graph for this function:
Here is the caller graph for this function:

◆ storage_workflow()

None picurv_cli.storage.storage_workflow (   args)

Dispatch nested storage actions using existing PICurv workflow conventions.

Parameters
[in]argsValue supplied through the args argument.

Definition at line 1760 of file storage.py.

1760def storage_workflow(args) -> None:
1761 """!
1762 @brief Dispatch nested storage actions using existing PICurv workflow conventions.
1763 @param[in] args Value supplied through the `args` argument.
1764 """
1765 try:
1766 action = args.storage_action
1767 if action == "setup":
1768 storage_setup_workflow(args)
1769 elif action == "status":
1770 storage_status_workflow(args)
1771 elif action == "plan":
1772 storage_plan_workflow(args)
1773 elif action == "protect":
1774 storage_archive_workflow(args, prune_local=False)
1775 elif action == "offload":
1776 storage_archive_workflow(args, prune_local=True)
1777 elif action == "restore":
1778 storage_restore_workflow(args)
1779 elif action == "verify":
1780 storage_verify_workflow(args)
1781 elif action == "list":
1782 storage_list_workflow(args)
1783 elif action == "show":
1784 storage_show_workflow(args)
1785 else:
1786 raise StorageError(f"Unsupported storage action: {action}")
1787 except StorageError as exc:
1788 print(f"[FATAL] {exc}", file=sys.stderr)
1789 raise SystemExit(1)
1790
1791
Here is the call graph for this function:

◆ add_storage_parser()

argparse.ArgumentParser picurv_cli.storage.add_storage_parser (   subparsers)

Attach the nested storage command parser to PICurv's top-level parser.

Parameters
[in]subparsersValue supplied through the subparsers argument.
Returns
Result produced by this operation.

Definition at line 1792 of file storage.py.

1792def add_storage_parser(subparsers) -> argparse.ArgumentParser:
1793 """!
1794 @brief Attach the nested storage command parser to PICurv's top-level parser.
1795 @param[in] subparsers Value supplied through the `subparsers` argument.
1796 @return Result produced by this operation.
1797 """
1798 parser = subparsers.add_parser(
1799 "storage",
1800 help="Protect, offload, inspect, verify, and restore run/study artifacts.",
1801 formatter_class=argparse.RawTextHelpFormatter,
1802 description=(
1803 "Manage PICurv run and study data through a configured rclone remote.\n"
1804 "Remote archives are checksum-verified before local payload can be pruned.\n\n"
1805 "Examples:\n"
1806 " picurv storage setup --remote labstore:picurv-data\n"
1807 " picurv storage status --run-dir runs/my_run\n"
1808 " picurv storage protect --run-dir runs/my_run --label 'baseline'\n"
1809 " picurv storage offload --study-dir studies/my_study --case-id case_0003\n"
1810 " picurv storage list --search '64-grid'\n"
1811 " picurv storage restore --archive-id <id>"
1812 ),
1813 epilog="Use `picurv storage <action> --help` for action-specific controls.",
1814 )
1815 actions = parser.add_subparsers(dest="storage_action", required=True, help="Storage action")
1816
1817 setup = actions.add_parser("setup", help="Configure a non-secret rclone storage profile.")
1818 setup.add_argument("--remote", required=True, help="Rclone remote and base path, such as labstore:picurv-data.")
1819 setup.add_argument("--profile", default=DEFAULT_PROFILE_NAME, help="Profile name (default: archive).")
1820 setup.add_argument("--storage-config", help=f"Storage YAML path (default: ./{STORAGE_CONFIG_FILENAME}).")
1821 setup.add_argument("--compression", choices=["auto", "none", "fast", "balanced", "maximum"], default="auto")
1822 setup.add_argument("--chunk-size-gib", type=float, default=DEFAULT_CHUNK_SIZE_GIB)
1823 setup.add_argument("--staging-directory", help="Optional local directory for one archive chunk at a time.")
1824 setup.add_argument("--dry-run", action="store_true")
1825
1826 def add_profile_options(action_parser):
1827 """!
1828 @brief Attach shared storage-profile selectors to one action parser.
1829 @param[in] action_parser Value supplied through the `action_parser` argument.
1830 """
1831 action_parser.add_argument("--profile", help="Configured storage profile name.")
1832 action_parser.add_argument("--storage-config", help="Explicit storage YAML path.")
1833
1834 def add_local_target(action_parser, require=True):
1835 """!
1836 @brief Attach the standard run-or-study target selectors to one action parser.
1837 @param[in] action_parser Value supplied through the `action_parser` argument.
1838 @param[in] require Value supplied through the `require` argument.
1839 """
1840 group = action_parser.add_mutually_exclusive_group(required=require)
1841 group.add_argument("--run-dir", help="Standalone run directory.")
1842 group.add_argument("--study-dir", help="Sweep study directory.")
1843 action_parser.add_argument(
1844 "--case-id", dest="case_ids", action="append",
1845 help="One numbered study member, such as case_0003; repeat to select several.",
1846 )
1847
1848 status = actions.add_parser("status", help="Show local, protected, cold, and busy artifact state.")
1849 add_local_target(status)
1850 status.add_argument("--format", dest="output_format", choices=["text", "json"], default="text")
1851
1852 plan = actions.add_parser("plan", help="Show packaging, dependencies, and safety checks without writing.")
1853 add_local_target(plan)
1854 add_profile_options(plan)
1855 plan.add_argument("--compression", choices=["auto", "none", "fast", "balanced", "maximum"])
1856
1857 for name, help_text in (
1858 ("protect", "Upload and verify an archive while retaining all local files."),
1859 ("offload", "Upload and verify an archive, then prune heavy local payload."),
1860 ):
1861 action_parser = actions.add_parser(name, help=help_text)
1862 add_local_target(action_parser)
1863 add_profile_options(action_parser)
1864 action_parser.add_argument("--label", help="Human-readable searchable label.")
1865 action_parser.add_argument("--tag", dest="tags", action="append", help="Repeatable KEY=VALUE catalog tag.")
1866 action_parser.add_argument("--compression", choices=["auto", "none", "fast", "balanced", "maximum"])
1867 action_parser.add_argument("--dry-run", action="store_true")
1868
1869 restore = actions.add_parser("restore", help="Restore a complete archive or selected checkpoints.")
1870 restore_source = restore.add_mutually_exclusive_group(required=True)
1871 restore_source.add_argument("--archive-id", help="Globally unique remote archive ID.")
1872 restore_source.add_argument("--run-dir", help="Cold run containing a local storage marker.")
1873 restore_source.add_argument("--study-dir", help="Cold study containing a local storage marker.")
1874 restore.add_argument("--case-id", dest="case_ids", action="append")
1875 add_profile_options(restore)
1876 restore.add_argument("--to", dest="destination", help="Optional alternate restore destination.")
1877 restore.add_argument("--checkpoint", dest="checkpoints", action="append", type=int)
1878 restore.add_argument("--force", action="store_true", help="Allow merge into a non-matching existing destination.")
1879
1880 verify = actions.add_parser("verify", help="Verify a remote archive completion marker and chunk checksums.")
1881 verify_source = verify.add_mutually_exclusive_group(required=True)
1882 verify_source.add_argument("--archive-id")
1883 verify_source.add_argument("--run-dir")
1884 verify_source.add_argument("--study-dir")
1885 verify.add_argument("--case-id", dest="case_ids", action="append")
1886 add_profile_options(verify)
1887
1888 list_parser = actions.add_parser("list", help="List/search remote archives without local directories.")
1889 add_profile_options(list_parser)
1890 list_parser.add_argument("--search", help="Case-insensitive search across IDs, labels, identities, and tags.")
1891 list_parser.add_argument("--format", dest="output_format", choices=["text", "json"], default="text")
1892
1893 show = actions.add_parser("show", help="Print the complete manifest for one archive.")
1894 show.add_argument("--archive-id", required=True)
1895 add_profile_options(show)
1896 return parser

Variable Documentation

◆ STORAGE_CONFIG_FILENAME

str picurv_cli.storage.STORAGE_CONFIG_FILENAME = ".picurv-storage.yml"

Definition at line 33 of file storage.py.

◆ STORAGE_STATE_FILENAME

str picurv_cli.storage.STORAGE_STATE_FILENAME = ".picurv-storage.json"

Definition at line 34 of file storage.py.

◆ STORAGE_LOCK_FILENAME

str picurv_cli.storage.STORAGE_LOCK_FILENAME = ".picurv-storage.lock.json"

Definition at line 35 of file storage.py.

◆ STORAGE_SCHEMA_VERSION

int picurv_cli.storage.STORAGE_SCHEMA_VERSION = 1

Definition at line 36 of file storage.py.

◆ REMOTE_OBJECTS_DIRECTORY

str picurv_cli.storage.REMOTE_OBJECTS_DIRECTORY = "objects"

Definition at line 37 of file storage.py.

◆ REMOTE_MANIFEST_FILENAME

str picurv_cli.storage.REMOTE_MANIFEST_FILENAME = "manifest.json"

Definition at line 38 of file storage.py.

◆ REMOTE_COMPLETE_FILENAME

str picurv_cli.storage.REMOTE_COMPLETE_FILENAME = "COMPLETE"

Definition at line 39 of file storage.py.

◆ DEFAULT_PROFILE_NAME

str picurv_cli.storage.DEFAULT_PROFILE_NAME = "archive"

Definition at line 40 of file storage.py.

◆ DEFAULT_CHUNK_SIZE_GIB

float picurv_cli.storage.DEFAULT_CHUNK_SIZE_GIB = 8.0

Definition at line 41 of file storage.py.

◆ AUTO_NO_COMPRESSION_BYTES

int picurv_cli.storage.AUTO_NO_COMPRESSION_BYTES = 256 * 1024 * 1024

Definition at line 42 of file storage.py.

◆ AUTO_MAXIMUM_COMPRESSION_BYTES

int picurv_cli.storage.AUTO_MAXIMUM_COMPRESSION_BYTES = 20 * 1024 * 1024 * 1024

Definition at line 43 of file storage.py.

◆ CHECKPOINT_DIRECTORY_PATTERN

picurv_cli.storage.CHECKPOINT_DIRECTORY_PATTERN = re.compile(r"^step_(\d{12})$")

Definition at line 44 of file storage.py.

◆ INCOMPLETE_CHECKPOINT_PATTERN

picurv_cli.storage.INCOMPLETE_CHECKPOINT_PATTERN = re.compile(r"^\.step_\d{12}\.incomplete\.")

Definition at line 45 of file storage.py.

◆ ARCHIVE_ID_PATTERN

picurv_cli.storage.ARCHIVE_ID_PATTERN = re.compile(r"^[0-9a-f]{32}$")

Definition at line 46 of file storage.py.

◆ KNOWN_CHECKPOINT_VERSION

int picurv_cli.storage.KNOWN_CHECKPOINT_VERSION = 1

Definition at line 47 of file storage.py.