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

Functions

list _completed_study_members (str study_root)
 Select the study members that are finished and idle.
 
list _select_completed_case_ids (str study_root)
 Resolve --completed into member ids, raising if none qualify.
 
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, str workspace=None, bool include_inputs=False, bool completed=False)
 Resolve explicit run/study/workspace selectors into 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 explicit external-reference descriptors inside an artifact.
 
list _discover_dependencies (str root)
 Discover absolute restart/source paths embedded in generated controls.
 
list _walk_archive_entries (str root, excluded_roots=())
 Enumerate archive entries without following symlinks.
 
dict _artifact_component_layout (str root, str artifact_type)
 Read from the artifact's own manifests where each component lives.
 
 _checkpoint_step_from_bundle (str bundle_root)
 Read a committed checkpoint's own recorded step number.
 
str _classify_workspace_component (str relative_path)
 Classify one workspace-owned path for packaging and retention.
 
str _classify_component (str relative_path, dict layout=None)
 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.
 
dict inspect_artifact (dict target, bool query_scheduler=True)
 Build a read-only inventory and lifecycle assessment for one artifact.
 
str _inventory_fingerprint (dict inventory)
 Stable digest of the file set an archive would package.
 

Variables

dict FALLBACK_COMPONENT_PATHS
 

Function Documentation

◆ _completed_study_members()

list picurv_cli.storage.inventory._completed_study_members ( str  study_root)
protected

Select the study members that are finished and idle.

"Finished" is deliberately conservative: a member qualifies only when it holds at least one committed checkpoint, is writing none, and has no active lock or scheduler job. Anything ambiguous is skipped rather than archived mid-flight.

Parameters
[in]study_rootStudy directory to scan.
Returns
Sorted member ids.

Definition at line 46 of file inventory.py.

46def _completed_study_members(study_root: str) -> list:
47 """!
48 @brief Select the study members that are finished and idle.
49
50 @details "Finished" is deliberately conservative: a member qualifies only when it
51 holds at least one committed checkpoint, is writing none, and has no
52 active lock or scheduler job. Anything ambiguous is skipped rather than
53 archived mid-flight.
54 @param[in] study_root Study directory to scan.
55 @return Sorted member ids.
56 """
57 cases_dir = os.path.join(study_root, "cases")
58 study_identity = read_artifact_identity(study_root)
59 selected = []
60 for name in sorted(os.listdir(cases_dir)):
61 member = os.path.join(cases_dir, name)
62 if not os.path.isdir(member):
63 continue
64 # A member is a member because it carries a run manifest, not because its
65 # directory is named `case_NNNN`. The name pattern is the fallback for a
66 # member staged before manifests recorded study membership.
67 member_identity = read_artifact_identity(member)
68 if member_identity["identity_source"] != "manifest" and not re.fullmatch(r"case_\d+", name):
69 continue
70 target = {
71 "artifact_type": "study-case", "root_path": member, "original_path": member,
72 "run_id": member_identity["run_id"] or name,
73 "study_id": study_identity["study_id"],
74 "case_id": member_identity["case_id"] or name,
75 "study_path": study_root,
76 "identity_source": member_identity["identity_source"],
77 }
78 try:
79 inventory = inspect_artifact(target)
80 _assert_archive_safe(inventory)
81 except StorageError:
82 continue
83 if inventory["checkpoint_steps"]:
84 selected.append(name)
85 return selected
86
87

◆ _select_completed_case_ids()

list picurv_cli.storage.inventory._select_completed_case_ids ( str  study_root)
protected

Resolve --completed into member ids, raising if none qualify.

Parameters
[in]study_rootStudy directory to scan.
Returns
Sorted member ids; never empty.

Definition at line 88 of file inventory.py.

88def _select_completed_case_ids(study_root: str) -> list:
89 """!
90 @brief Resolve `--completed` into member ids, raising if none qualify.
91 @param[in] study_root Study directory to scan.
92 @return Sorted member ids; never empty.
93 """
94 case_ids = _completed_study_members(study_root)
95 if not case_ids:
96 raise StorageError(
97 f"No completed member found in {study_root}. A member is complete when it "
98 "holds a committed checkpoint and no run is active on it."
99 )
100 print("[INFO] Selected completed member(s): " + ", ".join(case_ids))
101 return case_ids
102
103

