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

Functions

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.
 
str _write_tar_chunk (str root, dict spec, str destination, str compression, int workers=1)
 Package explicitly inventoried entries without following symlinks.
 
set _normalize_retention_selection (values, str flag)
 Validate one –retain/–drop selection into a component name set.
 
dict _resolve_offload_policy (dict profile, str requested=None, keep_latest_checkpoint=None, retain=None, drop=None)
 Resolve semantic local-retention behavior for an offload.
 
bool _entry_retained_by_policy (dict entry, dict policy, latest_step)
 Return whether one inventoried entry remains local after offload.
 
tuple _compression_size_range (int source_bytes, str compression)
 Return a deliberately broad planning estimate, never a promised ratio.
 
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 _merge_tree (str source, str destination)
 Merge a verified restore tree into a known cold artifact skeleton.
 

Function Documentation

◆ _select_compression()

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

38def _select_compression(requested: str, total_bytes: int, profile: dict) -> str:
39 """!
40 @brief Resolve automatic or configured compression policy.
41 @param[in] requested Value supplied through the `requested` argument.
42 @param[in] total_bytes Value supplied through the `total_bytes` argument.
43 @param[in] profile Value supplied through the `profile` argument.
44 @return Result produced by this operation.
45 """
46 selected = requested or profile.get("compression", "auto")
47 selected = str(selected).strip().lower()
48 if selected not in STORAGE_COMPRESSION_POLICIES:
49 raise StorageError("Compression must be one of: auto, none, fast, balanced, maximum.")
50 if selected != "auto":
51 return selected
52 if total_bytes < AUTO_NO_COMPRESSION_BYTES:
53 return "none"
54 if total_bytes >= AUTO_MAXIMUM_COMPRESSION_BYTES:
55 return "maximum"
56 return "balanced"
57
58

◆ _chunk_extension()

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

59def _chunk_extension(compression: str) -> str:
60 """!
61 @brief Return the archive suffix for a compression policy.
62 @param[in] compression Value supplied through the `compression` argument.
63 @return Result produced by this operation.
64 """
65 return STORAGE_COMPRESSION_EXTENSIONS[compression]
66
67

◆ _build_chunk_specs()

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

68def _build_chunk_specs(inventory: dict, chunk_size_bytes: int) -> list:
69 """!
70 @brief Group archive entries into independently transferable component chunks.
71 @param[in] inventory Value supplied through the `inventory` argument.
72 @param[in] chunk_size_bytes Value supplied through the `chunk_size_bytes` argument.
73 @return Result produced by this operation.
74 """
75 groups = {}
76 directory_groups = {}
77 for entry in inventory["entries"]:
78 if entry["type"] == "directory":
79 # A directory belongs to the same component as the files inside it. Sweeping
80 # every directory into `metadata` instead meant restoring one checkpoint
81 # recreated the empty directory tree of every other checkpoint, so a
82 # partially restored run listed steps whose payload was not there.
83 directory_groups.setdefault(entry["component"], []).append(entry["path"])
84 continue
85 groups.setdefault(entry["component"], []).append(entry)
86 specs = []
87 component_order = sorted(
88 set(groups) | set(directory_groups),
89 key=lambda name: (not name.startswith("checkpoint:"), name),
90 )
91 for component in component_order:
92 # Directories ride in the first chunk of their own component, so extracting
93 # that component creates its tree and no one else's.
94 pending_directories = sorted(directory_groups.get(component, []))
95 if component not in groups:
96 if pending_directories:
97 specs.append({
98 "component": component,
99 "entries": pending_directories,
100 "uncompressed_bytes": 0,
101 })
102 continue
103 current = list(pending_directories)
104 current_bytes = 0
105 for entry in groups[component]:
106 entry_size = max(1, int(entry["size"]))
107 if current and current_bytes + entry_size > chunk_size_bytes:
108 specs.append({"component": component, "entries": current, "uncompressed_bytes": current_bytes})
109 current = []
110 current_bytes = 0
111 current.append(entry["path"])
112 current_bytes += int(entry["size"])
113 if current:
114 specs.append({"component": component, "entries": current, "uncompressed_bytes": current_bytes})
115 return specs
116
117
Head of a generic C-style linked list.
Definition variables.h:475

◆ _safe_component_name()

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

118def _safe_component_name(component: str) -> str:
119 """!
120 @brief Convert a component token into a portable archive filename fragment.
121 @param[in] component Value supplied through the `component` argument.
122 @return Result produced by this operation.
123 """
124 return re.sub(r"[^A-Za-z0-9_.-]+", "-", component).strip("-") or "data"
125
126

◆ _write_tar_chunk()

