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

Functions

dict workspace_asset_references (str workspace_root)
 Count what still refers to each published workspace asset.
 
list prune_unused_workspace_assets (str workspace_root, dict profile, bool dry_run=False)
 Remove local asset objects that nothing local needs and storage has verified.
 
dict _find_reusable_archive (dict profile, dict target, str fingerprint)
 Find a completed archive of this artifact whose content is already current.
 
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.
 
str resolve_workspace_archive_id (dict profile, str workspace_id)
 Find the newest complete workspace archive carrying one workspace identity.
 

Function Documentation

◆ workspace_asset_references()

dict picurv_cli.storage.catalog.workspace_asset_references ( str  workspace_root)

Count what still refers to each published workspace asset.

A local copy may be removed once nothing local needs it and a verified remote copy exists. Runs that are themselves cold still reference the asset - that is what keeps the remote copy alive - but they do not keep the local one.

Parameters
[in]workspace_rootInitialized workspace root.
Returns
Mapping of asset id to its reference counts and local object path.

Definition at line 41 of file catalog.py.

41def workspace_asset_references(workspace_root: str) -> dict:
42 """!
43 @brief Count what still refers to each published workspace asset.
44
45 @details A local copy may be removed once nothing local needs it and a verified
46 remote copy exists. Runs that are themselves cold still *reference* the
47 asset - that is what keeps the remote copy alive - but they do not keep
48 the local one.
49 @param[in] workspace_root Initialized workspace root.
50 @return Mapping of asset id to its reference counts and local object path.
51 """
52 references = {}
53 objects_root = os.path.join(workspace_root, "assets", "objects")
54 if os.path.isdir(objects_root):
55 for kind in sorted(os.listdir(objects_root)):
56 kind_root = os.path.join(objects_root, kind)
57 if not os.path.isdir(kind_root):
58 continue
59 for asset_id in sorted(os.listdir(kind_root)):
60 object_root = os.path.join(kind_root, asset_id)
61 if os.path.isdir(object_root):
62 references[asset_id] = {
63 "asset_id": asset_id, "kind": kind, "object": object_root,
64 "active_local_runs": 0, "cold_runs": 0,
65 }
66 for artifacts in ("runs", "studies"):
67 root = os.path.join(workspace_root, artifacts)
68 if not os.path.isdir(root):
69 continue
70 for lock_path in Path(root).glob("**/inputs/assets.lock.yml"):
71 try:
72 with open(lock_path, "r", encoding="utf-8") as stream:
73 lock = yaml.safe_load(stream) or {}
74 except (OSError, ValueError):
75 continue
76 run_root = lock_path.parent.parent
77 cold = is_artifact_cold(str(run_root))
78 for reference in (lock.get("assets") or {}).values():
79 entry = references.get(reference.get("asset_id"))
80 if entry is None:
81 continue
82 entry["cold_runs" if cold else "active_local_runs"] += 1
83 return references
84
85
Here is the caller graph for this function:

◆ prune_unused_workspace_assets()

list picurv_cli.storage.catalog.prune_unused_workspace_assets ( str  workspace_root,
dict  profile,
bool   dry_run = False 
)

Remove local asset objects that nothing local needs and storage has verified.

Parameters
[in]workspace_rootInitialized workspace root.
[in]profileResolved storage profile.
[in]dry_runReport the decision without removing anything.
Returns
Removal decisions, one per published asset.

Definition at line 86 of file catalog.py.

87 dry_run: bool = False) -> list:
88 """!
89 @brief Remove local asset objects that nothing local needs and storage has verified.
90 @param[in] workspace_root Initialized workspace root.
91 @param[in] profile Resolved storage profile.
92 @param[in] dry_run Report the decision without removing anything.
93 @return Removal decisions, one per published asset.
94 """
95 protected = set()
96 for manifest in list_remote_manifests(profile):
97 if manifest.get("artifact_type") != "workspace":
98 continue
99 for asset_id in manifest.get("workspace_assets") or []:
100 protected.add(asset_id)
101 decisions = []
102 for entry in workspace_asset_references(workspace_root).values():
103 verified = entry["asset_id"] in protected
104 removable = verified and entry["active_local_runs"] == 0
105 decision = {**entry, "remote_protection": "verified" if verified else "none",
106 "local_removal": "safe" if removable else "blocked"}
107 if removable and not dry_run:
108 shutil.rmtree(entry["object"], ignore_errors=True)
109 decision["removed"] = True
110 decisions.append(decision)
111 return decisions
112
113
Here is the call graph for this function:

◆ _find_reusable_archive()

dict picurv_cli.storage.catalog._find_reusable_archive ( dict  profile,
dict  target,
str  fingerprint 
)
protected