◆ _validate_case_id()

str picurv_cli.storage.inventory._validate_case_id ( str  case_id)
protected

Validate a canonical numbered study-member identifier.

This checks a value the user typed before it becomes a path segment, so it stays a pattern match on purpose: it is what stops --case-id ../.. from resolving outside the study. What a member is is still decided by its manifest, not by this.

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

Definition at line 104 of file inventory.py.

104def _validate_case_id(case_id: str) -> str:
105 """!
106 @brief Validate a canonical numbered study-member identifier.
107
108 @details This checks a value the user typed before it becomes a path segment, so it
109 stays a pattern match on purpose: it is what stops `--case-id ../..` from
110 resolving outside the study. What a member *is* is still decided by its
111 manifest, not by this.
112 @param[in] case_id Value supplied through the `case_id` argument.
113 @return Result produced by this operation.
114 """
115 if not re.fullmatch(r"case_\d+", str(case_id or "")):
116 raise StorageError(f"Invalid study case ID {case_id!r}; expected a value such as case_0003.")
117 return str(case_id)
118
119

◆ resolve_local_storage_targets()

list picurv_cli.storage.inventory.resolve_local_storage_targets ( str   run_dir = None,
str   study_dir = None,
  case_ids = None,
str   workspace = None,
bool   include_inputs = False,
bool   completed = False 
)

Resolve explicit run/study/workspace selectors into 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.
[in]workspaceWorkspace root to protect as its own artifact.
[in]include_inputsWhether workspace protection covers user-supplied inputs.
[in]completedSelect every finished study member instead of naming them.
Returns
Result produced by this operation.

Definition at line 120 of file inventory.py.

