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