str picurv_cli.storage.packaging._write_tar_chunk ( str  root,
dict  spec,
str  destination,
str  compression,
int   workers = 1 
)
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.
[in]workersMaximum compressor worker count.
Returns
Compressor implementation used for the archive chunk.

Definition at line 127 of file packaging.py.

128 workers: int = 1) -> str:
129 """!
130 @brief Package explicitly inventoried entries without following symlinks.
131 @param[in] root Value supplied through the `root` argument.
132 @param[in] spec Value supplied through the `spec` argument.
133 @param[in] destination Value supplied through the `destination` argument.
134 @param[in] compression Value supplied through the `compression` argument.
135 @param[in] workers Maximum compressor worker count.
136 @return Compressor implementation used for the archive chunk.
137 """
138 tar_executable = shutil.which("tar")
139 compressor = None
140 compressor_args = []
141 if compression in _PARALLEL_GZIP_VALUES and shutil.which("pigz"):
142 compressor = shutil.which("pigz")
143 compressor_args = [compressor, "-c", "-p", str(workers), "-1" if compression == "fast" else "-6"]
144 elif compression == "maximum" and shutil.which("xz"):
145 compressor = shutil.which("xz")
146 compressor_args = [compressor, "-c", "-T", str(workers), "-9e"]
147 if compressor and tar_executable:
148 list_file = tempfile.NamedTemporaryFile(prefix="picurv-tar-list-", delete=False)
149 try:
150 list_file.write(b"\0".join(path.encode("utf-8") for path in spec["entries"]) + b"\0")
151 list_file.close()
152 tar_command = [
153 tar_executable, "-C", root, "--null", "--verbatim-files-from", "--no-recursion",
154 "-T", list_file.name, "-cf", "-",
155 ]
156 with tempfile.TemporaryFile() as tar_stderr, open(destination, "wb") as output:
157 producer = subprocess.Popen(tar_command, stdout=subprocess.PIPE, stderr=tar_stderr)
158 consumer = subprocess.Popen(
159 compressor_args, stdin=producer.stdout, stdout=output, stderr=subprocess.PIPE
160 )
161 producer.stdout.close()
162 _, compressor_stderr = consumer.communicate()
163 producer_code = producer.wait()
164 if producer_code != 0 or consumer.returncode != 0:
165 tar_stderr.seek(0)
166 detail = (
167 tar_stderr.read().decode("utf-8", "replace")
168 or compressor_stderr.decode("utf-8", "replace")
169 or "parallel archive command failed"
170 ).strip()
171 raise StorageError(detail)
172 return f"{os.path.basename(compressor)}:{workers}"
173 finally:
174 try:
175 os.remove(list_file.name)
176 except FileNotFoundError:
177 pass
178
179 kwargs = {}
180 if compression == "none":
181 mode = "w"
182 elif compression == "fast":
183 mode = "w:gz"
184 kwargs["compresslevel"] = 1
185 elif compression == "balanced":
186 mode = "w:gz"
187 kwargs["compresslevel"] = 6
188 else:
189 mode = "w:xz"
190 kwargs["preset"] = 9
191 with tarfile.open(destination, mode, dereference=False, **kwargs) as archive:
192 for relative in spec["entries"]:
193 source = os.path.join(root, *relative.split("/"))
194 if not os.path.lexists(source):
195 raise StorageError(f"Artifact changed during packaging; entry disappeared: {source}")
196 archive.add(source, arcname=relative, recursive=False)
197 return "python-tarfile"
198
199

◆ _normalize_retention_selection()

set picurv_cli.storage.packaging._normalize_retention_selection (   values,
str  flag 
)
protected

Validate one –retain/–drop selection into a component name set.

Parameters
[in]valuesRepeated component names from the command line, or None.
[in]flagUser-facing flag name, for error messages.
Returns
Set of selectable component names.

Definition at line 200 of file packaging.py.

200def _normalize_retention_selection(values, flag: str) -> set:
201 """!
202 @brief Validate one --retain/--drop selection into a component name set.
203 @param[in] values Repeated component names from the command line, or None.
204 @param[in] flag User-facing flag name, for error messages.
205 @return Set of selectable component names.
206 """
207 selected = set()
208 for raw in values or []:
209 for token in str(raw).split(","):
210 name = token.strip()
211 if not name:
212 continue
213 if name not in STORAGE_RETENTION_COMPONENTS:
214 raise StorageError(
215 f"{flag} {name!r} is not a selectable component. Choose from: "
216 + ", ".join(STORAGE_RETENTION_COMPONENTS)
217 )
218 selected.add(name)
219 return selected
220
221
Here is the caller graph for this function:

◆ _resolve_offload_policy()