122 completed: bool = False) -> list:
123 """!
124 @brief Resolve explicit run/study/workspace selectors into artifact descriptions.
125 @param[in] run_dir Value supplied through the `run_dir` argument.
126 @param[in] study_dir Value supplied through the `study_dir` argument.
127 @param[in] case_ids Value supplied through the `case_ids` argument.
128 @param[in] workspace Workspace root to protect as its own artifact.
129 @param[in] include_inputs Whether workspace protection covers user-supplied inputs.
130 @param[in] completed Select every finished study member instead of naming them.
131 @return Result produced by this operation.
132 """
133 selectors = [bool(run_dir), bool(study_dir), bool(workspace)]
134 if sum(selectors) != 1:
135 raise StorageError("Select exactly one of --run-dir, --study-dir, or --workspace.")
136 if include_inputs and not workspace:
137 raise StorageError("--include-inputs is valid only with --workspace.")
138 if workspace:
139 if case_ids:
140 raise StorageError("--case-id is valid only with --study-dir.")
141 root = os.path.abspath(workspace)
142 if not os.path.isfile(os.path.join(root, WORKSPACE_CONFIG_FILENAME)):
143 raise StorageError(
144 f"Directory is not an initialized PICurv workspace (missing "
145 f"{WORKSPACE_CONFIG_FILENAME}): {root}"
146 )
147 identity = {}
148 try:
149 with open(os.path.join(root, WORKSPACE_CONFIG_FILENAME), "r", encoding="utf-8") as stream:
150 identity = (yaml.safe_load(stream) or {}).get("workspace") or {}
151 except (OSError, ValueError):
152 identity = {}
153 return [{
154 "artifact_type": "workspace",
155 "root_path": root,
156 "original_path": root,
157 "run_id": None,
158 "study_id": None,
159 "case_id": None,
160 "workspace_id": identity.get("id") or os.path.basename(root),
161 "include_inputs": bool(include_inputs),
162 }]
163 if run_dir:
164 if case_ids:
165 raise StorageError("--case-id is valid only with --study-dir.")
166 root = os.path.abspath(run_dir)
167 if not os.path.isdir(root):
168 raise StorageError(f"Run directory not found: {root}")
169 # The run's own manifest says what it is and what it is called. A directory
170 # with no manifest is still accepted - runs staged by an older release have
171 # none - but it must at least carry the config snapshot every run writes, and
172 # the fallback identity is recorded rather than passed off as the real one.
173 identity = read_artifact_identity(root)
174 if identity["identity_source"] != "manifest" and not os.path.isdir(
175 os.path.join(root, "config")):
176 raise StorageError(
177 f"Directory does not look like a PICurv run (no {RUN_MANIFEST_FILENAME} "
178 f"and no config/): {root}"
179 )
180 if identity["artifact_type"] == "study":
181 raise StorageError(f"That is a study, not a run; use --study-dir: {root}")
182 return [{
183 "artifact_type": "run",
184 "root_path": root,
185 "original_path": root,
186 "run_id": identity["run_id"],
187 "study_id": identity["study_id"] if identity["case_id"] else None,
188 "case_id": identity["case_id"],
189 "identity_source": identity["identity_source"],
190 }]
191
192 study_root = os.path.abspath(study_dir)
193 if not os.path.isdir(study_root):
194 raise StorageError(f"Study directory not found: {study_root}")
195 study_identity = read_artifact_identity(study_root)
196 if study_identity["identity_source"] != "manifest" and not os.path.isdir(
197 os.path.join(study_root, "cases")):
198 raise StorageError(
199 f"Directory does not look like a PICurv study (no {STUDY_MANIFEST_FILENAME} "
200 f"and no cases/): {study_root}"
201 )
202 if not os.path.isdir(os.path.join(study_root, "cases")):
203 raise StorageError(f"Study has no cases/ directory: {study_root}")
204 if completed:
205 if case_ids:
206 raise StorageError("--completed selects members itself; do not also pass --case-id.")
207 case_ids = _select_completed_case_ids(study_root)
208 if case_ids:
209 targets = []
210 for raw_case_id in case_ids:
211 case_id = _validate_case_id(raw_case_id)
212 case_root = os.path.join(study_root, "cases", case_id)
213 if not os.path.isdir(case_root):
214 raise StorageError(f"Study member not found: {case_root}")
215 member_identity = read_artifact_identity(case_root)
216 targets.append({
217 "artifact_type": "study-case",
218 "root_path": case_root,
219 "original_path": case_root,
220 "run_id": member_identity["run_id"] or case_id,
221 "study_id": study_identity["study_id"],
222 "case_id": member_identity["case_id"] or case_id,
223 "study_path": study_root,
224 "identity_source": member_identity["identity_source"],
225 })
226 return targets
227 return [{
228 "artifact_type": "study",
229 "root_path": study_root,
230 "original_path": study_root,
231 "run_id": None,
232 "study_id": study_identity["study_id"],
233 "case_id": None,
234 "identity_source": study_identity["identity_source"],
235 }]
236
237

◆ _path_is_within()

bool picurv_cli.storage.inventory._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 238 of file inventory.py.

238def _path_is_within(root: str, candidate: str) -> bool:
239 """!
240 @brief Return whether an absolute candidate remains within a root directory.
241 @param[in] root Value supplied through the `root` argument.
242 @param[in] candidate Value supplied through the `candidate` argument.
243 @return Result produced by this operation.
244 """
245 try:
246 return os.path.commonpath([os.path.abspath(root), os.path.abspath(candidate)]) == os.path.abspath(root)
247 except ValueError:
248 return False
249
250

◆ _resolve_configured_path()

str picurv_cli.storage.inventory._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 251 of file inventory.py.

251def _resolve_configured_path(root: str, value: str) -> str:
252 """!
253 @brief Resolve runtime-directory syntax using the run directory as working directory.
254 @param[in] root Value supplied through the `root` argument.
255 @param[in] value Value supplied through the `value` argument.
256 @return Result produced by this operation.
257 """
258 return os.path.abspath(value if os.path.isabs(value) else os.path.join(root, value))
259
260

◆ _artifact_runtime_roots()

list picurv_cli.storage.inventory._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 261 of file inventory.py.

261def _artifact_runtime_roots(root: str) -> list:
262 """!
263 @brief Return run-like roots contained by a standalone run or whole study.
264 @param[in] root Value supplied through the `root` argument.
265 @return Result produced by this operation.
266 """
267 root_path = Path(os.path.abspath(root))
268 case_root = root_path / "cases"
269 if case_root.is_dir():
270 return [
271 str(path) for path in sorted(case_root.glob("case_*"))
272 if path.is_dir()
273 ]
274 return [str(root_path)]
275
276

