Attach the nested storage command parser to PICurv's top-level parser.
1249def add_storage_parser(subparsers) -> argparse.ArgumentParser:
1250 """!
1251 @brief Attach the nested storage command parser to PICurv's top-level parser.
1252 @param[in] subparsers Value supplied through the `subparsers` argument.
1253 @return Result produced by this operation.
1254 """
1255 parser = subparsers.add_parser(
1256 "storage",
1257 help="Protect, offload, inspect, verify, and restore run/study artifacts.",
1258 formatter_class=argparse.RawTextHelpFormatter,
1259 description=(
1260 "Manage PICurv run and study data through a configured rclone remote.\n"
1261 "Remote archives are checksum-verified before local payload can be pruned.\n\n"
1262 "Examples:\n"
1263 " picurv storage setup --remote labstore:picurv-data\n"
1264 " picurv storage status --run-dir runs/my_run\n"
1265 " picurv storage protect --run-dir runs/my_run --label 'baseline'\n"
1266 " picurv storage offload --study-dir studies/my_study --case-id case_0003\n"
1267 " picurv storage list --search '64-grid'\n"
1268 " picurv storage restore --archive-id <id>"
1269 ),
1270 epilog="Use `picurv storage <action> --help` for action-specific controls.",
1271 )
1272 actions = parser.add_subparsers(dest="storage_action", required=True, help="Storage action")
1273
1274 setup = actions.add_parser("setup", help="Configure a non-secret rclone storage profile.")
1275 setup.add_argument("--remote", required=True, help="Rclone remote and base path, such as labstore:picurv-data.")
1276 setup.add_argument("--profile", default=DEFAULT_PROFILE_NAME, help="Profile name (default: archive).")
1277 setup.add_argument("--storage-config", help=f"Storage YAML path (default: ./{STORAGE_CONFIG_FILENAME}).")
1278 setup.add_argument(
"--compression", choices=
list(STORAGE_COMPRESSION_POLICIES), default=
"auto")
1279 setup.add_argument("--chunk-size-gib", type=float, default=DEFAULT_CHUNK_SIZE_GIB)
1280 setup.add_argument("--workers", type=int, default=DEFAULT_STORAGE_WORKERS,
1281 help="CPU workers for compression and restoration.")
1282 setup.add_argument(
"--offload-policy", choices=
list(STORAGE_OFFLOAD_POLICIES), default=
"metadata-only")
1283 setup.add_argument("--keep-latest-checkpoint", action="store_true",
1284 help="Retain the newest committed checkpoint after offload.")
1285 setup.add_argument("--staging-directory", help="Optional local directory for one archive chunk at a time.")
1286 setup.add_argument("--dry-run", action="store_true")
1287
1288 def add_profile_options(action_parser):
1289 """!
1290 @brief Attach shared storage-profile selectors to one action parser.
1291 @param[in] action_parser Value supplied through the `action_parser` argument.
1292 """
1293 action_parser.add_argument("--profile", help="Configured storage profile name.")
1294 action_parser.add_argument("--storage-config", help="Explicit storage YAML path.")
1295
1296 def add_local_target(action_parser, require=True, allow_workspace=True):
1297 """!
1298 @brief Attach the standard run/study/workspace target selectors to one parser.
1299 @param[in] action_parser Value supplied through the `action_parser` argument.
1300 @param[in] require Value supplied through the `require` argument.
1301 @param[in] allow_workspace Whether a whole workspace is a valid target here.
1302 """
1303 group = action_parser.add_mutually_exclusive_group(required=require)
1304 group.add_argument("--run-dir", help="Standalone run directory.")
1305 group.add_argument("--study-dir", help="Sweep study directory.")
1306 if allow_workspace:
1307 group.add_argument(
1308 "--workspace",
1309 help="Workspace root: its configuration, catalog, and assets, not its "
1310 "runs and studies, which are their own artifacts.",
1311 )
1312 action_parser.add_argument(
1313 "--case-id", dest="case_ids", action="append",
1314 help="One numbered study member, such as case_0003; repeat to select several.",
1315 )
1316 action_parser.add_argument(
1317 "--completed", action="store_true",
1318 help="With --study-dir, select every finished member and skip the rest.",
1319 )
1320
1321 def add_retention_options(parser):
1322 """!
1323 @brief Add the per-component local-retention overrides to one action parser.
1324 @param[in] parser Action parser being configured.
1325 @return None.
1326 """
1327 parser.add_argument(
1328 "--retain", action="append", metavar="COMPONENT",
1329 help="Keep this component local regardless of --policy; repeatable and\n"
1330 "comma-separated. One of: " + ", ".join(STORAGE_RETENTION_COMPONENTS) + ".",
1331 )
1332 parser.add_argument(
1333 "--drop", action="append", metavar="COMPONENT",
1334 help="Prune this component locally regardless of --policy; same names as --retain.",
1335 )
1336
1337 status = actions.add_parser("status", help="Show local, protected, cold, and busy artifact state.")
1338 add_local_target(status)
1339 status.add_argument(
"--format", dest=
"output_format", choices=
list(CLI_OUTPUT_FORMATS), default=
"text")
1340
1341 plan = actions.add_parser("plan", help="Show packaging, dependencies, and safety checks without writing.")
1342 add_local_target(plan)
1343 add_profile_options(plan)
1344 plan.add_argument(
"--compression", choices=
list(STORAGE_COMPRESSION_POLICIES))
1345 plan.add_argument(
"--policy", choices=
list(STORAGE_OFFLOAD_POLICIES))
1346 add_retention_options(plan)
1347 plan.add_argument("--workers", type=int)
1348 plan_checkpoint = plan.add_mutually_exclusive_group()
1349 plan_checkpoint.add_argument("--keep-latest-checkpoint", dest="keep_latest_checkpoint", action="store_true")
1350 plan_checkpoint.add_argument("--drop-all-checkpoints", dest="keep_latest_checkpoint", action="store_false")
1351 plan.set_defaults(keep_latest_checkpoint=None)
1352
1353 for name, help_text in (
1354 ("protect", "Upload and verify an archive while retaining all local files."),
1355 ("offload", "Upload and verify an archive, then prune heavy local payload."),
1356 ):
1357 action_parser = actions.add_parser(name, help=help_text)
1358 add_local_target(action_parser)
1359 add_profile_options(action_parser)
1360 action_parser.add_argument(
1361 "--include-inputs", action="store_true",
1362 help="With --workspace, also archive user-supplied files under inputs/.",
1363 )
1364 action_parser.add_argument("--label", help="Human-readable searchable label.")
1365 action_parser.add_argument(
1366 "--notes", help="Free-text note recorded with the archive and shown by `show`."
1367 )
1368 action_parser.add_argument("--tag", dest="tags", action="append", help="Repeatable KEY=VALUE catalog tag.")
1369 action_parser.add_argument(
"--compression", choices=
list(STORAGE_COMPRESSION_POLICIES))
1370 action_parser.add_argument(
"--policy", choices=
list(STORAGE_OFFLOAD_POLICIES))
1371 add_retention_options(action_parser)
1372 action_parser.add_argument("--workers", type=int)
1373 checkpoint_group = action_parser.add_mutually_exclusive_group()
1374 checkpoint_group.add_argument("--keep-latest-checkpoint", dest="keep_latest_checkpoint", action="store_true")
1375 checkpoint_group.add_argument("--drop-all-checkpoints", dest="keep_latest_checkpoint", action="store_false")
1376 action_parser.set_defaults(keep_latest_checkpoint=None)
1377 action_parser.add_argument("--dry-run", action="store_true")
1378
1379 restore = actions.add_parser("restore", help="Restore a complete archive or selected checkpoints.")
1380 restore_source = restore.add_mutually_exclusive_group(required=True)
1381 restore_source.add_argument("--archive-id", help="Globally unique remote archive ID.")
1382 restore_source.add_argument(
1383 "--workspace-id", help="Restore a workspace archive by its recorded workspace identity."
1384 )
1385 restore_source.add_argument("--run-dir", help="Cold run containing a local storage marker.")
1386 restore_source.add_argument("--study-dir", help="Cold study containing a local storage marker.")
1387 restore.add_argument("--case-id", dest="case_ids", action="append")
1388 add_profile_options(restore)
1389 restore.add_argument("--to", dest="destination", help="Optional alternate restore destination.")
1390 restore.add_argument(
1391 "--checkpoint", dest="checkpoints", action="append", type=int,
1392 help="One committed step; repeat to select several.",
1393 )
1394 restore.add_argument(
1395 "--checkpoints", dest="checkpoint_ranges", action="append",
1396 help="An inclusive step range as START:END or START:END:STRIDE; repeatable.",
1397 )
1398 restore.add_argument(
1399 "--component", dest="components", action="append",
1400 choices=STORAGE_RESTORE_COMPONENTS,
1401 help="Restore one semantic component; repeat as needed.",
1402 )
1403 restore.add_argument("--force", action="store_true", help="Allow merge into a non-matching existing destination.")
1404 restore.add_argument("--workers", type=int, help="Parallel download/extraction workers.")
1405
1406 prune = actions.add_parser(
1407 "prune", help="Remove verified local asset objects that nothing local still needs."
1408 )
1409 prune.add_argument("--workspace", help="Workspace root; defaults to discovery from the cwd.")
1410 prune.add_argument(
1411 "--assets", action="store_true", required=True,
1412 help="Select the workspace asset store. Required: prune removes nothing else.",
1413 )
1414 prune.add_argument(
1415 "--unused-locally", action="store_true", required=True,
1416 help="Confirm that only objects with no active local run are removed.",
1417 )
1418 prune.add_argument("--dry-run", action="store_true", help="Report the decision only.")
1419 add_profile_options(prune)
1420
1421 verify = actions.add_parser("verify", help="Verify a remote archive completion marker and chunk checksums.")
1422 verify_source = verify.add_mutually_exclusive_group(required=True)
1423 verify_source.add_argument("--archive-id")
1424 verify_source.add_argument("--run-dir")
1425 verify_source.add_argument("--study-dir")
1426 verify.add_argument("--case-id", dest="case_ids", action="append")
1427 add_profile_options(verify)
1428
1429 list_parser = actions.add_parser("list", help="List/search remote archives without local directories.")
1430 add_profile_options(list_parser)
1431 list_parser.add_argument("--search", help="Case-insensitive search across IDs, labels, identities, and tags.")
1432 list_parser.add_argument(
1433 "--workspace-label", help="Show only archives belonging to this workspace identity."
1434 )
1435 list_parser.add_argument(
"--format", dest=
"output_format", choices=
list(CLI_OUTPUT_FORMATS), default=
"text")
1436
1437 show = actions.add_parser("show", help="Print the complete manifest for one archive.")
1438 show.add_argument("--archive-id", required=True)
1439 add_profile_options(show)
1440 return parser