dict picurv_cli.storage.packaging._resolve_offload_policy ( dict  profile,
str   requested = None,
  keep_latest_checkpoint = None,
  retain = None,
  drop = None 
)
protected

Resolve semantic local-retention behavior for an offload.

A named policy is a preset, not a ceiling. --retain/--drop adjust the preset one component at a time, so a campaign that needs, say, the analysis of analysis-ready but also its checkpoints does not have to choose the policy that happens to bundle both. The preset is still what decides everything not named explicitly.

Parameters
[in]profileActive storage profile.
[in]requestedOptional command-level policy override.
[in]keep_latest_checkpointOptional checkpoint-retention override.
[in]retainComponent names to retain locally regardless of the preset.
[in]dropComponent names to prune locally regardless of the preset.
Returns
Normalized retention policy mapping.

Definition at line 222 of file packaging.py.

223 keep_latest_checkpoint=None, retain=None, drop=None) -> dict:
224 """!
225 @brief Resolve semantic local-retention behavior for an offload.
226
227 @details A named policy is a preset, not a ceiling. `--retain`/`--drop` adjust the
228 preset one component at a time, so a campaign that needs, say, the
229 analysis of `analysis-ready` but also its checkpoints does not have to
230 choose the policy that happens to bundle both. The preset is still what
231 decides everything not named explicitly.
232 @param[in] profile Active storage profile.
233 @param[in] requested Optional command-level policy override.
234 @param[in] keep_latest_checkpoint Optional checkpoint-retention override.
235 @param[in] retain Component names to retain locally regardless of the preset.
236 @param[in] drop Component names to prune locally regardless of the preset.
237 @return Normalized retention policy mapping.
238 """
239 name = requested or profile.get("offload_policy", "metadata-only")
240 if name not in STORAGE_OFFLOAD_POLICIES:
241 raise StorageError("Offload policy must be one of: " + ", ".join(STORAGE_OFFLOAD_POLICIES))
242 retain_set = _normalize_retention_selection(retain, "--retain")
243 drop_set = _normalize_retention_selection(drop, "--drop")
244 conflicting = retain_set & drop_set
245 if conflicting:
246 raise StorageError(
247 "A component cannot be both retained and dropped: " + ", ".join(sorted(conflicting))
248 )
249 # The conflict is between two things the user actually asked for, so it is decided
250 # on the requested value: the derived default is False far more often than
251 # --drop-all-checkpoints was typed, and refusing on that would reject the ordinary
252 # `--retain checkpoints` with no second flag at all.
253 if "checkpoints" in retain_set and keep_latest_checkpoint is False:
254 raise StorageError(
255 "--retain checkpoints keeps every committed step, which contradicts "
256 "--drop-all-checkpoints. Pass one or the other."
257 )
258 if keep_latest_checkpoint is None:
259 # Nothing explicit was requested: fall back to the profile default, or to
260 # `restart-ready`'s own promise to keep the newest checkpoint. An explicit
261 # --keep-latest-checkpoint/--drop-all-checkpoints always overrides both,
262 # rather than being silently overruled by the policy it was paired with.
263 keep_latest_checkpoint = bool(profile.get("keep_latest_checkpoint", False)) or name == "restart-ready"
264 retained = set({
265 "metadata-only": {"metadata", "logs"},
266 "restart-ready": {"metadata", "logs", "inputs"},
267 "analysis-ready": {"metadata", "logs", "analysis", "visualization"},
268 }[name])
269 retained |= retain_set
270 retained -= drop_set
271 # Identity is never optional: a cold artifact that cannot say what it is, what it
272 # ran, and what it consumed cannot be restored or reasoned about.
273 retained.add("metadata")
274 return {
275 "name": name,
276 "retained_components": sorted(retained),
277 "explicit_retain": sorted(retain_set),
278 "explicit_drop": sorted(drop_set),
279 "keep_latest_checkpoint": bool(keep_latest_checkpoint),
280 }
281
282
Here is the call graph for this function:

◆ _entry_retained_by_policy()

bool picurv_cli.storage.packaging._entry_retained_by_policy ( dict  entry,
dict  policy,
  latest_step 
)
protected

Return whether one inventoried entry remains local after offload.

Parameters
[in]entryInventoried artifact entry.
[in]policyNormalized offload policy.
[in]latest_stepNewest committed checkpoint step, if any.
Returns
True when the entry is retained locally.

Definition at line 283 of file packaging.py.