◆ _discover_external_paths()

list picurv_cli.storage.inventory._discover_external_paths ( str  root)
protected

Report explicit external-reference descriptors inside an artifact.

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

Definition at line 277 of file inventory.py.

277def _discover_external_paths(root: str) -> list:
278 """!
279 @brief Report explicit external-reference descriptors inside an artifact.
280 @param[in] root Value supplied through the `root` argument.
281 @return Result produced by this operation.
282 """
283 external = []
284 archive_root = os.path.abspath(root)
285 for descriptor in Path(archive_root).glob("**/*.reference.yml"):
286 try:
287 with descriptor.open("r", encoding="utf-8") as stream:
288 payload = yaml.safe_load(stream) or {}
289 target = payload.get("picurv_external_reference")
290 if isinstance(target, str) and os.path.isabs(target):
291 external.append({
292 "source": os.path.relpath(descriptor, archive_root).replace(os.sep, "/"),
293 "path": os.path.abspath(target),
294 })
295 except (OSError, ValueError, TypeError):
296 pass
297 return sorted(external, key=lambda item: (item["source"], item["path"]))
298
299

◆ _discover_dependencies()

list picurv_cli.storage.inventory._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 300 of file inventory.py.

300def _discover_dependencies(root: str) -> list:
301 """!
302 @brief Discover absolute restart/source paths embedded in generated controls.
303 @param[in] root Value supplied through the `root` argument.
304 @return Result produced by this operation.
305 """
306 dependencies = []
307 archive_root = os.path.abspath(root)
308 controls = []
309 for runtime_root in _artifact_runtime_roots(root):
310 controls.extend(sorted(Path(runtime_root).glob("config/*.control")))
311 for control in controls:
312 try:
313 lines = control.read_text(encoding="utf-8", errors="replace").splitlines()
314 except OSError:
315 continue
316 for line in lines:
317 stripped = line.strip()
318 if not stripped.startswith("-restart_dir "):
319 continue
320 try:
321 tokens = __import__("shlex").split(stripped)
322 except ValueError:
323 continue
324 if len(tokens) >= 2 and os.path.isabs(tokens[1]) and not _path_is_within(archive_root, tokens[1]):
325 dependencies.append({"kind": "restart", "path": os.path.abspath(tokens[1])})
326 return dependencies
327
328

◆ _walk_archive_entries()

list picurv_cli.storage.inventory._walk_archive_entries ( str  root,
  excluded_roots = () 
)
protected

Enumerate archive entries without following symlinks.

Parameters
[in]rootValue supplied through the root argument.
[in]excluded_rootsTop-level directory names to skip entirely.
Returns
Result produced by this operation.

Definition at line 329 of file inventory.py.

329def _walk_archive_entries(root: str, excluded_roots=()) -> list:
330 """!
331 @brief Enumerate archive entries without following symlinks.
332 @param[in] root Value supplied through the `root` argument.
333 @param[in] excluded_roots Top-level directory names to skip entirely.
334 @return Result produced by this operation.
335 """
336 root_abs = os.path.abspath(root)
337 excluded = set(excluded_roots or ())
338 entries = []
339
340 def visit(directory: str):
341 """!
342 @brief Recursively inventory directory entries without following symlinks.
343 @param[in] directory Value supplied through the `directory` argument.
344 """
345 try:
346 children = sorted(os.scandir(directory), key=lambda item: item.name)
347 except OSError as exc:
348 raise StorageError(f"Unable to inventory {directory}: {exc}") from exc
349 for child in children:
350 if child.name in {STORAGE_STATE_FILENAME, STORAGE_LOCK_FILENAME}:
351 continue
352 rel = os.path.relpath(child.path, root_abs).replace(os.sep, "/")
353 if rel in excluded:
354 continue
355 try:
356 stat_result = child.stat(follow_symlinks=False)
357 except OSError as exc:
358 raise StorageError(f"Unable to stat {child.path}: {exc}") from exc
359 if child.is_symlink():
360 entry_type = "symlink"
361 size = 0
362 elif child.is_dir(follow_symlinks=False):
363 entry_type = "directory"
364 size = 0
365 elif child.is_file(follow_symlinks=False):
366 entry_type = "file"
367 size = int(stat_result.st_size)
368 else:
369 raise StorageError(f"Unsupported filesystem entry in artifact: {child.path}")
370 entries.append({
371 "path": rel,
372 "type": entry_type,
373 "size": size,
374 "mode": int(stat_result.st_mode & 0o7777),
375 "mtime_ns": int(stat_result.st_mtime_ns),
376 })
377 if entry_type == "directory":
378 visit(child.path)
379
380 visit(root_abs)
381 return entries
382
383
384#: Component paths assumed when an artifact carries no manifest of its own: a run
385#: staged by an older release, or a directory restored without its identity. These are
386#: the fixed workspace topology the conductor writes, kept here so a manifest-less
387#: artifact still classifies rather than becoming wholly "unclassified" and unprunable.

