cmpnd 0.59.0__tar.gz → 0.61.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (393) hide show
  1. {cmpnd-0.59.0 → cmpnd-0.61.0}/PKG-INFO +1 -1
  2. cmpnd-0.61.0/cmpnd/cli/commands/programs.py +599 -0
  3. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/valsets.py +29 -4
  4. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/verbs.py +1 -0
  5. {cmpnd-0.59.0 → cmpnd-0.61.0}/docs/cli.md +70 -13
  6. {cmpnd-0.59.0 → cmpnd-0.61.0}/pyproject.toml +1 -1
  7. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_main.py +3 -1
  8. cmpnd-0.61.0/tests/test_cli/test_programs.py +612 -0
  9. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_valsets.py +66 -3
  10. {cmpnd-0.59.0 → cmpnd-0.61.0}/uv.lock +1 -1
  11. cmpnd-0.59.0/cmpnd/cli/commands/programs.py +0 -330
  12. cmpnd-0.59.0/tests/test_cli/test_programs.py +0 -290
  13. {cmpnd-0.59.0 → cmpnd-0.61.0}/.gitignore +0 -0
  14. {cmpnd-0.59.0 → cmpnd-0.61.0}/AGENTS.md +0 -0
  15. {cmpnd-0.59.0 → cmpnd-0.61.0}/CLAUDE.md +0 -0
  16. {cmpnd-0.59.0 → cmpnd-0.61.0}/README.md +0 -0
  17. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/__init__.py +0 -0
  18. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/_assessments.py +0 -0
  19. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/_authored_import.py +0 -0
  20. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/_deployments.py +0 -0
  21. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/_exporter_pool.py +0 -0
  22. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/_gepa_patch.py +0 -0
  23. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/_http.py +0 -0
  24. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/_program_patch.py +0 -0
  25. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/_rlm_patch.py +0 -0
  26. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/_runtime_tools.py +0 -0
  27. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/_timespec.py +0 -0
  28. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/_traces.py +0 -0
  29. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/_valsets.py +0 -0
  30. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/authored.py +0 -0
  31. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/callback.py +0 -0
  32. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/__init__.py +0 -0
  33. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/api_client.py +0 -0
  34. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/__init__.py +0 -0
  35. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/admin.py +0 -0
  36. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/assessments.py +0 -0
  37. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/auth.py +0 -0
  38. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/datasets.py +0 -0
  39. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/deployments.py +0 -0
  40. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/evals.py +0 -0
  41. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/login.py +0 -0
  42. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/optimizations.py +0 -0
  43. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/org.py +0 -0
  44. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  45. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/signatures.py +0 -0
  46. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  47. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/top.py +0 -0
  48. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/commands/traces.py +0 -0
  49. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/credentials.py +0 -0
  50. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/deprecation.py +0 -0
  51. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/pagination.py +0 -0
  52. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/shell.py +0 -0
  53. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/trace_render.py +0 -0
  54. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/cli/tree_help.py +0 -0
  55. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/client.py +0 -0
  56. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/configuration.py +0 -0
  57. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/context.py +0 -0
  58. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/credentials.py +0 -0
  59. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/dataset_sync.py +0 -0
  60. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/datasets.py +0 -0
  61. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/decorators.py +0 -0
  62. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/deployment.py +0 -0
  63. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/deployment_metadata.py +0 -0
  64. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/deployment_runs.py +0 -0
  65. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/eval_handler.py +0 -0
  66. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/execution.py +0 -0
  67. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/exporter.py +0 -0
  68. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/exporter_http.py +0 -0
  69. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/helpers.py +0 -0
  70. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/identity.py +0 -0
  71. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/imaging.py +0 -0
  72. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/ir/__init__.py +0 -0
  73. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/ir/decode.py +0 -0
  74. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/ir/encode.py +0 -0
  75. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/ir/hash.py +0 -0
  76. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/ir/program.py +0 -0
  77. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/ir/reflection.py +0 -0
  78. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/ir/types.py +0 -0
  79. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/ir/xxh64.py +0 -0
  80. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/lm_configuration.py +0 -0
  81. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/mcp_servers.py +0 -0
  82. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/models.py +0 -0
  83. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/named_programs.py +0 -0
  84. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/optimization.py +0 -0
  85. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/optimizers/__init__.py +0 -0
  86. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/optimizers/gepa.py +0 -0
  87. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  88. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/optimizers/resume.py +0 -0
  89. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/optimizers/tracker.py +0 -0
  90. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/packaging.py +0 -0
  91. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/programs.py +0 -0
  92. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/references.py +0 -0
  93. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/retry.py +0 -0
  94. {cmpnd-0.59.0 → cmpnd-0.61.0}/cmpnd/traces.py +0 -0
  95. {cmpnd-0.59.0 → cmpnd-0.61.0}/docs/2026-04-02-code-review.md +0 -0
  96. {cmpnd-0.59.0 → cmpnd-0.61.0}/docs/client.md +0 -0
  97. {cmpnd-0.59.0 → cmpnd-0.61.0}/docs/custom-proposer.md +0 -0
  98. {cmpnd-0.59.0 → cmpnd-0.61.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  99. {cmpnd-0.59.0 → cmpnd-0.61.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  100. {cmpnd-0.59.0 → cmpnd-0.61.0}/docs/reading-traces.md +0 -0
  101. {cmpnd-0.59.0 → cmpnd-0.61.0}/docs/sdk-instrumentation.md +0 -0
  102. {cmpnd-0.59.0 → cmpnd-0.61.0}/examples/generalizing_proposer.py +0 -0
  103. {cmpnd-0.59.0 → cmpnd-0.61.0}/examples/local_ollama.py +0 -0
  104. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/LINEAGE.md +0 -0
  105. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/README.md +0 -0
  106. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  107. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  108. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
  109. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  110. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/program_names.py +0 -0
  111. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  112. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/py.typed +0 -0
  113. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  114. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/retry.py +0 -0
  115. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  116. {cmpnd-0.59.0 → cmpnd-0.61.0}/ingest/pyproject.toml +0 -0
  117. {cmpnd-0.59.0 → cmpnd-0.61.0}/scripts/audit-sdist.sh +0 -0
  118. {cmpnd-0.59.0 → cmpnd-0.61.0}/scripts/capture_review_run.py +0 -0
  119. {cmpnd-0.59.0 → cmpnd-0.61.0}/scripts/check-pip-install.sh +0 -0
  120. {cmpnd-0.59.0 → cmpnd-0.61.0}/scripts/check_release_artifacts.py +0 -0
  121. {cmpnd-0.59.0 → cmpnd-0.61.0}/scripts/fixtures/review_run.json.gz +0 -0
  122. {cmpnd-0.59.0 → cmpnd-0.61.0}/scripts/gepa_resume_bug.py +0 -0
  123. {cmpnd-0.59.0 → cmpnd-0.61.0}/scripts/install-test-env.sh +0 -0
  124. {cmpnd-0.59.0 → cmpnd-0.61.0}/scripts/release-stress.sh +0 -0
  125. {cmpnd-0.59.0 → cmpnd-0.61.0}/scripts/require_version_bump.py +0 -0
  126. {cmpnd-0.59.0 → cmpnd-0.61.0}/scripts/seed_review_run.py +0 -0
  127. {cmpnd-0.59.0 → cmpnd-0.61.0}/scripts/selfhost_compat.py +0 -0
  128. {cmpnd-0.59.0 → cmpnd-0.61.0}/scripts/test-in-linux.sh +0 -0
  129. {cmpnd-0.59.0 → cmpnd-0.61.0}/stubs/dspy/__init__.pyi +0 -0
  130. {cmpnd-0.59.0 → cmpnd-0.61.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  131. {cmpnd-0.59.0 → cmpnd-0.61.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  132. {cmpnd-0.59.0 → cmpnd-0.61.0}/stubs/dspy/utils/__init__.pyi +0 -0
  133. {cmpnd-0.59.0 → cmpnd-0.61.0}/stubs/dspy/utils/callback.pyi +0 -0
  134. {cmpnd-0.59.0 → cmpnd-0.61.0}/stubs/dspy/utils/dummies.pyi +0 -0
  135. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/__init__.py +0 -0
  136. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/_credential_route.py +0 -0
  137. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/_deployment_signature_helpers.py +0 -0
  138. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/_exporter_helpers.py +0 -0
  139. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/_fake_clock.py +0 -0
  140. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/_identity_helpers.py +0 -0
  141. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/_resolve_refs.py +0 -0
  142. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/_traced_run.py +0 -0
  143. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/conftest.py +0 -0
  144. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/__init__.py +0 -0
  145. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/committee_task.py +0 -0
  146. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/conftest.py +0 -0
  147. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  148. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  149. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/fake_lm.py +0 -0
  150. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/helpers.py +0 -0
  151. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/target_capabilities.py +0 -0
  152. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_assessments_cli.py +0 -0
  153. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_capture_prompts.py +0 -0
  154. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_composite_module.py +0 -0
  155. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  156. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_deploy_execute.py +0 -0
  157. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_deploy_pandas.py +0 -0
  158. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_deploy_programs.py +0 -0
  159. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_edge_cases.py +0 -0
  160. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_example_hash.py +0 -0
  161. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_gepa_optimization.py +0 -0
  162. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_interpreter_spans.py +0 -0
  163. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_lm_accuracy.py +0 -0
  164. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_module_tracing.py +0 -0
  165. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_optimization_lineage.py +0 -0
  166. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  167. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_optimize_failures.py +0 -0
  168. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_optimize_loop.py +0 -0
  169. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  170. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  171. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_parallel_export.py +0 -0
  172. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_project_attribution.py +0 -0
  173. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_react_tool_spans.py +0 -0
  174. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_save_load.py +0 -0
  175. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_trace_structure.py +0 -0
  176. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_traces_read.py +0 -0
  177. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_user_attribution.py +0 -0
  178. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/e2e/test_valsets_cli.py +0 -0
  179. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  180. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  181. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  182. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  183. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  184. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  185. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  186. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  187. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  188. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/10_react.json +0 -0
  189. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/11_refine.json +0 -0
  190. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/12_retrieve.json +0 -0
  191. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/13_knn.json +0 -0
  192. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/14_parallel.json +0 -0
  193. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/identity_vectors/README.md +0 -0
  194. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/ingest_conformance.py +0 -0
  195. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/integration/__init__.py +0 -0
  196. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/integration/authored_optimize/judge.py +0 -0
  197. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  198. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/integration/conftest.py +0 -0
  199. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  200. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/integration/hosted_optimize_probe.py +0 -0
  201. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/integration/optimization_run_probe.py +0 -0
  202. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  203. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/integration/test_logs_renders_traces.py +0 -0
  204. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/integration/test_optimization_run.py +0 -0
  205. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/integration/test_optimizing_progress.py +0 -0
  206. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/integration/test_unhappy_path.py +0 -0
  207. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/load/__init__.py +0 -0
  208. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/load/__main__.py +0 -0
  209. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/load/drain.py +0 -0
  210. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/load/ingest.py +0 -0
  211. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/load/latency_transport.py +0 -0
  212. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/load/rlm_shape.py +0 -0
  213. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/load/rlm_size_report.py +0 -0
  214. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/load/sweep.py +0 -0
  215. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/load/test_rlm_shape.py +0 -0
  216. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/load/test_rlm_size_report.py +0 -0
  217. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/__init__.py +0 -0
  218. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/conftest.py +0 -0
  219. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/seed_parity.py +0 -0
  220. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/test_auth.py +0 -0
  221. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/test_datasets.py +0 -0
  222. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/test_deployments.py +0 -0
  223. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/test_evals.py +0 -0
  224. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/test_health.py +0 -0
  225. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/test_modules.py +0 -0
  226. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/test_optimizations.py +0 -0
  227. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/test_programs.py +0 -0
  228. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/test_signatures.py +0 -0
  229. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/test_trace_streaming.py +0 -0
  230. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/parity/test_traces.py +0 -0
  231. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/span_reference_fixture.py +0 -0
  232. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/__init__.py +0 -0
  233. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/_provider.py +0 -0
  234. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/_readiness.py +0 -0
  235. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/_reliability.py +0 -0
  236. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/_result.py +0 -0
  237. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/mint_bedrock_token.py +0 -0
  238. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_deploy_programs.py +0 -0
  239. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_multi_client_stress.py +0 -0
  240. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_optimize_client_committee.py +0 -0
  241. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_optimize_loop.py +0 -0
  242. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  243. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_optimize_stress.py +0 -0
  244. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_provider_roles.py +0 -0
  245. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_readiness.py +0 -0
  246. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_real_provider_twins.py +0 -0
  247. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_reliability.py +0 -0
  248. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_reliability_classify.py +0 -0
  249. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_rlm_wasm.py +0 -0
  250. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/stress/test_trace_search.py +0 -0
  251. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_adapter_reentrancy.py +0 -0
  252. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_assessments_api.py +0 -0
  253. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_authored.py +0 -0
  254. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_authored_binding_credentials.py +0 -0
  255. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_authored_bundle_collisions.py +0 -0
  256. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_authored_metric_contract.py +0 -0
  257. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_authored_named_models.py +0 -0
  258. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_authored_optimize.py +0 -0
  259. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_callback.py +0 -0
  260. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_candidate_lineage.py +0 -0
  261. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/__init__.py +0 -0
  262. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/conftest.py +0 -0
  263. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_admin.py +0 -0
  264. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_api_client.py +0 -0
  265. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_assessments.py +0 -0
  266. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_auth.py +0 -0
  267. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_credentials.py +0 -0
  268. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_datasets.py +0 -0
  269. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_deployment_runs.py +0 -0
  270. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_deployments.py +0 -0
  271. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_evals.py +0 -0
  272. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_login.py +0 -0
  273. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_optimizations.py +0 -0
  274. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_org.py +0 -0
  275. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_pagination.py +0 -0
  276. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_parity.py +0 -0
  277. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_provider_credentials.py +0 -0
  278. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_schema_sample.py +0 -0
  279. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_shell.py +0 -0
  280. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_signatures.py +0 -0
  281. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_tool_servers.py +0 -0
  282. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_top.py +0 -0
  283. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_traces.py +0 -0
  284. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  285. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cli/test_tree_help.py +0 -0
  286. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_client.py +0 -0
  287. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_client_floor.py +0 -0
  288. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_client_program_names.py +0 -0
  289. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_clustering/__init__.py +0 -0
  290. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_clustering/conftest.py +0 -0
  291. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_clustering/test_edge_cases.py +0 -0
  292. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  293. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_clustering/test_type_conversion.py +0 -0
  294. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_cmp551_grouping_identity.py +0 -0
  295. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_configuration.py +0 -0
  296. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_context.py +0 -0
  297. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_context_propagation.py +0 -0
  298. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_credential_kwargs.py +0 -0
  299. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_credential_selection.py +0 -0
  300. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_credential_values.py +0 -0
  301. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_dataset_polling.py +0 -0
  302. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_dataset_sync.py +0 -0
  303. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_datasets.py +0 -0
  304. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_decision_client_spans.py +0 -0
  305. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_decorators.py +0 -0
  306. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_deploy.py +0 -0
  307. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  308. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_deploy_program_id.py +0 -0
  309. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_deployment_metadata.py +0 -0
  310. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_deployment_runs.py +0 -0
  311. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_deployments_read.py +0 -0
  312. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_eval.py +0 -0
  313. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_eval_example_attribution.py +0 -0
  314. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_eval_start.py +0 -0
  315. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_eval_trace_grouping.py +0 -0
  316. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_exception_paths.py +0 -0
  317. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_execute.py +0 -0
  318. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_execute_run_identity.py +0 -0
  319. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_exporter.py +0 -0
  320. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_exporter_program_names.py +0 -0
  321. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_exporter_queue.py +0 -0
  322. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_exporter_real.py +0 -0
  323. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_fake_lm.py +0 -0
  324. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_gepa_callback.py +0 -0
  325. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_gepa_integration.py +0 -0
  326. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_gepa_output_normalization.py +0 -0
  327. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_gepa_resume.py +0 -0
  328. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_identity.py +0 -0
  329. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_imaging.py +0 -0
  330. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_import_weight.py +0 -0
  331. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_ingest_conformance.py +0 -0
  332. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_ingest_second_producer.py +0 -0
  333. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_integration.py +0 -0
  334. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_interpreter_callback.py +0 -0
  335. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_ir_encoder_properties.py +0 -0
  336. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_ir_golden_vectors.py +0 -0
  337. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_ir_roundtrip.py +0 -0
  338. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_lm15_native_calls.py +0 -0
  339. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_lm_configuration.py +0 -0
  340. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_lm_usage_shapes.py +0 -0
  341. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_mcp_tools.py +0 -0
  342. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_models.py +0 -0
  343. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_multi_client_stress_readiness.py +0 -0
  344. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_optimization_progress.py +0 -0
  345. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_optimization_start.py +0 -0
  346. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_optimize.py +0 -0
  347. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_optimize_budget.py +0 -0
  348. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_optimize_lift.py +0 -0
  349. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_optimize_lms.py +0 -0
  350. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_optimize_proposer.py +0 -0
  351. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_optimize_reflection_lm.py +0 -0
  352. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_optimize_terminal_failure.py +0 -0
  353. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_packaging.py +0 -0
  354. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_packaging_callable_source.py +0 -0
  355. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_packaging_mcp_tools.py +0 -0
  356. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_packaging_program_source.py +0 -0
  357. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_poll_until.py +0 -0
  358. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_program_id_deprecation.py +0 -0
  359. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_program_identity.py +0 -0
  360. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_program_lineage_persist.py +0 -0
  361. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_program_names_in_deploy.py +0 -0
  362. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_programs_create.py +0 -0
  363. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_project_payload.py +0 -0
  364. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_real_provider_twins_readiness.py +0 -0
  365. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_reflector_properties.py +0 -0
  366. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_reflector_smoke.py +0 -0
  367. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_release_stress_script.py +0 -0
  368. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_replaced_names.py +0 -0
  369. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_repro_reactv2_bugs.py +0 -0
  370. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_require_version_bump.py +0 -0
  371. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_retry.py +0 -0
  372. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_reversible_failures.py +0 -0
  373. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_rlm_patch.py +0 -0
  374. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_run_attribution_paths.py +0 -0
  375. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_runtime_tools.py +0 -0
  376. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_schemas.py +0 -0
  377. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_seal_lm_credential.py +0 -0
  378. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_seed_review_run.py +0 -0
  379. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_signature_inputs.py +0 -0
  380. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_span_references.py +0 -0
  381. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_span_sent_once.py +0 -0
  382. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_system_one.py +0 -0
  383. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_trace_context_concurrency.py +0 -0
  384. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_trace_fact_wire_contract.py +0 -0
  385. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_trace_facts.py +0 -0
  386. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_trace_render.py +0 -0
  387. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_traces_read.py +0 -0
  388. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_tracked_gepa.py +0 -0
  389. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_tracker_enrich.py +0 -0
  390. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_truncation.py +0 -0
  391. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_upload_auth_headers.py +0 -0
  392. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_valsets_api.py +0 -0
  393. {cmpnd-0.59.0 → cmpnd-0.61.0}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.59.0
3
+ Version: 0.61.0
4
4
  Summary: DSPy observability and deployment SDK for cmpnd
5
5
  Project-URL: Homepage, https://cmpnd.ai
6
6
  Author-email: cmpnd <hello@cmpnd.ai>
@@ -0,0 +1,599 @@
1
+ """Programs commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ import urllib.parse
9
+ from argparse import ArgumentParser, _SubParsersAction
10
+ from typing import Any
11
+
12
+ from .. import api_client, pagination
13
+ from . import provider_credentials
14
+
15
+
16
+ # `show`, `traces`, `stats` and `usage` take a program name or a p_ id, as the
17
+ # Trash commands do. The `programs optimized` group and `select` take a compiled
18
+ # program hash, the value `programs optimized list` prints as `program_hash`.
19
+ # The routes resolve that hash against optimization runs, so a program
20
+ # signature hash 404s there. A compiled hash and a program name have the same
21
+ # shape, so no command takes both in one slot.
22
+ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
23
+ parser = subparsers.add_parser("programs", help="Create and browse programs")
24
+ sub = parser.add_subparsers(dest="action")
25
+
26
+ create = sub.add_parser(
27
+ "create",
28
+ help="Create a program with a name",
29
+ description=(
30
+ "Create a program with PROGRAM_NAME in your organization. A program name that already "
31
+ "has a program is refused. A deploy makes the program the first time it uses the program "
32
+ "name, so this command is needed only to make the program first. Deploy under the program "
33
+ "name with `cmpnd deploy SOURCE PROGRAM_NAME`, and declare it in Python with "
34
+ "cmpnd.Program(PROGRAM_NAME, module=..., lm=...)."
35
+ ),
36
+ )
37
+ create.add_argument(
38
+ "name", nargs="?", default=None, metavar="PROGRAM_NAME", help="Program name, unique in your organization"
39
+ )
40
+ # Deprecated program id input: --name, the display name of an unnamed program, is refused, and goes when the
41
+ # deprecated inputs are stripped out. It stays declared so that a command that passes it gets an error naming
42
+ # the program name form.
43
+ create.add_argument(
44
+ "--name",
45
+ dest="display_name",
46
+ default=None,
47
+ help=argparse.SUPPRESS,
48
+ )
49
+ create.add_argument("--json", action="store_true", help="Emit the created program as JSON instead of its id")
50
+ create.set_defaults(func=handle_create)
51
+
52
+ ls = sub.add_parser(
53
+ "list",
54
+ help="List your organization's programs",
55
+ description=(
56
+ "List your organization's live programs, newest first, with each program's name and p_ id. A program "
57
+ "lists from its creation on, before anything runs under it. With --trashed, list the Trash instead."
58
+ ),
59
+ )
60
+ ls.add_argument(
61
+ "--trashed",
62
+ action="store_true",
63
+ help="List the programs in the Trash instead, with when each is purged",
64
+ )
65
+ pagination.add_args(ls, with_page_aliases=True, default_limit=50)
66
+ # The optimized-program filters and sort moved to `programs optimized list`. They stay declared so that a
67
+ # command that passes one gets an error naming the command that takes it.
68
+ for flag, dest in [
69
+ ("--name", "program_name"),
70
+ ("--model", "model_name"),
71
+ ("--order-by", "order_by"),
72
+ ("--order", "order"),
73
+ ]:
74
+ ls.add_argument(flag, dest=dest, default=None, help=argparse.SUPPRESS)
75
+ ls.set_defaults(func=handle_list)
76
+
77
+ # The optimized programs are the compiled programs optimization runs produced, grouped by compiled program
78
+ # hash. They are different records from the org's programs, so they nest, as the registry does.
79
+ optimized = sub.add_parser("optimized", help="Browse the optimized programs that optimization runs produced")
80
+ optimized_sub = optimized.add_subparsers(dest="optimized_action")
81
+ opt_list = optimized_sub.add_parser(
82
+ "list",
83
+ help="List optimized programs by compiled program hash",
84
+ description=(
85
+ "List the compiled programs your optimization runs produced, one row per compiled program hash, with "
86
+ "their run counts and scores. `programs optimized show`, `traces`, `stats` and `usage` take the hash "
87
+ "from the "
88
+ "program_hash column."
89
+ ),
90
+ )
91
+ opt_list.add_argument("--name", dest="program_name", help="Filter by program name")
92
+ opt_list.add_argument("--model", dest="model_name", help="Filter by model name")
93
+ opt_list.add_argument("--order-by", default="run_count", help="Sort field (default: run_count)")
94
+ opt_list.add_argument("--order", default="desc", choices=["asc", "desc"], help="Sort direction")
95
+ pagination.add_args(opt_list, with_page_aliases=True, default_limit=50)
96
+ opt_list.set_defaults(func=handle_optimized_list)
97
+
98
+ opt_show = optimized_sub.add_parser("show", help="Show the optimization runs that produced a compiled program")
99
+ opt_show.add_argument("hash", help=_COMPILED_HASH_HELP)
100
+ opt_show.set_defaults(func=handle_optimized_show)
101
+
102
+ opt_traces = optimized_sub.add_parser("traces", help="List the traces of a compiled program's program")
103
+ opt_traces.add_argument("hash", help=_COMPILED_HASH_HELP)
104
+ pagination.add_args(opt_traces, with_page_aliases=False, default_limit=50)
105
+ opt_traces.set_defaults(func=handle_optimized_traces)
106
+
107
+ opt_stats = optimized_sub.add_parser("stats", help="Show the execution statistics of a compiled program's program")
108
+ opt_stats.add_argument("hash", help=_COMPILED_HASH_HELP)
109
+ opt_stats.set_defaults(func=handle_optimized_stats)
110
+
111
+ opt_usage = optimized_sub.add_parser("usage", help="Show the usage over time of a compiled program's program")
112
+ opt_usage.add_argument("hash", help=_COMPILED_HASH_HELP)
113
+ _add_usage_window(opt_usage)
114
+ opt_usage.set_defaults(func=handle_optimized_usage)
115
+
116
+ show = sub.add_parser(
117
+ "show",
118
+ help="Show a program",
119
+ description=(
120
+ "Show PROGRAM: its name, p_ id, whether it is in the Trash, its trace count and latest trace, and "
121
+ "the optimized programs its optimization runs produced."
122
+ ),
123
+ )
124
+ show.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
125
+ show.set_defaults(func=handle_show)
126
+
127
+ tr = sub.add_parser(
128
+ "traces",
129
+ help="List a program's traces, newest first, over the widest window trace search reads",
130
+ description=(
131
+ "List PROGRAM's traces, newest first, through trace search over its widest window, 365 days. "
132
+ "`cmpnd traces list --hash ID --since DATE` sets the window itself. --limit is at most 500."
133
+ ),
134
+ )
135
+ tr.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
136
+ pagination.add_args(tr, with_page_aliases=False, default_limit=50)
137
+ tr.set_defaults(func=handle_traces)
138
+
139
+ stats = sub.add_parser("stats", help="Show a program's execution statistics over all history")
140
+ stats.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
141
+ stats.set_defaults(func=handle_stats)
142
+
143
+ usage = sub.add_parser("usage", help="Show a program's trace volume, tokens and cost over time")
144
+ usage.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
145
+ _add_usage_window(usage)
146
+ usage.set_defaults(func=handle_usage)
147
+
148
+ # The registry is the programs an SDK client registered, as distinct from
149
+ # the optimized programs `programs list` aggregates out of runs — different
150
+ # records, so they nest rather than share a flat verb.
151
+ registry = sub.add_parser("registry", help="Browse the program registry")
152
+ registry_sub = registry.add_subparsers(dest="registry_action")
153
+
154
+ reg_list = registry_sub.add_parser("list", help="List registered programs")
155
+ pagination.add_args(reg_list, with_page_aliases=False, default_limit=50)
156
+ reg_list.set_defaults(func=handle_registry_list)
157
+
158
+ reg_show = registry_sub.add_parser("show", help="Show a registry entry and its signatures")
159
+ reg_show.add_argument("id", help="Registry ID")
160
+ reg_show.set_defaults(func=handle_registry_show)
161
+
162
+ # The Trash. Each command takes a program name or a p_ id. A program in the
163
+ # Trash leaves every view and frees its place under the plan's program limit,
164
+ # and stays restorable until it is purged.
165
+ trash = sub.add_parser(
166
+ "trash",
167
+ help="Move a program to the Trash",
168
+ description=(
169
+ "Move PROGRAM to the Trash. It leaves every view and no longer counts toward your plan's program "
170
+ "limit. Restore it with `cmpnd programs restore PROGRAM` before it is purged, 7 days later."
171
+ ),
172
+ )
173
+ trash.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
174
+ trash.set_defaults(func=handle_trash)
175
+
176
+ restore = sub.add_parser("restore", help="Restore a program from the Trash")
177
+ restore.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
178
+ restore.set_defaults(func=handle_restore)
179
+
180
+ purge = sub.add_parser(
181
+ "purge",
182
+ help="Delete a program in the Trash for good",
183
+ description=(
184
+ "Delete PROGRAM, which must be in the Trash, for good, or every program in the Trash with --all. "
185
+ "Nothing restores a purged program."
186
+ ),
187
+ )
188
+ purge.add_argument("program", metavar="PROGRAM", nargs="?", default=None, help="Program name or p_ id")
189
+ purge.add_argument("--all", action="store_true", help="Purge every program in the Trash")
190
+ purge.add_argument("--force", action="store_true", help="Skip the confirmation (required when not on a terminal)")
191
+ purge.set_defaults(func=handle_purge)
192
+
193
+ select = sub.add_parser("select", help="Pin an optimization run as this program's chosen version")
194
+ select.add_argument("id", help=_COMPILED_HASH_HELP)
195
+ select.add_argument("--run", dest="run_id", required=True, help="Optimization run ID (op_...) to pin")
196
+ select.set_defaults(func=handle_select)
197
+
198
+
199
+ _COMPILED_HASH_HELP = "Compiled program hash, as `programs optimized list` prints it in the program_hash column"
200
+
201
+
202
+ def _add_usage_window(parser: ArgumentParser) -> None:
203
+ parser.add_argument("--hours", type=int, default=24, help="Rolling window in hours (default %(default)s)")
204
+ # Constrained on purpose: an unrecognised period buckets hourly server-side
205
+ # rather than failing, so a typo would answer a different question silently.
206
+ parser.add_argument(
207
+ "--period",
208
+ choices=["minute", "5m", "10m", "30m", "hour", "day"],
209
+ help="Bucket width (default: hour)",
210
+ )
211
+
212
+
213
+ def _usage_params(args: Any) -> dict[str, str]:
214
+ params = {"hours": str(args.hours)}
215
+ if args.period:
216
+ params["period"] = args.period
217
+ return params
218
+
219
+
220
+ def handle_create(client: api_client.ApiClient, args: Any) -> Any:
221
+ """Create the program named ``args.name``, and refuse a name that already has one.
222
+
223
+ Prints the bare id on stdout and says what it made on stderr; ``--json`` returns
224
+ serve's whole answer instead, printed like the group's reads. The program name is
225
+ the only form: a create without one, and ``--name``, are refused.
226
+ """
227
+ from ... import programs
228
+ from . import top
229
+
230
+ # Deprecated program id input: both refusals go when the deprecated inputs are stripped out.
231
+ if args.display_name is not None:
232
+ top.refuse_deprecated_program_id_input("`cmpnd programs create --name`", top.hint_programs_create())
233
+ if args.name is None:
234
+ top.refuse_deprecated_program_id_input(
235
+ "`cmpnd programs create` without a program name", top.hint_programs_create()
236
+ )
237
+ try:
238
+ answer = programs.send_resolve(client, args.name)
239
+ except ValueError as exc:
240
+ raise api_client.CliError(str(exc)) from exc
241
+ if answer is None:
242
+ raise api_client.CliError(
243
+ "This server has no program names, so it cannot create a program from a program name. A serve from "
244
+ "before program names, such as self-host release 2026-001, needs no program first: "
245
+ f"`{top._hint_program_name('deploy', source='SOURCE', name='PROGRAM_NAME')}` deploys there without "
246
+ "a program id."
247
+ )
248
+ if not answer.get("created"):
249
+ raise api_client.CliError(
250
+ f"the program name {args.name!r} already has a program, {answer['program_id']}. "
251
+ f"Deploy under it with `{top._hint_program_name('deploy', source='SOURCE', name=args.name)}`."
252
+ )
253
+ if getattr(args, "json", False):
254
+ return answer
255
+ print(answer["program_id"])
256
+ print(f"created program {answer['program_id']} named {args.name}", file=sys.stderr)
257
+ return None
258
+
259
+
260
+ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
261
+ """List the org's live programs by name and p_ id, or the Trash with ``--trashed``."""
262
+ for attr, flag in [
263
+ ("program_name", "--name"),
264
+ ("model_name", "--model"),
265
+ ("order_by", "--order-by"),
266
+ ("order", "--order"),
267
+ ]:
268
+ if getattr(args, attr, None) is not None:
269
+ from . import top
270
+
271
+ raise api_client.CliError(
272
+ f"{flag} filters the optimized programs, which `{top.hint_programs('optimized', 'list')}` lists. "
273
+ f"`{top.hint_programs('list')}` lists your organization's programs by name and p_ id."
274
+ )
275
+ if getattr(args, "trashed", False):
276
+ # The Trash is its own short list, unpaged and newest first, so paging does not apply. A flag left at its
277
+ # default cannot be told apart from one not passed.
278
+ for attr, flag, default in [
279
+ ("limit", "--limit", 50),
280
+ ("offset", "--offset", 0),
281
+ ("per_page", "--per-page", None),
282
+ ("page", "--page", None),
283
+ ]:
284
+ if getattr(args, attr, default) != default:
285
+ raise api_client.CliError(f"{flag} does not apply to --trashed, which lists the whole Trash")
286
+ return _trash_listing(client)
287
+ params = pagination.wire(args, "limit_offset", as_str=True)
288
+ # Read as text: a server from before the program list, such as self-host release 2026-001, redirects this
289
+ # path to its dispatcher, and the redirect's body is HTML.
290
+ try:
291
+ raw = client.get_text("/api/v1/programs", params=params)
292
+ except api_client.CliError as exc:
293
+ if exc.status_code not in (404, 405):
294
+ raise
295
+ return _list_recorded(client, params)
296
+ try:
297
+ listed = json.loads(raw)
298
+ except ValueError:
299
+ listed = None
300
+ if isinstance(listed, dict) and "programs" in listed:
301
+ return listed
302
+ return _list_recorded(client, params)
303
+
304
+
305
+ # GET /api/v1/modules on a server from before the program list answers at most this many programs
306
+ # (modulesPageCap in self-host release 2026-001), and nothing pages past them.
307
+ _RECORDED_PROGRAMS_CAP = 200
308
+
309
+
310
+ def _list_recorded(client: api_client.ApiClient, params: dict[str, str]) -> dict[str, Any]:
311
+ """List the programs a server from before the program list recorded traces for, paged here.
312
+
313
+ GET /api/v1/modules answers them in one array, by name and legacy hash, up to its cap. A program with no
314
+ traces yet is not in it. A full array may be missing programs past the cap, so the answer then has no total
315
+ and says it is incomplete.
316
+ """
317
+ rows = client.get("/api/v1/modules")
318
+ if not isinstance(rows, list):
319
+ raise api_client.CliError(
320
+ "this cmpnd server answered neither the program list nor the list of recorded programs"
321
+ )
322
+ complete = len(rows) < _RECORDED_PROGRAMS_CAP
323
+ note = "note: this cmpnd server predates the program list, so this lists the programs it recorded traces for."
324
+ if not complete:
325
+ note += f" It answers at most {_RECORDED_PROGRAMS_CAP} of them, so programs past those are missing."
326
+ print(note + " Upgrade the server to list every program by name and p_ id.", file=sys.stderr)
327
+ limit, offset = int(params["limit"]), int(params["offset"])
328
+ return {
329
+ "programs": rows[offset : offset + limit],
330
+ "total": len(rows) if complete else None,
331
+ "complete": complete,
332
+ "limit": limit,
333
+ "offset": offset,
334
+ "has_more": offset + limit < len(rows),
335
+ }
336
+
337
+
338
+ def handle_optimized_list(client: api_client.ApiClient, args: Any) -> Any:
339
+ """List the optimized programs, one row per compiled program hash."""
340
+ params: dict[str, str] = pagination.wire(args, "per_page", as_str=True)
341
+ params["order_by"] = args.order_by
342
+ params["order_direction"] = args.order
343
+ if args.program_name:
344
+ params["program_name"] = args.program_name
345
+ if args.model_name:
346
+ params["model_name"] = args.model_name
347
+ return client.get("/api/v1/programs/v2", params=params)
348
+
349
+
350
+ # A server from before program names, p_ ids and the Trash API, such as self-host release 2026-001, reads every
351
+ # path under /api/v1/programs/ as a compiled program hash. A GET there answers 404 and a POST answers 405, and its
352
+ # trace search ignores program_id. A current server answers the Trash listing with 200, so that read tells them
353
+ # apart once a command has already failed.
354
+ _OLD_SERVER = "this cmpnd server predates program names, p_ ids and the Trash API, as self-host release 2026-001 does."
355
+
356
+
357
+ def _predates_program_ids(client: api_client.ApiClient) -> bool:
358
+ try:
359
+ client.get("/api/v1/programs/trash")
360
+ except api_client.CliError as exc:
361
+ return exc.status_code == 404
362
+ return False
363
+
364
+
365
+ def _old_server_refusal(remedy: str) -> api_client.CliError:
366
+ return api_client.CliError(f"{_OLD_SERVER} {remedy}")
367
+
368
+
369
+ def _refuse_if_old(client: api_client.ApiClient, exc: api_client.CliError, remedy: str) -> None:
370
+ """Raise the old-server refusal in place of ``exc`` when the server predates program ids."""
371
+ if exc.status_code in (404, 405) and _predates_program_ids(client):
372
+ raise _old_server_refusal(remedy) from exc
373
+
374
+
375
+ def _trash_remedy() -> str:
376
+ return "Manage its Trash in the web app, at /ui/account/trash, or upgrade the server."
377
+
378
+
379
+ def _read_remedy(verb: str) -> str:
380
+ from . import top
381
+
382
+ return (
383
+ f"`{top.hint_programs('optimized', verb, 'HASH')}` reads a compiled program hash there, and "
384
+ f"`{top.hint_programs('list')}` lists the programs it recorded traces for."
385
+ )
386
+
387
+
388
+ def _resolve_for_read(client: api_client.ApiClient, ref: str, verb: str, *, trashed_ok: bool = False) -> str:
389
+ """Return the p_ id of the program ``ref`` names, for a read of its activity.
390
+
391
+ A program in the Trash has no activity to read, so it is refused unless
392
+ ``trashed_ok``. An unknown name may be a compiled program hash, which the
393
+ ``programs optimized`` command of the same verb takes.
394
+ """
395
+ from . import top
396
+
397
+ program_id, trashed = resolve(
398
+ client,
399
+ ref,
400
+ unknown_hint=f"A compiled program hash goes to `{top.hint_programs('optimized', verb, ref)}`.",
401
+ old_server_remedy=_read_remedy(verb),
402
+ )
403
+ if trashed and not trashed_ok:
404
+ raise api_client.CliError(
405
+ f"{ref} is in the Trash, so it has no activity to read. "
406
+ f"Restore it with `{top.hint_programs('restore', ref)}`."
407
+ )
408
+ return program_id
409
+
410
+
411
+ def _read(client: api_client.ApiClient, verb: str, path: str, params: dict[str, str] | None = None) -> Any:
412
+ try:
413
+ return client.get(path) if params is None else client.get(path, params=params)
414
+ except api_client.CliError as exc:
415
+ _refuse_if_old(client, exc, _read_remedy(verb))
416
+ raise
417
+
418
+
419
+ def handle_show(client: api_client.ApiClient, args: Any) -> Any:
420
+ program_id = _resolve_for_read(client, args.program, "show", trashed_ok=True)
421
+ return _read(client, "show", f"/api/v1/programs/{program_id}")
422
+
423
+
424
+ # Trace search reads a page_size above this as its default of 50, which would answer other rows than the ones asked
425
+ # for.
426
+ _MAX_TRACE_PAGE = 500
427
+
428
+
429
+ def handle_traces(client: api_client.ApiClient, args: Any) -> Any:
430
+ """List the program's traces through trace search, which takes a program id, over its widest window."""
431
+ if args.limit > _MAX_TRACE_PAGE:
432
+ raise api_client.CliError(
433
+ f"--limit is at most {_MAX_TRACE_PAGE}, the largest page trace search returns. Page with --offset."
434
+ )
435
+ program_id = _resolve_for_read(client, args.program, "traces")
436
+ body: dict[str, Any] = {"program_id": program_id, **pagination.wire(args, "page_size"), "max_window": True}
437
+ answer = client.post("/api/v1/traces/search", json=body)
438
+ # A server that ignores program_id answers every trace in the org, which must not pass for the program's.
439
+ if "program_id" in (answer.get("ignored_fields") or []):
440
+ raise _old_server_refusal(_read_remedy("traces"))
441
+ return answer
442
+
443
+
444
+ def handle_stats(client: api_client.ApiClient, args: Any) -> Any:
445
+ program_id = _resolve_for_read(client, args.program, "stats")
446
+ return _read(client, "stats", f"/api/v1/programs/{program_id}/execution-stats")
447
+
448
+
449
+ def handle_usage(client: api_client.ApiClient, args: Any) -> Any:
450
+ program_id = _resolve_for_read(client, args.program, "usage")
451
+ return _read(client, "usage", f"/api/v1/programs/{program_id}/usage-history", _usage_params(args))
452
+
453
+
454
+ def handle_optimized_show(client: api_client.ApiClient, args: Any) -> Any:
455
+ return client.get(f"/api/v1/programs/{args.hash}")
456
+
457
+
458
+ def handle_optimized_traces(client: api_client.ApiClient, args: Any) -> Any:
459
+ params = pagination.wire(args, "limit_offset", as_str=True)
460
+ return client.get(f"/api/v1/programs/{args.hash}/traces", params=params)
461
+
462
+
463
+ def handle_optimized_stats(client: api_client.ApiClient, args: Any) -> Any:
464
+ return client.get(f"/api/v1/programs/{args.hash}/execution-stats")
465
+
466
+
467
+ def handle_optimized_usage(client: api_client.ApiClient, args: Any) -> Any:
468
+ return client.get(f"/api/v1/programs/{args.hash}/usage-history", params=_usage_params(args))
469
+
470
+
471
+ def handle_registry_list(client: api_client.ApiClient, args: Any) -> Any:
472
+ # This route reads limit/offset directly, unlike the page-numbered
473
+ # `programs list`.
474
+ params = pagination.wire(args, "limit_offset", as_str=True)
475
+ return client.get("/api/v1/programs/registry", params=params)
476
+
477
+
478
+ def handle_registry_show(client: api_client.ApiClient, args: Any) -> Any:
479
+ return client.get(f"/api/v1/programs/registry/{args.id}")
480
+
481
+
482
+ def handle_select(client: api_client.ApiClient, args: Any) -> Any:
483
+ """Pin one optimization run as the program's chosen version.
484
+
485
+ A run-scoped credential is refused by the server: this repoints any program
486
+ in the organization at any run, so it is a person's decision to make and the
487
+ audit trail records who made it.
488
+ """
489
+ return client.post(f"/api/v1/programs/{args.id}/select", json={"optimization_run_id": args.run_id})
490
+
491
+
492
+ def resolve(
493
+ client: api_client.ApiClient, ref: str, *, unknown_hint: str = "", old_server_remedy: str = ""
494
+ ) -> tuple[str, bool | None]:
495
+ """Return the program id ``ref`` names, and whether it is in the Trash when known.
496
+
497
+ A ``p_`` id is used as given, because no program name starts with ``p_``, and its
498
+ Trash state is unknown until the route answers. A name is looked up, which never
499
+ makes a program, and the answer says whether it is in the Trash. ``unknown_hint``
500
+ follows the error for a name no program has. On a server from before program
501
+ names the lookup fails too, and the error is the old-server refusal with
502
+ ``old_server_remedy``.
503
+ """
504
+ from ... import programs
505
+
506
+ if ref.lower().startswith("p_"):
507
+ return ref, None
508
+ try:
509
+ programs.validate_program_name(ref)
510
+ except ValueError as exc:
511
+ raise api_client.CliError(str(exc)) from exc
512
+ try:
513
+ answer = client.get("/api/v1/programs/by-name/" + urllib.parse.quote(ref, safe=""))
514
+ except api_client.CliError as exc:
515
+ if exc.status_code == 404:
516
+ _refuse_if_old(client, exc, old_server_remedy)
517
+ message = f"no program in your organization is named {ref!r}"
518
+ raise api_client.CliError(f"{message}. {unknown_hint}" if unknown_hint else message) from exc
519
+ if exc.status_code == 410:
520
+ raise api_client.CliError(f"{ref!r} belongs to a purged program, which nothing restores") from exc
521
+ raise
522
+ return answer["program_id"], bool(answer.get("trashed"))
523
+
524
+
525
+ def handle_trash(client: api_client.ApiClient, args: Any) -> Any:
526
+ """Move a program to the Trash, which frees its place under the plan's program limit."""
527
+ from . import top
528
+
529
+ program_id, trashed = resolve(client, args.program, old_server_remedy=_trash_remedy())
530
+ if trashed:
531
+ print(
532
+ f"{args.program} is already in the Trash. Restore it with `{top.hint_programs('restore', args.program)}`.",
533
+ file=sys.stderr,
534
+ )
535
+ return {"program_id": program_id, "trashed": True}
536
+ return _trash_write(client, f"/api/v1/programs/{program_id}/trash")
537
+
538
+
539
+ def handle_restore(client: api_client.ApiClient, args: Any) -> Any:
540
+ """Return a program from the Trash. The server refuses it when the plan's program limit is full."""
541
+ from . import top
542
+
543
+ program_id, trashed = resolve(client, args.program, old_server_remedy=_trash_remedy())
544
+ if trashed is False:
545
+ # A name another program took from a Trashed one reaches the new, live program. The Trashed one is
546
+ # restored by its id.
547
+ raise api_client.CliError(
548
+ f"{args.program} is not in the Trash. If its name now belongs to another program, restore the one "
549
+ f"in the Trash by the p_ id that `{top.hint_programs('list', '--trashed')}` prints."
550
+ )
551
+ return _trash_write(client, f"/api/v1/programs/{program_id}/restore")
552
+
553
+
554
+ def handle_purge(client: api_client.ApiClient, args: Any) -> Any:
555
+ """Delete one program in the Trash, or the whole Trash with ``--all``, for good."""
556
+ from . import top
557
+
558
+ consequence = "Nothing restores a purged program, and its traces stay hidden."
559
+ if args.all:
560
+ if args.program is not None:
561
+ raise api_client.CliError("name either one PROGRAM or --all, not both")
562
+ # The count is for the person confirming. --force skips the listing, which needs traces:read, so a key
563
+ # that may only write can still empty the Trash.
564
+ if not args.force:
565
+ listed = _trash_listing(client)
566
+ count = len(listed.get("programs") or [])
567
+ if count == 0:
568
+ return {"purged": 0, "kept": 0}
569
+ noun = "program" if count == 1 else "programs"
570
+ provider_credentials.confirm(args, f"the {count} {noun} in the Trash for good", consequence)
571
+ return _trash_write(client, "/api/v1/programs/trash/empty")
572
+ if args.program is None:
573
+ raise api_client.CliError("name the PROGRAM to purge, or pass --all to empty the Trash")
574
+ program_id, trashed = resolve(client, args.program, old_server_remedy=_trash_remedy())
575
+ if trashed is False:
576
+ raise api_client.CliError(
577
+ f"{args.program} is live, and only a program in the Trash can be purged. "
578
+ f"Move it there first with `{top.hint_programs('trash', args.program)}`."
579
+ )
580
+ provider_credentials.confirm(args, f"the program {args.program} for good", consequence)
581
+ return _trash_write(client, f"/api/v1/programs/{program_id}/purge")
582
+
583
+
584
+ def _trash_listing(client: api_client.ApiClient) -> Any:
585
+ """GET the Trash. On a server from before the Trash API it answers 404, which is the old-server signal."""
586
+ try:
587
+ return client.get("/api/v1/programs/trash")
588
+ except api_client.CliError as exc:
589
+ if exc.status_code == 404:
590
+ raise _old_server_refusal(_trash_remedy()) from exc
591
+ raise
592
+
593
+
594
+ def _trash_write(client: api_client.ApiClient, path: str) -> Any:
595
+ try:
596
+ return client.post(path)
597
+ except api_client.CliError as exc:
598
+ _refuse_if_old(client, exc, _trash_remedy())
599
+ raise
@@ -8,6 +8,7 @@ inputs and expected outputs of one validation example.
8
8
  from __future__ import annotations
9
9
 
10
10
  import json
11
+ import re
11
12
  from argparse import ArgumentParser, _SubParsersAction
12
13
  from typing import Any
13
14
 
@@ -23,7 +24,7 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
23
24
  create.add_argument(
24
25
  "program_hash",
25
26
  metavar="program",
26
- help="Owning program: a program id (p_…), or a legacy signature hash "
27
+ help="Owning program: a program name, a program id (p_…), or a legacy signature hash "
27
28
  "(16-hex, as `cmpnd signatures list` shows)",
28
29
  )
29
30
  create.add_argument(
@@ -33,7 +34,7 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
33
34
  create.set_defaults(func=handle_create)
34
35
 
35
36
  ls = sub.add_parser("list", help="List a program's validation sets")
36
- ls.add_argument("program", help="Program id (p_…), or a legacy signature hash (16-hex)")
37
+ ls.add_argument("program", help="Program name, program id (p_…), or a legacy signature hash (16-hex)")
37
38
  pagination.add_args(ls, with_page_aliases=False, default_limit=50)
38
39
  ls.set_defaults(func=handle_list)
39
40
 
@@ -75,15 +76,39 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
75
76
  edelete.set_defaults(func=handle_entries_delete)
76
77
 
77
78
 
79
+ # A legacy program signature hash in its display form: 16 hex digits.
80
+ _LEGACY_HASH = re.compile(r"[0-9a-fA-F]{16}")
81
+
82
+
83
+ def _program(client: api_client.ApiClient, ref: str) -> str:
84
+ """The value the valsets routes take for ``ref``.
85
+
86
+ A p_ id and a legacy 16-hex hash pass unchanged. Anything else is a program
87
+ name, which resolves to its p_ id without making a program. A program whose
88
+ name is 16 hex digits is reached by its p_ id.
89
+ """
90
+ from . import programs
91
+
92
+ if _LEGACY_HASH.fullmatch(ref):
93
+ return ref
94
+ program_id, _ = programs.resolve(
95
+ client,
96
+ ref,
97
+ old_server_remedy="Name the program by its 16-hex signature hash, as `cmpnd signatures list` shows, "
98
+ "or upgrade the server.",
99
+ )
100
+ return program_id
101
+
102
+
78
103
  def handle_create(client: api_client.ApiClient, args: Any) -> Any:
79
104
  # One generator (cmpnd._valsets.default_name) for both the CLI and the
80
105
  # Client, so the collision-avoiding suffix can't drift between them.
81
106
  name = args.name or _valsets.default_name()
82
- return client.post("/api/v1/valsets", json={"program": args.program_hash, "name": name})
107
+ return client.post("/api/v1/valsets", json={"program": _program(client, args.program_hash), "name": name})
83
108
 
84
109
 
85
110
  def handle_list(client: api_client.ApiClient, args: Any) -> Any:
86
- params = {"program": args.program, **pagination.wire(args, "limit_offset", as_str=True)}
111
+ params = {"program": _program(client, args.program), **pagination.wire(args, "limit_offset", as_str=True)}
87
112
  return client.get("/api/v1/valsets", params=params)
88
113
 
89
114