Find a completed archive of this artifact whose content is already current.

Parameters
[in]profileResolved storage profile.
[in]targetLocal artifact target.
[in]fingerprintInventory fingerprint of the artifact as it stands now.
Returns
Matching remote manifest, or None.

Definition at line 114 of file catalog.py.

114def _find_reusable_archive(profile: dict, target: dict, fingerprint: str) -> dict:
115 """!
116 @brief Find a completed archive of this artifact whose content is already current.
117 @param[in] profile Resolved storage profile.
118 @param[in] target Local artifact target.
119 @param[in] fingerprint Inventory fingerprint of the artifact as it stands now.
120 @return Matching remote manifest, or None.
121 """
122 if not fingerprint:
123 return None
124 identity = (target["artifact_type"], target.get("run_id"),
125 target.get("study_id"), target.get("case_id"))
126 try:
127 candidates = list_remote_manifests(profile)
128 except StorageError as exc:
129 # Reuse is an optimization. A remote that cannot be listed - not yet created,
130 # briefly unreachable - must fall through to a normal upload, never fail here.
131 print(f"[INFO] Could not check for a reusable archive ({exc}); uploading.")
132 return None
133 newest = None
134 for manifest in candidates:
135 if manifest.get("inventory_sha256") != fingerprint:
136 continue
137 if (manifest.get("artifact_type"), manifest.get("run_id"),
138 manifest.get("study_id"), manifest.get("case_id")) != identity:
139 continue
140 if newest is None or str(manifest.get("created_at", "")) > str(newest.get("created_at", "")):
141 newest = manifest
142 return newest
143
144

◆ list_remote_manifests()

list picurv_cli.storage.catalog.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 145 of file catalog.py.

145def list_remote_manifests(profile: dict) -> list:
146 """!
147 @brief Enumerate completed archive manifests from the remote catalog.
148 @param[in] profile Value supplied through the `profile` argument.
149 @return Result produced by this operation.
150 """
151 root = _remote_join(profile["remote"], REMOTE_OBJECTS_DIRECTORY)
152 result = _transport._run_rclone([
153 "lsf", root, "--recursive", "--files-only", "--include", f"*/{REMOTE_MANIFEST_FILENAME}"
154 ])
155 manifests = []
156 for relative in sorted(line.strip() for line in result.stdout.splitlines() if line.strip()):
157 archive_id = relative.split("/", 1)[0]
158 if not ARCHIVE_ID_PATTERN.fullmatch(archive_id):
159 continue
160 try:
161 manifests.append(_load_remote_manifest(profile, archive_id))
162 except StorageError:
163 continue
164 return manifests
165
166

◆ verify_remote_archive()

dict picurv_cli.storage.catalog.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 167 of file catalog.py.

167def verify_remote_archive(profile: dict, archive_id: str) -> dict:
168 """!
169 @brief Verify the completion marker and every stored chunk checksum.
170 @param[in] profile Value supplied through the `profile` argument.
171 @param[in] archive_id Value supplied through the `archive_id` argument.
172 @return Result produced by this operation.
173 """
174 manifest = _load_remote_manifest(profile, archive_id)
175 for chunk in manifest.get("chunks", []):
176 actual = _remote_sha256(_chunk_remote_path(profile, archive_id, chunk))
177 if actual != chunk.get("sha256"):
178 raise StorageError(
179 f"Archive {archive_id} chunk checksum mismatch: {chunk['name']} "
180 f"(expected {chunk.get('sha256')}, got {actual})."
181 )
182 return manifest
183
184

◆ resolve_workspace_archive_id()

str picurv_cli.storage.catalog.resolve_workspace_archive_id ( dict  profile,
str  workspace_id 
)

Find the newest complete workspace archive carrying one workspace identity.

Parameters
[in]profileResolved storage profile.
[in]workspace_idWorkspace identity recorded at archive time.
Returns
Archive id.
Exceptions
StorageErrorwhen no such archive exists.

Definition at line 185 of file catalog.py.

185def resolve_workspace_archive_id(profile: dict, workspace_id: str) -> str:
186 """!
187 @brief Find the newest complete workspace archive carrying one workspace identity.
188 @param[in] profile Resolved storage profile.
189 @param[in] workspace_id Workspace identity recorded at archive time.
190 @return Archive id.
191 @throws StorageError when no such archive exists.
192 """
193 matches = [
194 manifest for manifest in list_remote_manifests(profile)
195 if manifest.get("artifact_type") == "workspace"
196 and manifest.get("workspace_id") == workspace_id
197 ]
198 if not matches:
199 raise StorageError(f"No workspace archive found for identity {workspace_id!r}.")
200 return max(matches, key=lambda item: str(item.get("created_at", "")))["archive_id"]