◆ _artifact_component_layout()

dict picurv_cli.storage.inventory._artifact_component_layout ( str  root,
str  artifact_type 
)
protected

Read from the artifact's own manifests where each component lives.

Storage classifies what it packages by asking the run what its directories are, not by assuming the topology from path prefixes. A run records its component map in manifest.json under paths, and a study's members each record their own, so a renamed, restored, or re-rooted artifact classifies the same way it did where it was written.

Members are found by the manifest each one carries, not by matching a case_NNNN name. When a manifest is missing the fixed workspace topology is used and reported through identity_source, so a caller can tell an answer from a fallback.

Parameters
[in]rootAbsolute artifact root.
[in]artifact_type"run", "study", "study-case", or "workspace".
Returns
Mapping with members (member-prefix to component-path map) and identity_source.

Definition at line 400 of file inventory.py.

400def _artifact_component_layout(root: str, artifact_type: str) -> dict:
401 """!
402 @brief Read from the artifact's own manifests where each component lives.
403
404 @details Storage classifies what it packages by asking the run what its directories
405 are, not by assuming the topology from path prefixes. A run records its
406 component map in `manifest.json` under `paths`, and a study's members each
407 record their own, so a renamed, restored, or re-rooted artifact classifies
408 the same way it did where it was written.
409
410 Members are found by the manifest each one carries, not by matching a
411 `case_NNNN` name. When a manifest is missing the fixed workspace topology
412 is used and reported through `identity_source`, so a caller can tell an
413 answer from a fallback.
414 @param[in] root Absolute artifact root.
415 @param[in] artifact_type "run", "study", "study-case", or "workspace".
416 @return Mapping with `members` (member-prefix to component-path map) and
417 `identity_source`.
418 """
419 root_abs = os.path.abspath(root)
420
421 def component_paths(member_root: str) -> tuple:
422 """!
423 @brief Return one artifact's declared component map and where it came from.
424 @param[in] member_root Absolute run or study-member root.
425 @return Tuple of (component path mapping, identity source).
426 """
427 identity = read_artifact_identity(member_root)
428 declared = identity.get("paths") or {}
429 # A study manifest's `paths` names its scripts and tables, not the run-relative
430 # component tree, so it is not a component map and must not be read as one.
431 usable = {
432 key: str(value).strip("/")
433 for key, value in declared.items()
434 if isinstance(value, str) and value and not os.path.isabs(value)
435 }
436 if identity["identity_source"] == "manifest" and usable:
437 return usable, "manifest"
438 return dict(FALLBACK_COMPONENT_PATHS), "fixed-topology"
439
440 members = {}
441 sources = set()
442 if artifact_type == "study":
443 cases_root = os.path.join(root_abs, "cases")
444 if os.path.isdir(cases_root):
445 for name in sorted(os.listdir(cases_root)):
446 member_root = os.path.join(cases_root, name)
447 if not os.path.isdir(member_root):
448 continue
449 paths, source = component_paths(member_root)
450 members[f"cases/{name}"] = paths
451 sources.add(source)
452 paths, source = component_paths(root_abs)
453 members[""] = paths
454 sources.add(source)
455 return {
456 "root": root_abs,
457 "members": members,
458 "identity_source": "manifest" if sources == {"manifest"} else "mixed"
459 if "manifest" in sources else "fixed-topology",
460 }
461
462

