cmpnd 0.53.0__tar.gz → 0.54.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 (381) hide show
  1. {cmpnd-0.53.0 → cmpnd-0.54.0}/AGENTS.md +4 -2
  2. {cmpnd-0.53.0 → cmpnd-0.54.0}/PKG-INFO +1 -1
  3. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/admin.py +172 -3
  4. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/optimizations.py +35 -1
  5. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/optimization.py +18 -0
  6. {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/cli.md +51 -0
  7. {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/sdk-instrumentation.md +1 -1
  8. {cmpnd-0.53.0 → cmpnd-0.54.0}/pyproject.toml +1 -1
  9. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_admin.py +135 -0
  10. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_optimizations.py +35 -0
  11. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize.py +34 -0
  12. {cmpnd-0.53.0 → cmpnd-0.54.0}/uv.lock +1 -1
  13. {cmpnd-0.53.0 → cmpnd-0.54.0}/.gitignore +0 -0
  14. {cmpnd-0.53.0 → cmpnd-0.54.0}/CLAUDE.md +0 -0
  15. {cmpnd-0.53.0 → cmpnd-0.54.0}/README.md +0 -0
  16. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/__init__.py +0 -0
  17. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_assessments.py +0 -0
  18. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_authored_import.py +0 -0
  19. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_deployments.py +0 -0
  20. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_exporter_pool.py +0 -0
  21. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_gepa_patch.py +0 -0
  22. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_http.py +0 -0
  23. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_program_patch.py +0 -0
  24. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_rlm_patch.py +0 -0
  25. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_runtime_tools.py +0 -0
  26. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_timespec.py +0 -0
  27. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_traces.py +0 -0
  28. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_valsets.py +0 -0
  29. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/authored.py +0 -0
  30. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/callback.py +0 -0
  31. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/__init__.py +0 -0
  32. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/api_client.py +0 -0
  33. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/__init__.py +0 -0
  34. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/assessments.py +0 -0
  35. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/auth.py +0 -0
  36. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/datasets.py +0 -0
  37. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/deployments.py +0 -0
  38. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/evals.py +0 -0
  39. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/login.py +0 -0
  40. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/org.py +0 -0
  41. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/programs.py +0 -0
  42. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  43. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/signatures.py +0 -0
  44. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  45. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/top.py +0 -0
  46. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/traces.py +0 -0
  47. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/valsets.py +0 -0
  48. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/credentials.py +0 -0
  49. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/deprecation.py +0 -0
  50. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/pagination.py +0 -0
  51. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/shell.py +0 -0
  52. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/trace_render.py +0 -0
  53. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/tree_help.py +0 -0
  54. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/verbs.py +0 -0
  55. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/client.py +0 -0
  56. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/configuration.py +0 -0
  57. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/context.py +0 -0
  58. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/dataset_sync.py +0 -0
  59. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/datasets.py +0 -0
  60. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/decorators.py +0 -0
  61. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/deployment.py +0 -0
  62. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/deployment_metadata.py +0 -0
  63. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/deployment_runs.py +0 -0
  64. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/eval_handler.py +0 -0
  65. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/execution.py +0 -0
  66. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/exporter.py +0 -0
  67. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/exporter_http.py +0 -0
  68. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/helpers.py +0 -0
  69. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/identity.py +0 -0
  70. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/imaging.py +0 -0
  71. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/__init__.py +0 -0
  72. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/decode.py +0 -0
  73. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/encode.py +0 -0
  74. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/hash.py +0 -0
  75. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/program.py +0 -0
  76. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/reflection.py +0 -0
  77. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/types.py +0 -0
  78. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/xxh64.py +0 -0
  79. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/lm_configuration.py +0 -0
  80. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/mcp_servers.py +0 -0
  81. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/models.py +0 -0
  82. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/named_programs.py +0 -0
  83. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/optimizers/__init__.py +0 -0
  84. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/optimizers/gepa.py +0 -0
  85. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  86. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/optimizers/resume.py +0 -0
  87. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/optimizers/tracker.py +0 -0
  88. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/packaging.py +0 -0
  89. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/programs.py +0 -0
  90. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/references.py +0 -0
  91. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/retry.py +0 -0
  92. {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/traces.py +0 -0
  93. {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/2026-04-02-code-review.md +0 -0
  94. {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/client.md +0 -0
  95. {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/custom-proposer.md +0 -0
  96. {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  97. {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  98. {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/reading-traces.md +0 -0
  99. {cmpnd-0.53.0 → cmpnd-0.54.0}/examples/generalizing_proposer.py +0 -0
  100. {cmpnd-0.53.0 → cmpnd-0.54.0}/examples/local_ollama.py +0 -0
  101. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/LINEAGE.md +0 -0
  102. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/README.md +0 -0
  103. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  104. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  105. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
  106. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  107. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/program_names.py +0 -0
  108. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  109. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/py.typed +0 -0
  110. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  111. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/retry.py +0 -0
  112. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  113. {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/pyproject.toml +0 -0
  114. {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/audit-sdist.sh +0 -0
  115. {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/capture_review_run.py +0 -0
  116. {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/check-pip-install.sh +0 -0
  117. {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/check_release_artifacts.py +0 -0
  118. {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/fixtures/review_run.json.gz +0 -0
  119. {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/gepa_resume_bug.py +0 -0
  120. {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/release-stress.sh +0 -0
  121. {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/require_version_bump.py +0 -0
  122. {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/seed_review_run.py +0 -0
  123. {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/test-in-linux.sh +0 -0
  124. {cmpnd-0.53.0 → cmpnd-0.54.0}/stubs/dspy/__init__.pyi +0 -0
  125. {cmpnd-0.53.0 → cmpnd-0.54.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  126. {cmpnd-0.53.0 → cmpnd-0.54.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  127. {cmpnd-0.53.0 → cmpnd-0.54.0}/stubs/dspy/utils/__init__.pyi +0 -0
  128. {cmpnd-0.53.0 → cmpnd-0.54.0}/stubs/dspy/utils/callback.pyi +0 -0
  129. {cmpnd-0.53.0 → cmpnd-0.54.0}/stubs/dspy/utils/dummies.pyi +0 -0
  130. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/__init__.py +0 -0
  131. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/_deployment_signature_helpers.py +0 -0
  132. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/_exporter_helpers.py +0 -0
  133. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/_fake_clock.py +0 -0
  134. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/_identity_helpers.py +0 -0
  135. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/_resolve_refs.py +0 -0
  136. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/_traced_run.py +0 -0
  137. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/conftest.py +0 -0
  138. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/__init__.py +0 -0
  139. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/committee_task.py +0 -0
  140. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/conftest.py +0 -0
  141. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  142. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  143. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/fake_lm.py +0 -0
  144. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/helpers.py +0 -0
  145. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/target_capabilities.py +0 -0
  146. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_assessments_cli.py +0 -0
  147. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_capture_prompts.py +0 -0
  148. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_composite_module.py +0 -0
  149. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  150. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_deploy_execute.py +0 -0
  151. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_deploy_pandas.py +0 -0
  152. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_deploy_programs.py +0 -0
  153. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_edge_cases.py +0 -0
  154. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_example_hash.py +0 -0
  155. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_gepa_optimization.py +0 -0
  156. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_interpreter_spans.py +0 -0
  157. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_lm_accuracy.py +0 -0
  158. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_module_tracing.py +0 -0
  159. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_optimization_lineage.py +0 -0
  160. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  161. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_optimize_failures.py +0 -0
  162. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_optimize_loop.py +0 -0
  163. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  164. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  165. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_parallel_export.py +0 -0
  166. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_project_attribution.py +0 -0
  167. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_react_tool_spans.py +0 -0
  168. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_save_load.py +0 -0
  169. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_trace_structure.py +0 -0
  170. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_traces_read.py +0 -0
  171. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_user_attribution.py +0 -0
  172. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_valsets_cli.py +0 -0
  173. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  174. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  175. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  176. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  177. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  178. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  179. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  180. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  181. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  182. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/10_react.json +0 -0
  183. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/11_refine.json +0 -0
  184. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/12_retrieve.json +0 -0
  185. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/13_knn.json +0 -0
  186. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/14_parallel.json +0 -0
  187. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/README.md +0 -0
  188. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/ingest_conformance.py +0 -0
  189. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/__init__.py +0 -0
  190. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/authored_optimize/judge.py +0 -0
  191. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  192. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/conftest.py +0 -0
  193. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  194. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/hosted_optimize_probe.py +0 -0
  195. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/optimization_run_probe.py +0 -0
  196. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  197. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/test_logs_renders_traces.py +0 -0
  198. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/test_optimization_run.py +0 -0
  199. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/test_optimizing_progress.py +0 -0
  200. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/test_unhappy_path.py +0 -0
  201. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/__init__.py +0 -0
  202. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/__main__.py +0 -0
  203. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/drain.py +0 -0
  204. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/ingest.py +0 -0
  205. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/latency_transport.py +0 -0
  206. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/rlm_shape.py +0 -0
  207. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/rlm_size_report.py +0 -0
  208. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/sweep.py +0 -0
  209. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/test_rlm_shape.py +0 -0
  210. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/test_rlm_size_report.py +0 -0
  211. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/__init__.py +0 -0
  212. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/conftest.py +0 -0
  213. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/seed_parity.py +0 -0
  214. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_auth.py +0 -0
  215. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_datasets.py +0 -0
  216. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_deployments.py +0 -0
  217. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_evals.py +0 -0
  218. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_health.py +0 -0
  219. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_modules.py +0 -0
  220. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_optimizations.py +0 -0
  221. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_programs.py +0 -0
  222. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_signatures.py +0 -0
  223. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_trace_streaming.py +0 -0
  224. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_traces.py +0 -0
  225. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/span_reference_fixture.py +0 -0
  226. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/__init__.py +0 -0
  227. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/_provider.py +0 -0
  228. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/_readiness.py +0 -0
  229. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/_reliability.py +0 -0
  230. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/_result.py +0 -0
  231. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/mint_bedrock_token.py +0 -0
  232. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_deploy_programs.py +0 -0
  233. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_multi_client_stress.py +0 -0
  234. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_optimize_client_committee.py +0 -0
  235. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_optimize_loop.py +0 -0
  236. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  237. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_optimize_stress.py +0 -0
  238. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_provider_roles.py +0 -0
  239. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_readiness.py +0 -0
  240. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_real_provider_twins.py +0 -0
  241. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_reliability.py +0 -0
  242. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_reliability_classify.py +0 -0
  243. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_rlm_wasm.py +0 -0
  244. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_trace_search.py +0 -0
  245. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_adapter_reentrancy.py +0 -0
  246. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_assessments_api.py +0 -0
  247. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_authored.py +0 -0
  248. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_authored_bundle_collisions.py +0 -0
  249. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_authored_metric_contract.py +0 -0
  250. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_authored_named_models.py +0 -0
  251. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_authored_optimize.py +0 -0
  252. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_callback.py +0 -0
  253. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_candidate_lineage.py +0 -0
  254. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/__init__.py +0 -0
  255. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/conftest.py +0 -0
  256. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_api_client.py +0 -0
  257. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_assessments.py +0 -0
  258. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_auth.py +0 -0
  259. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_credentials.py +0 -0
  260. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_datasets.py +0 -0
  261. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_deployment_runs.py +0 -0
  262. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_deployments.py +0 -0
  263. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_evals.py +0 -0
  264. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_login.py +0 -0
  265. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_main.py +0 -0
  266. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_org.py +0 -0
  267. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_pagination.py +0 -0
  268. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_parity.py +0 -0
  269. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_programs.py +0 -0
  270. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_provider_credentials.py +0 -0
  271. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_schema_sample.py +0 -0
  272. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_shell.py +0 -0
  273. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_signatures.py +0 -0
  274. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_tool_servers.py +0 -0
  275. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_top.py +0 -0
  276. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_traces.py +0 -0
  277. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  278. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_tree_help.py +0 -0
  279. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_valsets.py +0 -0
  280. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_client.py +0 -0
  281. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_client_floor.py +0 -0
  282. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_client_program_names.py +0 -0
  283. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_clustering/__init__.py +0 -0
  284. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_clustering/conftest.py +0 -0
  285. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_clustering/test_edge_cases.py +0 -0
  286. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  287. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_clustering/test_type_conversion.py +0 -0
  288. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cmp551_grouping_identity.py +0 -0
  289. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_configuration.py +0 -0
  290. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_context.py +0 -0
  291. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_context_propagation.py +0 -0
  292. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_dataset_polling.py +0 -0
  293. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_dataset_sync.py +0 -0
  294. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_datasets.py +0 -0
  295. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_decision_client_spans.py +0 -0
  296. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_decorators.py +0 -0
  297. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_deploy.py +0 -0
  298. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  299. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_deploy_program_id.py +0 -0
  300. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_deployment_metadata.py +0 -0
  301. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_deployment_runs.py +0 -0
  302. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_deployments_read.py +0 -0
  303. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_eval.py +0 -0
  304. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_eval_example_attribution.py +0 -0
  305. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_eval_start.py +0 -0
  306. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_eval_trace_grouping.py +0 -0
  307. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_exception_paths.py +0 -0
  308. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_execute.py +0 -0
  309. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_execute_run_identity.py +0 -0
  310. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_exporter.py +0 -0
  311. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_exporter_program_names.py +0 -0
  312. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_exporter_queue.py +0 -0
  313. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_exporter_real.py +0 -0
  314. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_fake_lm.py +0 -0
  315. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_gepa_callback.py +0 -0
  316. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_gepa_integration.py +0 -0
  317. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_gepa_output_normalization.py +0 -0
  318. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_gepa_resume.py +0 -0
  319. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_identity.py +0 -0
  320. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_imaging.py +0 -0
  321. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_import_weight.py +0 -0
  322. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_ingest_conformance.py +0 -0
  323. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_ingest_second_producer.py +0 -0
  324. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_integration.py +0 -0
  325. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_interpreter_callback.py +0 -0
  326. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_ir_encoder_properties.py +0 -0
  327. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_ir_golden_vectors.py +0 -0
  328. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_ir_roundtrip.py +0 -0
  329. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_lm15_native_calls.py +0 -0
  330. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_lm_configuration.py +0 -0
  331. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_lm_usage_shapes.py +0 -0
  332. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_mcp_tools.py +0 -0
  333. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_models.py +0 -0
  334. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_multi_client_stress_readiness.py +0 -0
  335. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimization_progress.py +0 -0
  336. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimization_start.py +0 -0
  337. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize_budget.py +0 -0
  338. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize_lift.py +0 -0
  339. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize_lms.py +0 -0
  340. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize_proposer.py +0 -0
  341. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize_reflection_lm.py +0 -0
  342. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize_terminal_failure.py +0 -0
  343. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_packaging.py +0 -0
  344. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_packaging_callable_source.py +0 -0
  345. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_packaging_mcp_tools.py +0 -0
  346. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_packaging_program_source.py +0 -0
  347. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_poll_until.py +0 -0
  348. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_program_id_deprecation.py +0 -0
  349. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_program_identity.py +0 -0
  350. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_program_lineage_persist.py +0 -0
  351. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_program_names_in_deploy.py +0 -0
  352. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_programs_create.py +0 -0
  353. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_project_payload.py +0 -0
  354. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_real_provider_twins_readiness.py +0 -0
  355. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_reflector_properties.py +0 -0
  356. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_reflector_smoke.py +0 -0
  357. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_release_stress_script.py +0 -0
  358. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_repro_reactv2_bugs.py +0 -0
  359. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_require_version_bump.py +0 -0
  360. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_retry.py +0 -0
  361. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_reversible_failures.py +0 -0
  362. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_rlm_patch.py +0 -0
  363. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_run_attribution_paths.py +0 -0
  364. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_runtime_tools.py +0 -0
  365. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_schemas.py +0 -0
  366. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_seed_review_run.py +0 -0
  367. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_signature_inputs.py +0 -0
  368. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_span_references.py +0 -0
  369. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_span_sent_once.py +0 -0
  370. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_system_one.py +0 -0
  371. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_trace_context_concurrency.py +0 -0
  372. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_trace_fact_wire_contract.py +0 -0
  373. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_trace_facts.py +0 -0
  374. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_trace_render.py +0 -0
  375. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_traces_read.py +0 -0
  376. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_tracked_gepa.py +0 -0
  377. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_tracker_enrich.py +0 -0
  378. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_truncation.py +0 -0
  379. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_upload_auth_headers.py +0 -0
  380. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_valsets_api.py +0 -0
  381. {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_xxh64.py +0 -0
@@ -145,6 +145,8 @@ Before you touch either `pyproject.toml`:
145
145
 
146
146
  - `ingest/cmpnd_ingest/exporter_http.py`: the ingest transport (`cmpnd.exporter_http`)
147
147
  - `ingest/cmpnd_ingest/retry.py`: the client half of the retryable-error contract (`cmpnd.retry`)
148
+ - `ingest/cmpnd_ingest/client_floor.py`: warns once when serve's `X-Cmpnd-Minimum-Client-Version` header names a floor above the installed release
149
+ - `ingest/cmpnd_ingest/program_names.py`: keeps the program ids serve gave for the program names a process declared
148
150
  - `cmpnd/identity.py`: hash computations (xxhash, SHA256). See the `cmpnd-hash-handling` skill.
149
151
  - `cmpnd/ir/`: the structural program-identity IR (algebra, encoder, hash, reflector)
150
152
  - `cmpnd/callback.py`: DSPy instrumentation and signature extraction
@@ -195,8 +197,8 @@ its `add_parser` call in `top.register()`, and add the entity method or the
195
197
  When you touch a handler:
196
198
 
197
199
  - **Write user-facing hints with the `_hint_*` helpers** in
198
- `cli/commands/top.py` (`_hint_ps`, `_hint_run`, `_hint_status`,
199
- `_hint_inspect`, `_hint_traces_list`). They check `_SHELL_MODE` and emit
200
+ `cli/commands/top.py` (`_hint_ps`, `_hint_run`, `_hint_program_name`,
201
+ `_hint_status`, `_hint_inspect`, `_hint_traces_list`). They check `_SHELL_MODE` and emit
200
202
  `deps.d_xxx.run({...})` in the shell or `cmpnd run d_xxx '{...}'` outside
201
203
  it. Never hardcode a `cmpnd <verb>` string in handler output.
202
204
  - **Add a shell entity** by subclassing `Entity` in `cli/shell.py`. Add a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.53.0
3
+ Version: 0.54.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>
@@ -8,7 +8,11 @@ adjust its member cap or GitHub link, and grant ownership from the election
8
8
  queue. `cmpnd admin user …` is the operator's user-lifecycle surface (list,
9
9
  inspect, create, approve/reject the pending-signup queue, promote/demote admin,
10
10
  disable/enable, reset a password, and manage a user's API-key scopes). `cmpnd
11
- admin feature-flags …` reads and toggles the server's runtime flags. These hit
11
+ admin org plan …` and `cmpnd admin org overrides …` move an org between plans
12
+ and override one of its limits, `cmpnd admin plans …` edits each plan's
13
+ default limits and feature flags, and `cmpnd admin signup …` sets who may sign up
14
+ and the plan a new account's org lands on. `cmpnd admin feature-flags …` reads and
15
+ toggles the server's runtime flags. These hit
12
16
  /api/v1/admin/* and require an API key carrying the admin:* scopes — the server
13
17
  is the real authority. The CMPND_CLI_ADMIN env gate (mirroring CMPND_CLI_DEV)
14
18
  only keeps these out of customers' --help; it grants nothing.
@@ -16,7 +20,7 @@ only keeps these out of customers' --help; it grants nothing.
16
20
 
17
21
  from __future__ import annotations
18
22
 
19
- from argparse import ArgumentParser, _SubParsersAction
23
+ from argparse import ArgumentParser, ArgumentTypeError, _SubParsersAction
20
24
  from typing import Any
21
25
 
22
26
  from .. import api_client, deprecation
@@ -28,9 +32,29 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
28
32
 
29
33
  _register_org(sub)
30
34
  _register_user(sub)
35
+ _register_plans(sub)
36
+ _register_signup(sub)
31
37
  _register_feature_flags(sub)
32
38
 
33
39
 
40
+ # The signup modes, as serve names them. argparse refuses a typo before the request,
41
+ # and the server still validates it. Plan names are serve's alone: the CLI sends the
42
+ # name as typed, and serve's refusal lists the plans it has, so a new plan needs no
43
+ # CLI release.
44
+ SIGNUP_MODES = ("closed", "invite", "open")
45
+ PLAN_HELP = "The plan, as `cmpnd admin plans list` names it"
46
+
47
+
48
+ def _limit_value(raw: str) -> int | None:
49
+ """A limit is a whole number, or ``none`` for no limit, which sends null."""
50
+ if raw.lower() == "none":
51
+ return None
52
+ try:
53
+ return int(raw)
54
+ except ValueError:
55
+ raise ArgumentTypeError(f"{raw!r} is not a whole number or 'none'") from None
56
+
57
+
34
58
  def _register_org(sub: _SubParsersAction[ArgumentParser]) -> None:
35
59
  org = sub.add_parser("org", help="Organization bootstrap")
36
60
  osub = org.add_subparsers(dest="org_action")
@@ -71,6 +95,7 @@ def _register_org(sub: _SubParsersAction[ArgumentParser]) -> None:
71
95
  set_owner.set_defaults(func=handle_org_set_owner)
72
96
 
73
97
  ls = osub.add_parser("list", help="List organizations")
98
+ ls.add_argument("--plan", help="Only the organizations on this plan")
74
99
  ls.set_defaults(func=handle_org_list)
75
100
 
76
101
  def _org_id_arg(p: ArgumentParser) -> None:
@@ -97,6 +122,32 @@ def _register_org(sub: _SubParsersAction[ArgumentParser]) -> None:
97
122
  elect_owner.add_argument("user_id", help="User id to make owner")
98
123
  elect_owner.set_defaults(func=handle_org_elect_owner)
99
124
 
125
+ plan = osub.add_parser("plan", help="An org's plan and its limits")
126
+ plsub = plan.add_subparsers(dest="org_plan_action")
127
+ plan_show = plsub.add_parser("show", help="Show the org's plan and every limit as it resolves for the org")
128
+ _org_id_arg(plan_show)
129
+ plan_show.set_defaults(func=handle_org_plan_show)
130
+ plan_update = plsub.add_parser("update", help="Move the org to a plan")
131
+ _org_id_arg(plan_update)
132
+ plan_update.add_argument("plan", help=PLAN_HELP)
133
+ plan_update.set_defaults(func=handle_org_plan_update)
134
+
135
+ overrides = osub.add_parser("overrides", help="An org's overrides of its plan's limits")
136
+ ovsub = overrides.add_subparsers(dest="org_overrides_action")
137
+ override_update = ovsub.add_parser(
138
+ "update", help="Override one of the org's limits; 'none' lifts the plan's limit for the org"
139
+ )
140
+ _org_id_arg(override_update)
141
+ override_update.add_argument("name", help="Limit name, such as optimizations.concurrent")
142
+ override_update.add_argument("value", type=_limit_value, help="A whole number, or 'none' for no limit")
143
+ override_update.set_defaults(func=handle_org_overrides_update)
144
+ override_delete = ovsub.add_parser(
145
+ "delete", help="Delete the org's override so it follows its plan for the limit again"
146
+ )
147
+ _org_id_arg(override_delete)
148
+ override_delete.add_argument("name", help="Limit name")
149
+ override_delete.set_defaults(func=handle_org_overrides_delete)
150
+
100
151
 
101
152
  def _register_user(sub: _SubParsersAction[ArgumentParser]) -> None:
102
153
  user = sub.add_parser("user", help="User lifecycle management")
@@ -173,6 +224,57 @@ def _register_user(sub: _SubParsersAction[ArgumentParser]) -> None:
173
224
  set_scopes.set_defaults(func=handle_user_set_scopes)
174
225
 
175
226
 
227
+ def _register_plans(sub: _SubParsersAction[ArgumentParser]) -> None:
228
+ plans = sub.add_parser("plans", help="Each plan's default limits and feature flags")
229
+ psub = plans.add_subparsers(dest="plans_action")
230
+
231
+ ls = psub.add_parser("list", help="List every plan's limits and feature flags")
232
+ ls.set_defaults(func=handle_plans_list)
233
+
234
+ limits = psub.add_parser("limits", help="A plan's default limits")
235
+ lsub = limits.add_subparsers(dest="plans_limits_action")
236
+ limit_update = lsub.add_parser("update", help="Set a plan's default for a limit; 'none' means no limit")
237
+ limit_update.add_argument("plan", help=PLAN_HELP)
238
+ limit_update.add_argument("name", help="Limit name, such as optimizations.concurrent")
239
+ limit_update.add_argument("value", type=_limit_value, help="A whole number, or 'none' for no limit")
240
+ limit_update.set_defaults(func=handle_plans_limits_update)
241
+ limit_delete = lsub.add_parser("delete", help="Delete a plan's default for a limit")
242
+ limit_delete.add_argument("plan", help=PLAN_HELP)
243
+ limit_delete.add_argument("name", help="Limit name")
244
+ limit_delete.set_defaults(func=handle_plans_limits_delete)
245
+
246
+ flags = psub.add_parser("flags", help="A plan's feature flags")
247
+ fsub = flags.add_subparsers(dest="plans_flags_action")
248
+ flag_update = fsub.add_parser(
249
+ "update", help="Pin a feature flag on (or, with --disabled, off) for every org on a plan"
250
+ )
251
+ flag_update.add_argument("plan", help=PLAN_HELP)
252
+ flag_update.add_argument("name", help="Flag name; the flag must already exist")
253
+ flag_update.add_argument("--disabled", action="store_true", help="Pin the flag off (default is on)")
254
+ flag_update.set_defaults(func=handle_plans_flags_update)
255
+ flag_delete = fsub.add_parser("delete", help="Delete a plan's row for a flag so its orgs follow the global default")
256
+ flag_delete.add_argument("plan", help=PLAN_HELP)
257
+ flag_delete.add_argument("name", help="Flag name")
258
+ flag_delete.set_defaults(func=handle_plans_flags_delete)
259
+
260
+
261
+ def _register_signup(sub: _SubParsersAction[ArgumentParser]) -> None:
262
+ signup = sub.add_parser("signup", help="Who may sign up, and the plan a new account's org lands on")
263
+ ssub = signup.add_subparsers(dest="signup_action")
264
+
265
+ show = ssub.add_parser("show", help="Show the signup mode in force and the plan for new orgs")
266
+ show.set_defaults(func=handle_signup_show)
267
+
268
+ set_signup = ssub.add_parser("update", help="Change the signup mode, the plan for new orgs, or both")
269
+ set_signup.add_argument(
270
+ "--mode",
271
+ choices=(*SIGNUP_MODES, "installation"),
272
+ help="closed, invite or open; 'installation' follows CMPND_SERVE_SIGNUP_MODE again",
273
+ )
274
+ set_signup.add_argument("--default-plan", dest="default_plan", help="The plan for new orgs")
275
+ set_signup.set_defaults(func=handle_signup_update)
276
+
277
+
176
278
  def _register_feature_flags(sub: _SubParsersAction[ArgumentParser]) -> None:
177
279
  ff = sub.add_parser("feature-flags", help="Runtime feature flags")
178
280
  ffsub = ff.add_subparsers(dest="ff_action")
@@ -218,7 +320,10 @@ def handle_org_set_owner(client: api_client.ApiClient, args: Any) -> Any:
218
320
  return {"org_id": args.org_id, "owner": args.email}
219
321
 
220
322
 
221
- def handle_org_list(client: api_client.ApiClient, _args: Any) -> Any:
323
+ def handle_org_list(client: api_client.ApiClient, args: Any) -> Any:
324
+ plan = getattr(args, "plan", None)
325
+ if plan:
326
+ return client.get("/api/v1/admin/organizations", params={"plan": plan})
222
327
  return client.get("/api/v1/admin/organizations")
223
328
 
224
329
 
@@ -245,6 +350,70 @@ def handle_org_elect_owner(client: api_client.ApiClient, args: Any) -> Any:
245
350
  return {"elected": args.user_id, "github_login": args.github_login}
246
351
 
247
352
 
353
+ def handle_org_plan_show(client: api_client.ApiClient, args: Any) -> Any:
354
+ return client.get(f"/api/v1/admin/organizations/{args.org_id}/plan")
355
+
356
+
357
+ def handle_org_plan_update(client: api_client.ApiClient, args: Any) -> Any:
358
+ return client.put(f"/api/v1/admin/organizations/{args.org_id}/plan", json={"plan": args.plan})
359
+
360
+
361
+ def handle_org_overrides_update(client: api_client.ApiClient, args: Any) -> Any:
362
+ return client.put(
363
+ f"/api/v1/admin/organizations/{args.org_id}/limits/{args.name}",
364
+ json={"value": args.value},
365
+ )
366
+
367
+
368
+ def handle_org_overrides_delete(client: api_client.ApiClient, args: Any) -> Any:
369
+ return client.delete(f"/api/v1/admin/organizations/{args.org_id}/limits/{args.name}")
370
+
371
+
372
+ # --- plan handlers --------------------------------------------------------
373
+
374
+
375
+ def handle_plans_list(client: api_client.ApiClient, _args: Any) -> Any:
376
+ return client.get("/api/v1/admin/plans")
377
+
378
+
379
+ def handle_plans_limits_update(client: api_client.ApiClient, args: Any) -> Any:
380
+ return client.put(f"/api/v1/admin/plans/{args.plan}/limits/{args.name}", json={"value": args.value})
381
+
382
+
383
+ def handle_plans_limits_delete(client: api_client.ApiClient, args: Any) -> Any:
384
+ return client.delete(f"/api/v1/admin/plans/{args.plan}/limits/{args.name}")
385
+
386
+
387
+ def handle_plans_flags_update(client: api_client.ApiClient, args: Any) -> Any:
388
+ return client.put(
389
+ f"/api/v1/admin/plans/{args.plan}/feature-flags/{args.name}",
390
+ json={"enabled": not args.disabled},
391
+ )
392
+
393
+
394
+ def handle_plans_flags_delete(client: api_client.ApiClient, args: Any) -> Any:
395
+ return client.delete(f"/api/v1/admin/plans/{args.plan}/feature-flags/{args.name}")
396
+
397
+
398
+ # --- signup handlers ------------------------------------------------------
399
+
400
+
401
+ def handle_signup_show(client: api_client.ApiClient, _args: Any) -> Any:
402
+ return client.get("/api/v1/admin/signup")
403
+
404
+
405
+ def handle_signup_update(client: api_client.ApiClient, args: Any) -> Any:
406
+ # Send only what was given, so changing one setting leaves the other alone.
407
+ body: dict[str, Any] = {}
408
+ if args.mode is not None:
409
+ body["mode"] = None if args.mode == "installation" else args.mode
410
+ if args.default_plan is not None:
411
+ body["default_plan"] = args.default_plan
412
+ if not body:
413
+ raise SystemExit("cmpnd admin signup update: give --mode, --default-plan, or both")
414
+ return client.patch("/api/v1/admin/signup", json=body)
415
+
416
+
248
417
  # --- user handlers --------------------------------------------------------
249
418
 
250
419
 
@@ -9,7 +9,11 @@ from .. import api_client, deprecation, pagination
9
9
 
10
10
 
11
11
  def _add_list_args(p: ArgumentParser) -> None:
12
- p.add_argument("--status", help="Filter by status (running, completed, failed, cancelled)")
12
+ p.add_argument(
13
+ "--status",
14
+ help="Filter by status (queued, running, completed, failed, cancelled). "
15
+ "queued lists every run waiting for a slot, in the order they will start, without paging",
16
+ )
13
17
  p.add_argument("--program", dest="program_name", help="Filter by program name")
14
18
  p.add_argument("--optimizer-type", help="Filter by optimizer type")
15
19
  p.add_argument("--min-score", type=float, help="Minimum best score")
@@ -77,6 +81,13 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
77
81
  show.add_argument("id", help="Optimization run ID (UUID)")
78
82
  show.set_defaults(func=handle_show)
79
83
 
84
+ cancel = sub.add_parser(
85
+ "cancel",
86
+ help="Cancel an open optimization run. A hosted run stops searching within seconds",
87
+ )
88
+ cancel.add_argument("id", help="Optimization run ID (op_...)")
89
+ cancel.set_defaults(func=handle_cancel)
90
+
80
91
  delete = sub.add_parser("delete", help="Delete an optimization run and everything recorded under it")
81
92
  delete.add_argument("id", help="Optimization run ID (op_...)")
82
93
  delete.set_defaults(func=handle_delete)
@@ -160,6 +171,8 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
160
171
 
161
172
 
162
173
  def handle_list(client: api_client.ApiClient, args: Any) -> Any:
174
+ if getattr(args, "status", None) == "queued":
175
+ return _list_queued(client, args)
163
176
  # This route paginates by page/per_page; the shared helper maps the
164
177
  # user-facing --limit/--offset onto that pair.
165
178
  body: dict[str, Any] = pagination.wire(args, "per_page")
@@ -175,10 +188,31 @@ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
175
188
  return client.post("/api/v1/optimizations/search", json=body)
176
189
 
177
190
 
191
+ def _list_queued(client: api_client.ApiClient, args: Any) -> Any:
192
+ # A queued run waits for a slot and has no run record until it starts, so the run
193
+ # search cannot find it. Its own route lists the org's waiting runs in line order.
194
+ # The list is short and unfiltered, so the search filters do not apply.
195
+ search_only = [("program_name", "--program"), ("optimizer_type", "--optimizer-type"), ("min_score", "--min-score")]
196
+ for attr, flag in search_only:
197
+ if getattr(args, attr, None) is not None:
198
+ raise api_client.CliError(f"{flag} does not apply to --status queued, which lists every waiting run")
199
+ return client.get("/api/v1/optimizations/queued")
200
+
201
+
178
202
  def handle_show(client: api_client.ApiClient, args: Any) -> Any:
179
203
  return client.get(f"/api/v1/optimizations/{args.id}")
180
204
 
181
205
 
206
+ def handle_cancel(client: api_client.ApiClient, args: Any) -> Any:
207
+ """Cancel an open run and return its id and status.
208
+
209
+ The run's record stays, marked cancelled. Serve stops a hosted run's search
210
+ on its next watcher tick, which also frees the run's place among its org's
211
+ running optimizations. A run that already ended answers 409 with its status.
212
+ """
213
+ return client.post(f"/api/v1/optimizations/{args.id}/cancel")
214
+
215
+
182
216
  def handle_delete(client: api_client.ApiClient, args: Any) -> Any:
183
217
  # 204 No Content → delete() returns None; synthesize a confirmation.
184
218
  client.delete(f"/api/v1/optimizations/{args.id}")
@@ -1467,6 +1467,11 @@ def _upload_bundle(
1467
1467
  raise OptimizeError(f"failed to {what}: HTTP {resp.status_code}: {body}")
1468
1468
 
1469
1469
 
1470
+ # The `queued_reason` serve gives a run it holds because the run's org already runs as
1471
+ # many hosted optimizations as its plan allows.
1472
+ _QUEUED_FOR_ORG_LIMIT = "org_concurrency_limit"
1473
+
1474
+
1470
1475
  def _poll_until_terminal(
1471
1476
  client: httpx.Client,
1472
1477
  endpoint: str,
@@ -1495,6 +1500,9 @@ def _poll_until_terminal(
1495
1500
  # The last status the worker reported, so a timeout discovered at the wire can
1496
1501
  # say what the run was doing when the clock ran out.
1497
1502
  last_status: object = None
1503
+ # Whether the caller has been told the run waits on its org's cap. Once is enough,
1504
+ # because the poll repeats the same answer until a place frees.
1505
+ warned_org_limit = False
1498
1506
 
1499
1507
  def _tolerate(resp: httpx.Response) -> bool:
1500
1508
  nonlocal first_missing
@@ -1556,6 +1564,16 @@ def _poll_until_terminal(
1556
1564
  return status_data
1557
1565
  if status == "failed":
1558
1566
  raise OptimizeFailed(status_data.get("error"), optimization_id=optimization_id)
1567
+ if status == "queued" and status_data.get("queued_reason") == _QUEUED_FOR_ORG_LIMIT and not warned_org_limit:
1568
+ warned_org_limit = True
1569
+ logger.warning(
1570
+ "optimization %s is waiting: your org's plan allows %s hosted optimization(s) at once "
1571
+ "and %s are running. It starts when one finishes. To free a place now, run "
1572
+ "`cmpnd optimizations list --status running` and `cmpnd optimizations cancel <id>`.",
1573
+ optimization_id,
1574
+ status_data.get("concurrency_limit"),
1575
+ status_data.get("running_optimizations"),
1576
+ )
1559
1577
  if time.monotonic() - start > timeout:
1560
1578
  raise _ran_out_of_time(status)
1561
1579
  # Clipped like a backoff, for the same reason: this wait is inside the
@@ -526,6 +526,30 @@ takes named models through its `lms` argument. An authored file's `lms`
526
526
  binding cannot be combined with `lms` in the parent call or `config["lms"]`.
527
527
  The CLI has no named-model flag.
528
528
 
529
+ ### Cancel a run
530
+
531
+ `cmpnd optimizations cancel RUN_ID` stops an open run and keeps its record,
532
+ marked `cancelled`. A hosted run stops searching within a few seconds, and its
533
+ place among the org's running optimizations frees at the same moment. The web
534
+ app's Cancel run button does the same thing. A run that already ended answers
535
+ with its status and changes nothing. A poll of a cancelled run reports
536
+ `status: failed` with `settlement: cancelled`, so a released SDK's wait loop
537
+ ends.
538
+
539
+ An org's plan may cap how many hosted optimizations run at once. A run started
540
+ over the cap waits in serve's queue and starts when one of the org's runs
541
+ finishes. Its poll answers `status: queued` with `queued_reason:
542
+ org_concurrency_limit`, and the SDK logs one warning that names the cap and
543
+ the commands that list and cancel running runs. A plan may also limit how many
544
+ runs wait. A run that would wait past it is refused with a 409 that names the
545
+ limit and the queued runs' ids.
546
+
547
+ `cmpnd optimizations list --status queued` lists the org's queued runs in the
548
+ order they will start, and `cmpnd optimizations cancel <id>` cancels one. A
549
+ queued run has no run record until it starts, so the other statuses never show
550
+ it. The web app shows the same list on its queued optimizations page, which the
551
+ optimizations page links to while runs wait.
552
+
529
553
  ## Platform administration
530
554
 
531
555
  Platform-operator commands (`cmpnd admin …`) appear only when
@@ -556,6 +580,33 @@ EMAIL` withdraws the open owner invites sent to that address, and the links
556
580
  stop working. `cmpnd admin org show` lists pending addresses under
557
581
  `seed.pending_owner_emails`.
558
582
 
583
+ ### Plans, limits and signups
584
+
585
+ Every org has a plan, `free`, `pro` or `enterprise`, and each plan sets
586
+ default limits and feature flags. An org's own override beats its plan.
587
+ `cmpnd admin org list` shows each org's plan, and `--plan free` keeps only the
588
+ orgs on Free. `cmpnd admin org plan show ORG_ID` shows the org's plan and every
589
+ limit, with the layer that set it. `cmpnd admin org plan update ORG_ID PLAN` moves the
590
+ org. `cmpnd admin org overrides update ORG_ID NAME VALUE` overrides one limit,
591
+ and a value of `none` lifts the plan's limit for that org.
592
+ `cmpnd admin org overrides delete ORG_ID NAME` deletes the override, so the org
593
+ follows its plan again.
594
+
595
+ `cmpnd admin plans list` shows every plan's limits and feature flags.
596
+ `cmpnd admin plans limits update PLAN NAME VALUE` and `limits delete PLAN NAME`
597
+ change a plan's default for every org on it. `cmpnd admin plans flags update
598
+ PLAN NAME [--disabled]` pins a flag for the plan, and `flags delete` hands the
599
+ plan back to the global default. A plan's flag must already exist, so create
600
+ it first with `cmpnd admin feature-flags set`.
601
+
602
+ `cmpnd admin signup show` reports who may sign up and the plan a new account's
603
+ org lands on. `cmpnd admin signup update --mode closed|invite|open` overrides
604
+ the installation's `CMPND_SERVE_SIGNUP_MODE`, and `--mode installation` follows
605
+ it again. `--default-plan PLAN` sets the plan for new orgs. Changing the mode
606
+ needs a key with `admin:users:provision`, and changing the plan one with
607
+ `admin:orgs:write`. The operator pages at `/ui/admin/plans`,
608
+ `/ui/admin/feature_flags` and `/ui/admin/signup` write the same settings.
609
+
559
610
  ### Preparing an org for a customer
560
611
 
561
612
  This procedure keeps every write under the operator's own identity. The
@@ -786,7 +786,7 @@ The id rides every body that can declare a program, beside `program_signature_ha
786
786
  | Body | Where | Built by |
787
787
  |---|---|---|
788
788
  | trace start, single and batch | top-level `program_id`, and `started_fact.program_id` | `TraceBuilder.to_start_dict()` / `to_api_dict()` |
789
- | deployment create | top-level `program_id`, always | `deployment.deploy()` / `deploy_bundle()` |
789
+ | deployment create | top-level `program_id`, when the deployment resolves one | `deployment.deploy()` / `deploy_bundle()` |
790
790
  | optimize step | top-level `program_id`, on the steps that optimize the caller's deployment | `optimization._initiate_step()` |
791
791
  | optimization-run upsert | top-level `program_id` | `OptimizationTracker.to_api_dict()` |
792
792
 
@@ -5,7 +5,7 @@ name = "cmpnd"
5
5
  # it's new) — no git tags. Bump with `uv version --bump {minor,patch}` or by
6
6
  # hand. Runtime `cmpnd.__version__` reads installed metadata, so it tracks
7
7
  # this automatically. See docs/plans/2026-07-02-sdk-pypi-tag-free-publish.md.
8
- version = "0.53.0"
8
+ version = "0.54.0"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"
@@ -85,6 +85,19 @@ class TestAdminOrg:
85
85
  mock_client.get.assert_called_once_with("/api/v1/admin/organizations")
86
86
  assert result == {"organizations": []}
87
87
 
88
+ def test_list_filters_by_plan(self, mock_client):
89
+ mock_client.get.return_value = {"organizations": [{"id": "o_1", "plan": "free"}]}
90
+ args = _parse("org", "list", "--plan", "free")
91
+ result = args.func(mock_client, args)
92
+ mock_client.get.assert_called_once_with("/api/v1/admin/organizations", params={"plan": "free"})
93
+ assert result["organizations"][0]["plan"] == "free"
94
+
95
+ def test_list_sends_any_plan_name_for_serve_to_check(self, mock_client):
96
+ mock_client.get.return_value = {"organizations": []}
97
+ args = _parse("org", "list", "--plan", "platinum")
98
+ args.func(mock_client, args)
99
+ mock_client.get.assert_called_once_with("/api/v1/admin/organizations", params={"plan": "platinum"})
100
+
88
101
  def test_get_calls_endpoint(self, mock_client):
89
102
  mock_client.get.return_value = {"id": "o_1", "name": "Acme"}
90
103
  result = admin.handle_org_show(mock_client, Namespace(org_id="o_1"))
@@ -260,3 +273,125 @@ class TestAdminFeatureFlags:
260
273
  result = admin.handle_feature_flags_set(mock_client, Namespace(name="beta", disabled=True))
261
274
  mock_client.put.assert_called_once_with("/api/v1/admin/feature-flags/beta", json={"enabled": False})
262
275
  assert result["enabled"] is False
276
+
277
+
278
+ def _parse(*argv: str) -> Namespace:
279
+ from argparse import ArgumentParser
280
+
281
+ parser = ArgumentParser()
282
+ admin.register(parser.add_subparsers())
283
+ return parser.parse_args(["admin", *argv])
284
+
285
+
286
+ class TestAdminOrgPlan:
287
+ def test_plan_shows_the_orgs_plan(self, mock_client):
288
+ mock_client.get.return_value = {"org_id": "o_1", "plan": "free", "limits": []}
289
+ args = _parse("org", "plan", "show", "o_1")
290
+ result = args.func(mock_client, args)
291
+ mock_client.get.assert_called_once_with("/api/v1/admin/organizations/o_1/plan")
292
+ assert result["plan"] == "free"
293
+
294
+ def test_set_plan_puts_the_plan(self, mock_client):
295
+ args = _parse("org", "plan", "update", "o_1", "pro")
296
+ args.func(mock_client, args)
297
+ mock_client.put.assert_called_once_with("/api/v1/admin/organizations/o_1/plan", json={"plan": "pro"})
298
+
299
+ def test_override_limit_sends_a_number(self, mock_client):
300
+ args = _parse("org", "overrides", "update", "o_1", "optimizations.concurrent", "3")
301
+ args.func(mock_client, args)
302
+ mock_client.put.assert_called_once_with(
303
+ "/api/v1/admin/organizations/o_1/limits/optimizations.concurrent", json={"value": 3}
304
+ )
305
+
306
+ def test_override_limit_none_lifts_the_limit(self, mock_client):
307
+ # "none" sends null, which lifts the plan's limit for this org. That is a
308
+ # different state from clearing the override.
309
+ args = _parse("org", "overrides", "update", "o_1", "optimizations.concurrent", "none")
310
+ args.func(mock_client, args)
311
+ mock_client.put.assert_called_once_with(
312
+ "/api/v1/admin/organizations/o_1/limits/optimizations.concurrent", json={"value": None}
313
+ )
314
+
315
+ def test_override_limit_rejects_a_word_that_is_not_none(self):
316
+ import pytest
317
+
318
+ with pytest.raises(SystemExit):
319
+ _parse("org", "overrides", "update", "o_1", "optimizations.concurrent", "lots")
320
+
321
+ def test_clear_limit_override_deletes_it(self, mock_client):
322
+ args = _parse("org", "overrides", "delete", "o_1", "optimizations.concurrent")
323
+ args.func(mock_client, args)
324
+ mock_client.delete.assert_called_once_with("/api/v1/admin/organizations/o_1/limits/optimizations.concurrent")
325
+
326
+
327
+ class TestAdminPlans:
328
+ def test_list(self, mock_client):
329
+ mock_client.get.return_value = {"plans": []}
330
+ args = _parse("plans", "list")
331
+ assert args.func(mock_client, args) == {"plans": []}
332
+ mock_client.get.assert_called_once_with("/api/v1/admin/plans")
333
+
334
+ def test_set_limit_and_none(self, mock_client):
335
+ args = _parse("plans", "limits", "update", "pro", "optimizations.concurrent", "5")
336
+ args.func(mock_client, args)
337
+ mock_client.put.assert_called_with("/api/v1/admin/plans/pro/limits/optimizations.concurrent", json={"value": 5})
338
+ args = _parse("plans", "limits", "update", "free", "optimizations.concurrent", "none")
339
+ args.func(mock_client, args)
340
+ mock_client.put.assert_called_with(
341
+ "/api/v1/admin/plans/free/limits/optimizations.concurrent", json={"value": None}
342
+ )
343
+
344
+ def test_clear_limit(self, mock_client):
345
+ args = _parse("plans", "limits", "delete", "pro", "optimizations.concurrent")
346
+ args.func(mock_client, args)
347
+ mock_client.delete.assert_called_once_with("/api/v1/admin/plans/pro/limits/optimizations.concurrent")
348
+
349
+ def test_set_flag_enables_by_default_and_disables_with_the_flag(self, mock_client):
350
+ args = _parse("plans", "flags", "update", "free", "beta")
351
+ args.func(mock_client, args)
352
+ mock_client.put.assert_called_with("/api/v1/admin/plans/free/feature-flags/beta", json={"enabled": True})
353
+ args = _parse("plans", "flags", "update", "free", "beta", "--disabled")
354
+ args.func(mock_client, args)
355
+ mock_client.put.assert_called_with("/api/v1/admin/plans/free/feature-flags/beta", json={"enabled": False})
356
+
357
+ def test_clear_flag(self, mock_client):
358
+ args = _parse("plans", "flags", "delete", "free", "beta")
359
+ args.func(mock_client, args)
360
+ mock_client.delete.assert_called_once_with("/api/v1/admin/plans/free/feature-flags/beta")
361
+
362
+ def test_plan_names_are_serves_to_check(self, mock_client):
363
+ args = _parse("plans", "limits", "update", "platinum", "optimizations.concurrent", "5")
364
+ args.func(mock_client, args)
365
+ mock_client.put.assert_called_once_with(
366
+ "/api/v1/admin/plans/platinum/limits/optimizations.concurrent", json={"value": 5}
367
+ )
368
+
369
+
370
+ class TestAdminSignup:
371
+ def test_show(self, mock_client):
372
+ mock_client.get.return_value = {"mode": "invite", "mode_source": "installation"}
373
+ args = _parse("signup", "show")
374
+ assert args.func(mock_client, args)["mode"] == "invite"
375
+ mock_client.get.assert_called_once_with("/api/v1/admin/signup")
376
+
377
+ def test_set_sends_only_what_was_given(self, mock_client):
378
+ args = _parse("signup", "update", "--mode", "closed")
379
+ args.func(mock_client, args)
380
+ mock_client.patch.assert_called_with("/api/v1/admin/signup", json={"mode": "closed"})
381
+ args = _parse("signup", "update", "--default-plan", "free")
382
+ args.func(mock_client, args)
383
+ mock_client.patch.assert_called_with("/api/v1/admin/signup", json={"default_plan": "free"})
384
+
385
+ def test_installation_mode_sends_null(self, mock_client):
386
+ # null hands signups back to the installation's CMPND_SERVE_SIGNUP_MODE.
387
+ args = _parse("signup", "update", "--mode", "installation", "--default-plan", "pro")
388
+ args.func(mock_client, args)
389
+ mock_client.patch.assert_called_once_with("/api/v1/admin/signup", json={"mode": None, "default_plan": "pro"})
390
+
391
+ def test_set_with_nothing_to_change_exits(self, mock_client):
392
+ import pytest
393
+
394
+ args = _parse("signup", "update")
395
+ with pytest.raises(SystemExit):
396
+ args.func(mock_client, args)
397
+ mock_client.patch.assert_not_called()
@@ -9,6 +9,7 @@ import pytest
9
9
  from cmpnd.cli.api_client import CliError
10
10
  from cmpnd.cli.commands import optimizations
11
11
  from cmpnd.cli.commands.optimizations import (
12
+ handle_cancel,
12
13
  handle_candidates,
13
14
  handle_compare,
14
15
  handle_compare_failed_examples,
@@ -58,6 +59,26 @@ class TestOptimizationsList:
58
59
  assert body["status"] == "completed"
59
60
  assert body["min_score"] == 0.8
60
61
 
62
+ def test_queued_lists_the_waiting_runs(self, mock_client):
63
+ # A queued run has no run record yet, so the run search cannot find it. The
64
+ # queued status reads the org's waiting runs from their own route.
65
+ mock_client.get.return_value = {"optimizations": [{"optimization_id": "op_1", "position": 1}]}
66
+ args = argparse.Namespace(
67
+ status="queued", program_name=None, optimizer_type=None, min_score=None, limit=50, offset=0
68
+ )
69
+ result = handle_list(mock_client, args)
70
+ mock_client.get.assert_called_once_with("/api/v1/optimizations/queued")
71
+ mock_client.post.assert_not_called()
72
+ assert result["optimizations"][0]["optimization_id"] == "op_1"
73
+
74
+ def test_queued_refuses_filters_it_cannot_apply(self, mock_client):
75
+ args = argparse.Namespace(
76
+ status="queued", program_name="RAG", optimizer_type=None, min_score=None, limit=50, offset=0
77
+ )
78
+ with pytest.raises(CliError, match="--status queued"):
79
+ handle_list(mock_client, args)
80
+ mock_client.get.assert_not_called()
81
+
61
82
  def test_deprecated_search_verb_dispatches_and_is_marked(self, mock_client):
62
83
  # The warning is the dispatcher's job (see test_main.py); the wiring is this one's.
63
84
  parser = _build_parser()
@@ -152,6 +173,20 @@ class TestOptimizationsDatasetAnalysis:
152
173
  mock_client.get.assert_called_once_with("/api/v1/optimizations/op_abc/dataset-analysis")
153
174
 
154
175
 
176
+ class TestOptimizationsCancel:
177
+ def test_posts_to_the_cancel_route(self, mock_client):
178
+ mock_client.post.return_value = {"optimization_id": "op_abc", "status": "cancelled"}
179
+ args = argparse.Namespace(id="op_abc")
180
+ result = handle_cancel(mock_client, args)
181
+ mock_client.post.assert_called_once_with("/api/v1/optimizations/op_abc/cancel")
182
+ assert result == {"optimization_id": "op_abc", "status": "cancelled"}
183
+
184
+ def test_parses_from_the_command_line(self):
185
+ args = _build_parser().parse_args(["optimizations", "cancel", "op_abc"])
186
+ assert args.func is handle_cancel
187
+ assert args.id == "op_abc"
188
+
189
+
155
190
  class TestOptimizationsDelete:
156
191
  def test_calls_correct_endpoint(self, mock_client):
157
192
  mock_client.delete.return_value = None