283def _entry_retained_by_policy(entry: dict, policy: dict, latest_step) -> bool:
284 """!
285 @brief Return whether one inventoried entry remains local after offload.
286 @param[in] entry Inventoried artifact entry.
287 @param[in] policy Normalized offload policy.
288 @param[in] latest_step Newest committed checkpoint step, if any.
289 @return True when the entry is retained locally.
290 """
291 component = entry["component"]
292 # Never pruned, whatever the policy: a file storage cannot classify, and a
293 # workspace's editable configuration and user-supplied inputs. Protecting a
294 # workspace is a backup, not a handover of the user's own files.
295 if component in ALWAYS_RETAINED_COMPONENTS:
296 return True
297 retained = set(policy["retained_components"])
298 if component.startswith("checkpoint:"):
299 # "checkpoints" retains every committed step; --keep-latest-checkpoint is the
300 # narrower selection of just the newest one.
301 if "checkpoints" in retained:
302 return True
303 return bool(
304 policy["keep_latest_checkpoint"]
305 and latest_step is not None
306 and component == f"checkpoint:{latest_step}"
307 )
308 return component in retained
309
310

◆ _compression_size_range()

tuple picurv_cli.storage.packaging._compression_size_range ( int  source_bytes,
str  compression 
)
protected

Return a deliberately broad planning estimate, never a promised ratio.

Parameters
[in]source_bytesUncompressed payload byte count.
[in]compressionSelected compression policy.
Returns
Estimated low and high archive sizes in bytes.

Definition at line 311 of file packaging.py.

311def _compression_size_range(source_bytes: int, compression: str) -> tuple:
312 """!
313 @brief Return a deliberately broad planning estimate, never a promised ratio.
314 @param[in] source_bytes Uncompressed payload byte count.
315 @param[in] compression Selected compression policy.
316 @return Estimated low and high archive sizes in bytes.
317 """
318 ratios = {
319 "none": (1.0, 1.03),
320 "fast": (0.45, 0.90),
321 "balanced": (0.35, 0.85),
322 "maximum": (0.25, 0.80),
323 }
324 low, high = ratios[compression]
325 return int(source_bytes * low), int(source_bytes * high)
326
327

◆ _validate_tar_members()

None picurv_cli.storage.packaging._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 328 of file packaging.py.

328def _validate_tar_members(archive: tarfile.TarFile) -> None:
329 """!
330 @brief Reject archive members that could escape the restore destination.
331 @param[in] archive Value supplied through the `archive` argument.
332 """
333 link_paths = set()
334 members = archive.getmembers()
335 for member in members:
336 normalized = os.path.normpath(member.name.replace("\\", "/"))
337 if normalized.startswith("../") or normalized == ".." or os.path.isabs(normalized):
338 raise StorageError(f"Unsafe archive member path: {member.name}")
339 for link_path in link_paths:
340 if normalized == link_path or normalized.startswith(link_path.rstrip("/") + "/"):
341 raise StorageError(f"Archive member traverses an earlier symlink: {member.name}")
342 if member.issym() or member.islnk():
343 if member.islnk():
344 link_target = os.path.normpath(member.linkname.replace("\\", "/"))
345 if link_target.startswith("../") or link_target == ".." or os.path.isabs(link_target):
346 raise StorageError(f"Unsafe archive hardlink target: {member.linkname}")
347 link_paths.add(normalized)
348
349

◆ _extract_chunk()

None picurv_cli.storage.packaging._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 350 of file packaging.py.

350def _extract_chunk(path: str, destination: str) -> None:
351 """!
352 @brief Safely extract one verified tar chunk into a staging tree.
353 @param[in] path Value supplied through the `path` argument.
354 @param[in] destination Value supplied through the `destination` argument.
355 """
356 with tarfile.open(path, "r:*") as archive:
357 _validate_tar_members(archive)
358 try:
359 archive.extractall(destination, filter="data")
360 except TypeError:
361 archive.extractall(destination)
362
363

◆ _merge_tree()

None picurv_cli.storage.packaging._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 364 of file packaging.py.

364def _merge_tree(source: str, destination: str) -> None:
365 """!
366 @brief Merge a verified restore tree into a known cold artifact skeleton.
367 @param[in] source Value supplied through the `source` argument.
368 @param[in] destination Value supplied through the `destination` argument.
369 """
370 os.makedirs(destination, exist_ok=True)
371 for entry in os.scandir(source):
372 target = os.path.join(destination, entry.name)
373 if entry.is_symlink():
374 if os.path.lexists(target):
375 if os.path.isdir(target) and not os.path.islink(target):
376 shutil.rmtree(target)
377 else:
378 os.remove(target)
379 os.symlink(os.readlink(entry.path), target)
380 elif entry.is_dir(follow_symlinks=False):
381 _merge_tree(entry.path, target)
382 else:
383 os.makedirs(os.path.dirname(target), exist_ok=True)
384 shutil.copy2(entry.path, target)