◆ _checkpoint_step_from_bundle()

picurv_cli.storage.inventory._checkpoint_step_from_bundle ( str  bundle_root)
protected

Read a committed checkpoint's own recorded step number.

The bundle writes -checkpoint_step into checkpoint.meta, which is what the step is; the directory name is a rendering of it. Reading the record keeps the classification on the same footing as the rest of this module, and stays correct for a bundle restored under a different name.

Parameters
[in]bundle_rootAbsolute path to one checkpoint bundle directory.
Returns
The recorded step as an int, or None when it cannot be read.

Definition at line 463 of file inventory.py.

463def _checkpoint_step_from_bundle(bundle_root: str):
464 """!
465 @brief Read a committed checkpoint's own recorded step number.
466
467 @details The bundle writes `-checkpoint_step` into `checkpoint.meta`, which is what
468 the step *is*; the directory name is a rendering of it. Reading the record
469 keeps the classification on the same footing as the rest of this module,
470 and stays correct for a bundle restored under a different name.
471 @param[in] bundle_root Absolute path to one checkpoint bundle directory.
472 @return The recorded step as an int, or None when it cannot be read.
473 """
474 try:
475 with open(os.path.join(bundle_root, "checkpoint.meta"), "r", encoding="utf-8") as stream:
476 for line in stream:
477 tokens = line.split("#", 1)[0].split()
478 if len(tokens) == 2 and tokens[0] == "-checkpoint_step":
479 return int(tokens[1])
480 except (OSError, ValueError):
481 return None
482 return None
483
484

◆ _classify_workspace_component()

str picurv_cli.storage.inventory._classify_workspace_component ( str  relative_path)
protected

Classify one workspace-owned path for packaging and retention.

A workspace has no manifest of components the way a run does: its shape is the fixed layout picurv init writes, and .picurv-workspace.yml records identity rather than topology. So this stays a layout decision, and the three named roots are the ones the workspace contract defines.

Parameters
[in]relative_pathWorkspace-relative path.
Returns
Component name.

Definition at line 485 of file inventory.py.

485def _classify_workspace_component(relative_path: str) -> str:
486 """!
487 @brief Classify one workspace-owned path for packaging and retention.
488
489 @details A workspace has no manifest of components the way a run does: its shape is
490 the fixed layout `picurv init` writes, and `.picurv-workspace.yml` records
491 identity rather than topology. So this stays a layout decision, and the
492 three named roots are the ones the workspace contract defines.
493 @param[in] relative_path Workspace-relative path.
494 @return Component name.
495 """
496 first = relative_path.split("/")[0]
497 if first == "config" or relative_path.startswith("."):
498 return "workspace-config"
499 if first == "inputs":
500 return "workspace-inputs"
501 if first == "assets":
502 return "assets"
503 return UNCLASSIFIED_COMPONENT
504
505

◆ _classify_component()

str picurv_cli.storage.inventory._classify_component ( str  relative_path,
dict   layout = None 
)
protected

Classify one artifact path for packaging and local retention.

Parameters
[in]relative_pathArtifact-relative path of the entry.
[in]layoutComponent layout from _artifact_component_layout(). When absent the fixed workspace topology is assumed.
Returns
Component name, or a checkpoint:<step> token.

Definition at line 506 of file inventory.py.

506def _classify_component(relative_path: str, layout: dict = None) -> str:
507 """!
508 @brief Classify one artifact path for packaging and local retention.
509 @param[in] relative_path Artifact-relative path of the entry.
510 @param[in] layout Component layout from `_artifact_component_layout()`. When absent
511 the fixed workspace topology is assumed.
512 @return Component name, or a `checkpoint:<step>` token.
513 """
514 layout = layout or {"root": "", "members": {"": dict(FALLBACK_COMPONENT_PATHS)}}
515 members = layout["members"]
516 # Longest member prefix wins, so a study member's own map beats the study's.
517 prefix = ""
518 for candidate in members:
519 if not candidate:
520 continue
521 if relative_path == candidate or relative_path.startswith(candidate + "/"):
522 if len(candidate) > len(prefix):
523 prefix = candidate
524 paths = members.get(prefix) or dict(FALLBACK_COMPONENT_PATHS)
525 local = relative_path[len(prefix):].lstrip("/") if prefix else relative_path
526 base = os.path.basename(relative_path)
527
528 def under(component_key: str) -> bool:
529 """!
530 @brief Whether the entry lies at or below one declared component path.
531 @param[in] component_key Component name in the artifact's own path map.
532 @return True when the entry belongs to that component.
533 """
534 declared = paths.get(component_key)
535 if not declared:
536 return False
537 return local == declared or local.startswith(declared + "/")
538
539 # The locks record which assets and which executables a run consumed. They are the
540 # run's provenance, not its payload: pruning them would leave a cold run unable to
541 # say what it was built from, and reference-aware asset pruning unable to see it.
542 if base in {
543 "manifest.json", "study_manifest.json", "study.yml", "cluster.yml",
544 "assets.lock.yml", "software.lock.json",
545 }:
546 return "metadata"
547 if under("config") or under("scheduler"):
548 return "metadata"
549 if under("checkpoints"):
550 declared = paths["checkpoints"]
551 remainder = local[len(declared):].lstrip("/")
552 bundle = remainder.split("/", 1)[0]
553 if bundle:
554 step = _checkpoint_step_from_bundle(
555 os.path.join(layout["root"], *(prefix.split("/") if prefix else []),
556 *declared.split("/"), bundle)
557 )
558 if step is None:
559 # The bundle's own record is the authority, but a checkpoint whose
560 # metadata cannot be read must not thereby stop being a checkpoint:
561 # that would move it into a component an ordinary policy prunes. Fall
562 # back to the name it was written under.
563 name_match = CHECKPOINT_DIRECTORY_PATTERN.fullmatch(bundle)
564 if name_match:
565 step = int(name_match.group(1))
566 if step is not None:
567 return f"checkpoint:{step}"
568 # Neither the record nor the name identifies a step. Leave it unclassified,
569 # which is archived and never pruned, rather than guessing.
570 return UNCLASSIFIED_COMPONENT
571 return "raw-output"
572 if under("analysis"):
573 return "analysis"
574 if under("visualization"):
575 return "visualization"
576 if under("output"):
577 return "raw-output"
578 if under("inputs"):
579 return "inputs"
580 if under("logs"):
581 return "logs"
582 if local.split("/")[0] == "assets":
583 return "assets"
584 if local.split("/")[0] == "results":
585 return "analysis"
586 # Nothing recognized this path. It is archived like everything else, but it is
587 # never pruned: storage must not delete a file whose purpose it cannot state.
588 return UNCLASSIFIED_COMPONENT
589
590

◆ _checkpoint_steps()

list picurv_cli.storage.inventory._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 591 of file inventory.py.

591def _checkpoint_steps(entries: list) -> list:
592 """!
593 @brief Return committed checkpoint steps represented in an inventory.
594 @param[in] entries Value supplied through the `entries` argument.
595 @return Result produced by this operation.
596 """
597 candidates = {}
598 for entry in entries:
599 component = entry.get("component") or ""
600 if component.startswith("checkpoint:"):
601 step = int(component.split(":", 1)[1])
602 candidates.setdefault(step, set()).add(os.path.basename(entry["path"]))
603 return sorted(step for step, names in candidates.items() if {"checkpoint.meta", "COMMITTED"} <= names)
604
605

◆ _find_incomplete_checkpoints()

list picurv_cli.storage.inventory._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 606 of file inventory.py.

606def _find_incomplete_checkpoints(entries: list) -> list:
607 """!
608 @brief Return incomplete checkpoint paths that make archival unsafe.
609 @param[in] entries Value supplied through the `entries` argument.
610 @return Result produced by this operation.
611 """
612 return [
613 entry["path"] for entry in entries
614 if any(INCOMPLETE_CHECKPOINT_PATTERN.match(part) for part in entry["path"].split("/"))
615 ]
616
617

◆ inspect_artifact()

dict picurv_cli.storage.inventory.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 618 of file inventory.py.

618def inspect_artifact(target: dict, query_scheduler: bool = True) -> dict:
619 """!
620 @brief Build a read-only inventory and lifecycle assessment for one artifact.
621 @param[in] target Value supplied through the `target` argument.
622 @param[in] query_scheduler Value supplied through the `query_scheduler` argument.
623 @return Result produced by this operation.
624 """
625 root = target["root_path"]
626 workspace = target["artifact_type"] == "workspace"
627 entries = _walk_archive_entries(
628 root, WORKSPACE_EXCLUDED_ROOTS if workspace else ()
629 )
630 layout = None if workspace else _artifact_component_layout(root, target["artifact_type"])
631 for entry in entries:
632 entry["component"] = (
633 _classify_workspace_component(entry["path"]) if workspace
634 else _classify_component(entry["path"], layout)
635 )
636 if workspace and not target.get("include_inputs"):
637 # User-supplied data is theirs. It is archived only on request, because a
638 # workspace protect is about the configuration and catalog, not about taking
639 # custody of possibly enormous imported fields.
640 entries = [
641 entry for entry in entries
642 if entry["component"] != "workspace-inputs" or entry["type"] == "directory"
643 ]
644 lock_paths = []
645 for name in ("post.lock.json", "solver.lock.json"):
646 for candidate in Path(root).glob(f"**/scheduler/{name}"):
647 if _lock_owner_active(str(candidate)):
648 lock_paths.append(os.path.relpath(candidate, root))
649 slurm = _slurm_activity(root) if query_scheduler else {"job_ids": [], "active": [], "unknown": False}
650 state = storage_state_summary(root)
651 return {
652 "target": dict(target),
653 "component_layout_source": layout["identity_source"] if layout else "workspace-contract",
654 "entries": entries,
655 "file_count": sum(entry["type"] in {"file", "symlink"} for entry in entries),
656 "total_bytes": sum(entry["size"] for entry in entries),
657 "checkpoint_steps": _checkpoint_steps(entries),
658 "incomplete_checkpoints": _find_incomplete_checkpoints(entries),
659 "active_locks": sorted(lock_paths),
660 "slurm": slurm,
661 "external_paths": _discover_external_paths(root),
662 "dependencies": _discover_dependencies(root),
663 "storage": state,
664 }
665
666

◆ _inventory_fingerprint()

str picurv_cli.storage.inventory._inventory_fingerprint ( dict  inventory)
protected

Stable digest of the file set an archive would package.

Built from each entry's run-relative path, size, and modification time - the same signals ordinary change detection uses - so it can be computed without reading file contents. Two archives of the same artifact agree on it exactly when nothing has been written since, which is what makes a re-upload detectably redundant.

Parameters
[in]inventoryInventory returned by inspect_artifact().
Returns
Hex digest over the normalized entry list.

Definition at line 667 of file inventory.py.

667def _inventory_fingerprint(inventory: dict) -> str:
668 """!
669 @brief Stable digest of the file set an archive would package.
670
671 @details Built from each entry's run-relative path, size, and modification time -
672 the same signals ordinary change detection uses - so it can be computed
673 without reading file contents. Two archives of the same artifact agree on
674 it exactly when nothing has been written since, which is what makes a
675 re-upload detectably redundant.
676 @param[in] inventory Inventory returned by `inspect_artifact()`.
677 @return Hex digest over the normalized entry list.
678 """
679 digest = hashlib.sha256()
680 for entry in sorted(inventory["entries"], key=lambda item: item["path"]):
681 digest.update(
682 f"{entry['path']}\0{entry.get('kind')}\0{entry.get('bytes')}\0"
683 f"{entry.get('mtime_ns')}\n".encode("utf-8")
684 )
685 return digest.hexdigest()

Variable Documentation

◆ FALLBACK_COMPONENT_PATHS

dict picurv_cli.storage.inventory.FALLBACK_COMPONENT_PATHS
Initial value:
1= {
2 "config": "config",
3 "scheduler": "scheduler",
4 "inputs": "inputs",
5 "output": "output",
6 "checkpoints": "output/checkpoints",
7 "analysis": "output/analysis",
8 "visualization": "output/visualization",
9 "logs": "logs",
10}

Definition at line 388 of file inventory.py.