cmpnd 0.40.1__tar.gz → 0.41.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 (370) hide show
  1. {cmpnd-0.40.1 → cmpnd-0.41.0}/PKG-INFO +5 -2
  2. {cmpnd-0.40.1 → cmpnd-0.41.0}/README.md +1 -1
  3. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/top.py +13 -19
  4. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/shell.py +3 -3
  5. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/deployment.py +40 -18
  6. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/optimization.py +10 -22
  7. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/programs.py +17 -1
  8. {cmpnd-0.40.1 → cmpnd-0.41.0}/docs/cli.md +9 -4
  9. {cmpnd-0.40.1 → cmpnd-0.41.0}/docs/sdk-instrumentation.md +8 -5
  10. {cmpnd-0.40.1 → cmpnd-0.41.0}/pyproject.toml +8 -1
  11. cmpnd-0.41.0/scripts/check-pip-install.sh +34 -0
  12. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_shell.py +12 -6
  13. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_top.py +21 -12
  14. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_deploy_program_id.py +56 -47
  15. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_programs_create.py +15 -0
  16. {cmpnd-0.40.1 → cmpnd-0.41.0}/uv.lock +43 -39
  17. {cmpnd-0.40.1 → cmpnd-0.41.0}/.gitignore +0 -0
  18. {cmpnd-0.40.1 → cmpnd-0.41.0}/AGENTS.md +0 -0
  19. {cmpnd-0.40.1 → cmpnd-0.41.0}/CLAUDE.md +0 -0
  20. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/__init__.py +0 -0
  21. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/_assessments.py +0 -0
  22. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/_authored_import.py +0 -0
  23. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/_deployments.py +0 -0
  24. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/_exporter_pool.py +0 -0
  25. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/_gepa_patch.py +0 -0
  26. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/_http.py +0 -0
  27. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/_program_patch.py +0 -0
  28. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/_rlm_patch.py +0 -0
  29. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/_runtime_tools.py +0 -0
  30. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/_timespec.py +0 -0
  31. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/_traces.py +0 -0
  32. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/_valsets.py +0 -0
  33. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/authored.py +0 -0
  34. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/callback.py +0 -0
  35. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/__init__.py +0 -0
  36. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/api_client.py +0 -0
  37. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/__init__.py +0 -0
  38. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/admin.py +0 -0
  39. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/assessments.py +0 -0
  40. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/auth.py +0 -0
  41. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/datasets.py +0 -0
  42. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/deployments.py +0 -0
  43. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/evals.py +0 -0
  44. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/login.py +0 -0
  45. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/optimizations.py +0 -0
  46. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/org.py +0 -0
  47. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/programs.py +0 -0
  48. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  49. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/signatures.py +0 -0
  50. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  51. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/traces.py +0 -0
  52. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/commands/valsets.py +0 -0
  53. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/credentials.py +0 -0
  54. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/deprecation.py +0 -0
  55. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/pagination.py +0 -0
  56. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/trace_render.py +0 -0
  57. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/tree_help.py +0 -0
  58. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/cli/verbs.py +0 -0
  59. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/client.py +0 -0
  60. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/configuration.py +0 -0
  61. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/context.py +0 -0
  62. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/dataset_sync.py +0 -0
  63. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/datasets.py +0 -0
  64. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/decorators.py +0 -0
  65. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/deployment_metadata.py +0 -0
  66. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/deployment_runs.py +0 -0
  67. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/eval_handler.py +0 -0
  68. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/execution.py +0 -0
  69. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/exporter.py +0 -0
  70. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/exporter_http.py +0 -0
  71. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/helpers.py +0 -0
  72. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/identity.py +0 -0
  73. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/imaging.py +0 -0
  74. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/ir/__init__.py +0 -0
  75. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/ir/decode.py +0 -0
  76. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/ir/encode.py +0 -0
  77. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/ir/hash.py +0 -0
  78. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/ir/program.py +0 -0
  79. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/ir/reflection.py +0 -0
  80. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/ir/types.py +0 -0
  81. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/ir/xxh64.py +0 -0
  82. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/lm_configuration.py +0 -0
  83. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/mcp_servers.py +0 -0
  84. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/models.py +0 -0
  85. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/optimizers/__init__.py +0 -0
  86. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/optimizers/gepa.py +0 -0
  87. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  88. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/optimizers/resume.py +0 -0
  89. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/optimizers/tracker.py +0 -0
  90. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/packaging.py +0 -0
  91. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/references.py +0 -0
  92. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/retry.py +0 -0
  93. {cmpnd-0.40.1 → cmpnd-0.41.0}/cmpnd/traces.py +0 -0
  94. {cmpnd-0.40.1 → cmpnd-0.41.0}/docs/2026-04-02-code-review.md +0 -0
  95. {cmpnd-0.40.1 → cmpnd-0.41.0}/docs/client.md +0 -0
  96. {cmpnd-0.40.1 → cmpnd-0.41.0}/docs/custom-proposer.md +0 -0
  97. {cmpnd-0.40.1 → cmpnd-0.41.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  98. {cmpnd-0.40.1 → cmpnd-0.41.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  99. {cmpnd-0.40.1 → cmpnd-0.41.0}/docs/reading-traces.md +0 -0
  100. {cmpnd-0.40.1 → cmpnd-0.41.0}/examples/generalizing_proposer.py +0 -0
  101. {cmpnd-0.40.1 → cmpnd-0.41.0}/examples/local_ollama.py +0 -0
  102. {cmpnd-0.40.1 → cmpnd-0.41.0}/ingest/LINEAGE.md +0 -0
  103. {cmpnd-0.40.1 → cmpnd-0.41.0}/ingest/README.md +0 -0
  104. {cmpnd-0.40.1 → cmpnd-0.41.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  105. {cmpnd-0.40.1 → cmpnd-0.41.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  106. {cmpnd-0.40.1 → cmpnd-0.41.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  107. {cmpnd-0.40.1 → cmpnd-0.41.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  108. {cmpnd-0.40.1 → cmpnd-0.41.0}/ingest/cmpnd_ingest/py.typed +0 -0
  109. {cmpnd-0.40.1 → cmpnd-0.41.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  110. {cmpnd-0.40.1 → cmpnd-0.41.0}/ingest/cmpnd_ingest/retry.py +0 -0
  111. {cmpnd-0.40.1 → cmpnd-0.41.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  112. {cmpnd-0.40.1 → cmpnd-0.41.0}/ingest/pyproject.toml +0 -0
  113. {cmpnd-0.40.1 → cmpnd-0.41.0}/scripts/audit-sdist.sh +0 -0
  114. {cmpnd-0.40.1 → cmpnd-0.41.0}/scripts/capture_review_run.py +0 -0
  115. {cmpnd-0.40.1 → cmpnd-0.41.0}/scripts/check_release_artifacts.py +0 -0
  116. {cmpnd-0.40.1 → cmpnd-0.41.0}/scripts/fixtures/review_run.json.gz +0 -0
  117. {cmpnd-0.40.1 → cmpnd-0.41.0}/scripts/gepa_resume_bug.py +0 -0
  118. {cmpnd-0.40.1 → cmpnd-0.41.0}/scripts/release-stress.sh +0 -0
  119. {cmpnd-0.40.1 → cmpnd-0.41.0}/scripts/require_version_bump.py +0 -0
  120. {cmpnd-0.40.1 → cmpnd-0.41.0}/scripts/seed_review_run.py +0 -0
  121. {cmpnd-0.40.1 → cmpnd-0.41.0}/scripts/test-in-linux.sh +0 -0
  122. {cmpnd-0.40.1 → cmpnd-0.41.0}/stubs/dspy/__init__.pyi +0 -0
  123. {cmpnd-0.40.1 → cmpnd-0.41.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  124. {cmpnd-0.40.1 → cmpnd-0.41.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  125. {cmpnd-0.40.1 → cmpnd-0.41.0}/stubs/dspy/utils/__init__.pyi +0 -0
  126. {cmpnd-0.40.1 → cmpnd-0.41.0}/stubs/dspy/utils/callback.pyi +0 -0
  127. {cmpnd-0.40.1 → cmpnd-0.41.0}/stubs/dspy/utils/dummies.pyi +0 -0
  128. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/__init__.py +0 -0
  129. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/_deployment_signature_helpers.py +0 -0
  130. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/_exporter_helpers.py +0 -0
  131. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/_fake_clock.py +0 -0
  132. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/_identity_helpers.py +0 -0
  133. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/_resolve_refs.py +0 -0
  134. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/_traced_run.py +0 -0
  135. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/conftest.py +0 -0
  136. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/__init__.py +0 -0
  137. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/committee_task.py +0 -0
  138. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/conftest.py +0 -0
  139. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  140. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  141. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/fake_lm.py +0 -0
  142. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/helpers.py +0 -0
  143. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/target_capabilities.py +0 -0
  144. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_assessments_cli.py +0 -0
  145. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_capture_prompts.py +0 -0
  146. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_composite_module.py +0 -0
  147. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  148. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_deploy_execute.py +0 -0
  149. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_deploy_pandas.py +0 -0
  150. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_deploy_programs.py +0 -0
  151. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_edge_cases.py +0 -0
  152. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_example_hash.py +0 -0
  153. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_gepa_optimization.py +0 -0
  154. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_interpreter_spans.py +0 -0
  155. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_lm_accuracy.py +0 -0
  156. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_module_tracing.py +0 -0
  157. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_optimization_lineage.py +0 -0
  158. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  159. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_optimize_failures.py +0 -0
  160. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_optimize_loop.py +0 -0
  161. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  162. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  163. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_parallel_export.py +0 -0
  164. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_project_attribution.py +0 -0
  165. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_react_tool_spans.py +0 -0
  166. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_save_load.py +0 -0
  167. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_trace_structure.py +0 -0
  168. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_traces_read.py +0 -0
  169. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_user_attribution.py +0 -0
  170. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/e2e/test_valsets_cli.py +0 -0
  171. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  172. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  173. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  174. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  175. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  176. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  177. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  178. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  179. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  180. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/10_react.json +0 -0
  181. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/11_refine.json +0 -0
  182. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/12_retrieve.json +0 -0
  183. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/13_knn.json +0 -0
  184. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/14_parallel.json +0 -0
  185. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/identity_vectors/README.md +0 -0
  186. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/ingest_conformance.py +0 -0
  187. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/integration/__init__.py +0 -0
  188. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/integration/authored_optimize/judge.py +0 -0
  189. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  190. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/integration/conftest.py +0 -0
  191. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  192. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/integration/hosted_optimize_probe.py +0 -0
  193. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/integration/optimization_run_probe.py +0 -0
  194. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  195. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/integration/test_logs_renders_traces.py +0 -0
  196. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/integration/test_optimization_run.py +0 -0
  197. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/integration/test_optimizing_progress.py +0 -0
  198. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/integration/test_unhappy_path.py +0 -0
  199. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/load/__init__.py +0 -0
  200. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/load/__main__.py +0 -0
  201. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/load/drain.py +0 -0
  202. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/load/ingest.py +0 -0
  203. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/load/latency_transport.py +0 -0
  204. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/load/rlm_shape.py +0 -0
  205. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/load/rlm_size_report.py +0 -0
  206. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/load/sweep.py +0 -0
  207. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/load/test_rlm_shape.py +0 -0
  208. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/load/test_rlm_size_report.py +0 -0
  209. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/__init__.py +0 -0
  210. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/conftest.py +0 -0
  211. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/seed_parity.py +0 -0
  212. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/test_auth.py +0 -0
  213. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/test_datasets.py +0 -0
  214. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/test_deployments.py +0 -0
  215. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/test_evals.py +0 -0
  216. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/test_health.py +0 -0
  217. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/test_modules.py +0 -0
  218. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/test_optimizations.py +0 -0
  219. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/test_programs.py +0 -0
  220. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/test_signatures.py +0 -0
  221. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/test_trace_streaming.py +0 -0
  222. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/parity/test_traces.py +0 -0
  223. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/span_reference_fixture.py +0 -0
  224. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/__init__.py +0 -0
  225. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/_provider.py +0 -0
  226. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/_readiness.py +0 -0
  227. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/_reliability.py +0 -0
  228. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/_result.py +0 -0
  229. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/mint_bedrock_token.py +0 -0
  230. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_deploy_programs.py +0 -0
  231. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_multi_client_stress.py +0 -0
  232. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_optimize_client_committee.py +0 -0
  233. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_optimize_loop.py +0 -0
  234. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  235. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_optimize_stress.py +0 -0
  236. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_provider_roles.py +0 -0
  237. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_readiness.py +0 -0
  238. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_real_provider_twins.py +0 -0
  239. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_reliability.py +0 -0
  240. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_reliability_classify.py +0 -0
  241. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_rlm_wasm.py +0 -0
  242. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/stress/test_trace_search.py +0 -0
  243. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_adapter_reentrancy.py +0 -0
  244. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_assessments_api.py +0 -0
  245. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_authored.py +0 -0
  246. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_authored_bundle_collisions.py +0 -0
  247. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_authored_metric_contract.py +0 -0
  248. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_authored_named_models.py +0 -0
  249. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_authored_optimize.py +0 -0
  250. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_callback.py +0 -0
  251. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_candidate_lineage.py +0 -0
  252. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/__init__.py +0 -0
  253. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/conftest.py +0 -0
  254. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_admin.py +0 -0
  255. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_api_client.py +0 -0
  256. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_assessments.py +0 -0
  257. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_auth.py +0 -0
  258. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_credentials.py +0 -0
  259. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_datasets.py +0 -0
  260. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_deployment_runs.py +0 -0
  261. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_deployments.py +0 -0
  262. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_evals.py +0 -0
  263. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_login.py +0 -0
  264. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_main.py +0 -0
  265. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_optimizations.py +0 -0
  266. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_org.py +0 -0
  267. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_pagination.py +0 -0
  268. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_parity.py +0 -0
  269. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_programs.py +0 -0
  270. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_provider_credentials.py +0 -0
  271. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_schema_sample.py +0 -0
  272. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_signatures.py +0 -0
  273. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_tool_servers.py +0 -0
  274. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_traces.py +0 -0
  275. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  276. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_tree_help.py +0 -0
  277. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cli/test_valsets.py +0 -0
  278. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_client.py +0 -0
  279. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_clustering/__init__.py +0 -0
  280. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_clustering/conftest.py +0 -0
  281. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_clustering/test_edge_cases.py +0 -0
  282. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  283. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_clustering/test_type_conversion.py +0 -0
  284. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_cmp551_grouping_identity.py +0 -0
  285. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_configuration.py +0 -0
  286. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_context.py +0 -0
  287. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_context_propagation.py +0 -0
  288. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_dataset_polling.py +0 -0
  289. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_dataset_sync.py +0 -0
  290. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_datasets.py +0 -0
  291. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_decorators.py +0 -0
  292. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_deploy.py +0 -0
  293. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  294. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_deployment_metadata.py +0 -0
  295. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_deployment_runs.py +0 -0
  296. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_deployments_read.py +0 -0
  297. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_eval.py +0 -0
  298. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_eval_example_attribution.py +0 -0
  299. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_eval_start.py +0 -0
  300. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_eval_trace_grouping.py +0 -0
  301. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_exception_paths.py +0 -0
  302. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_execute.py +0 -0
  303. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_execute_run_identity.py +0 -0
  304. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_exporter.py +0 -0
  305. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_exporter_queue.py +0 -0
  306. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_exporter_real.py +0 -0
  307. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_fake_lm.py +0 -0
  308. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_gepa_callback.py +0 -0
  309. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_gepa_integration.py +0 -0
  310. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_gepa_output_normalization.py +0 -0
  311. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_gepa_resume.py +0 -0
  312. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_identity.py +0 -0
  313. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_imaging.py +0 -0
  314. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_import_weight.py +0 -0
  315. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_ingest_conformance.py +0 -0
  316. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_ingest_second_producer.py +0 -0
  317. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_integration.py +0 -0
  318. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_interpreter_callback.py +0 -0
  319. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_ir_encoder_properties.py +0 -0
  320. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_ir_golden_vectors.py +0 -0
  321. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_ir_roundtrip.py +0 -0
  322. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_lm15_native_calls.py +0 -0
  323. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_lm_configuration.py +0 -0
  324. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_lm_usage_shapes.py +0 -0
  325. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_mcp_tools.py +0 -0
  326. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_models.py +0 -0
  327. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_multi_client_stress_readiness.py +0 -0
  328. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_optimization_progress.py +0 -0
  329. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_optimization_start.py +0 -0
  330. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_optimize.py +0 -0
  331. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_optimize_budget.py +0 -0
  332. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_optimize_lift.py +0 -0
  333. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_optimize_lms.py +0 -0
  334. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_optimize_proposer.py +0 -0
  335. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_optimize_reflection_lm.py +0 -0
  336. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_optimize_terminal_failure.py +0 -0
  337. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_packaging.py +0 -0
  338. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_packaging_callable_source.py +0 -0
  339. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_packaging_mcp_tools.py +0 -0
  340. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_packaging_program_source.py +0 -0
  341. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_poll_until.py +0 -0
  342. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_program_identity.py +0 -0
  343. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_program_lineage_persist.py +0 -0
  344. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_project_payload.py +0 -0
  345. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_real_provider_twins_readiness.py +0 -0
  346. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_reflector_properties.py +0 -0
  347. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_reflector_smoke.py +0 -0
  348. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_release_stress_script.py +0 -0
  349. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_repro_reactv2_bugs.py +0 -0
  350. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_require_version_bump.py +0 -0
  351. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_retry.py +0 -0
  352. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_reversible_failures.py +0 -0
  353. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_rlm_patch.py +0 -0
  354. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_run_attribution_paths.py +0 -0
  355. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_runtime_tools.py +0 -0
  356. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_schemas.py +0 -0
  357. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_seed_review_run.py +0 -0
  358. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_signature_inputs.py +0 -0
  359. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_span_references.py +0 -0
  360. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_span_sent_once.py +0 -0
  361. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_trace_context_concurrency.py +0 -0
  362. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_trace_fact_wire_contract.py +0 -0
  363. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_trace_facts.py +0 -0
  364. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_trace_render.py +0 -0
  365. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_traces_read.py +0 -0
  366. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_tracked_gepa.py +0 -0
  367. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_tracker_enrich.py +0 -0
  368. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_truncation.py +0 -0
  369. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_valsets_api.py +0 -0
  370. {cmpnd-0.40.1 → cmpnd-0.41.0}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.40.1
3
+ Version: 0.41.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>
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3.15
18
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
20
  Requires-Python: >=3.10
20
21
  Requires-Dist: dspy>=3.1.3
@@ -27,8 +28,10 @@ Requires-Dist: xxhash>=3.0.0
27
28
  Provides-Extra: dev
28
29
  Requires-Dist: basedpyright>=1.39; extra == 'dev'
29
30
  Requires-Dist: boto3>=1.34; extra == 'dev'
31
+ Requires-Dist: gepa; extra == 'dev'
30
32
  Requires-Dist: hypothesis>=6.100; extra == 'dev'
31
33
  Requires-Dist: jsonschema>=4.0; extra == 'dev'
34
+ Requires-Dist: litellm; extra == 'dev'
32
35
  Requires-Dist: numpy>=1.26; (python_version < '3.11') and extra == 'dev'
33
36
  Requires-Dist: numpy>=2.3; (python_version >= '3.11') and extra == 'dev'
34
37
  Requires-Dist: pandas>=2.2; extra == 'dev'
@@ -161,7 +164,7 @@ cmpnd.deploy(
161
164
  metric=None, # optional: a metric callable, packaged for optimization
162
165
  timeout=300, # optional: seconds to wait for the parser after the upload
163
166
  metadata=None, # optional: a JSON-serializable dict, at most 64 KiB, stored with the deployment
164
- program_id="p_…", # required unless configured: the program the deployment belongs to
167
+ program_id="p_…", # the program the deployment belongs to; serve requires it after self-host 2026-001
165
168
  )
166
169
  ```
167
170
 
@@ -111,7 +111,7 @@ cmpnd.deploy(
111
111
  metric=None, # optional: a metric callable, packaged for optimization
112
112
  timeout=300, # optional: seconds to wait for the parser after the upload
113
113
  metadata=None, # optional: a JSON-serializable dict, at most 64 KiB, stored with the deployment
114
- program_id="p_…", # required unless configured: the program the deployment belongs to
114
+ program_id="p_…", # the program the deployment belongs to; serve requires it after self-host 2026-001
115
115
  )
116
116
  ```
117
117
 
@@ -811,11 +811,12 @@ def _parse_deploy_metadata(raw: str | None) -> dict[str, Any] | None:
811
811
  raise api_client.CliError(f"--metadata is invalid: {exc}")
812
812
 
813
813
 
814
- def _program_id_arg(value: str | None, *, required: bool) -> str | None:
814
+ def _program_id_arg(value: str | None) -> str | None:
815
815
  """The program id a deploy or optimize names: ``--program-id``, else ``CMPND_PROGRAM_ID``.
816
816
 
817
- Checked here, before a file is staged, so a missing or mistyped id is a usage
818
- error naming the flag rather than a refusal after work was done.
817
+ Checked here, before a file is staged, so a mistyped id is a usage error naming
818
+ its source rather than a refusal after work was done. serve decides whether a
819
+ deployment needs an id, so a missing one is not refused here.
819
820
  """
820
821
  from ... import identity
821
822
 
@@ -823,12 +824,6 @@ def _program_id_arg(value: str | None, *, required: bool) -> str | None:
823
824
  if value is None and os.environ.get("CMPND_PROGRAM_ID"):
824
825
  value, source = os.environ["CMPND_PROGRAM_ID"], "CMPND_PROGRAM_ID"
825
826
  if value is None:
826
- if required:
827
- raise api_client.CliError(
828
- "--program-id is required: a deployment always names its program. "
829
- "Create one with `cmpnd programs create`, then pass --program-id p_… "
830
- "(or set CMPND_PROGRAM_ID)."
831
- )
832
827
  return None
833
828
  try:
834
829
  return identity.validate_program_id(value, source=source)
@@ -855,9 +850,7 @@ def cmd_deploy(client: api_client.ApiClient, args: Any) -> None:
855
850
 
856
851
  from ... import authored, configuration, deployment
857
852
 
858
- # Refused before the file is staged or anything is sent: a deployment always
859
- # names its program, and the server would refuse the create anyway.
860
- program_id = _program_id_arg(getattr(args, "program_id", None), required=True)
853
+ program_id = _program_id_arg(getattr(args, "program_id", None))
861
854
  configuration.configure(api_key=client._api_key, endpoint=client._endpoint, program_id=program_id)
862
855
  try:
863
856
  if args.source == "-" or args.source is None:
@@ -881,7 +874,10 @@ def cmd_deploy(client: api_client.ApiClient, args: Any) -> None:
881
874
 
882
875
  metadata = _parse_deploy_metadata(getattr(args, "metadata", None))
883
876
  print(_c("deploying…", "dim"), file=sys.stderr)
884
- deployed = deployment.deploy_bundle(prepared.bundle, prepared.facts, metadata=metadata, program_id=program_id)
877
+ try:
878
+ deployed = deployment.deploy_bundle(prepared.bundle, prepared.facts, metadata=metadata, program_id=program_id)
879
+ except ValueError as exc:
880
+ raise api_client.CliError(str(exc)) from exc
885
881
 
886
882
  deployment_id = deployed.deployment_id
887
883
  print(deployment_id)
@@ -946,9 +942,7 @@ def cmd_optimize(client: api_client.ApiClient, args: Any) -> Any:
946
942
  from ... import authored, configuration, optimization
947
943
 
948
944
  deployment_id = getattr(args, "deployment_id", None)
949
- # Required only when the file's program is deployed here; a reused deployment
950
- # already names its program.
951
- program_id = _program_id_arg(getattr(args, "program_id", None), required=deployment_id is None)
945
+ program_id = _program_id_arg(getattr(args, "program_id", None))
952
946
  configuration.configure(api_key=client._api_key, endpoint=client._endpoint, program_id=program_id)
953
947
  try:
954
948
  if args.source == "-" or args.source is None:
@@ -1266,7 +1260,7 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
1266
1260
  dest="program_id",
1267
1261
  default=None,
1268
1262
  help="Program id (p_…) the deployment belongs to, from `cmpnd programs create`. "
1269
- "Required; defaults to CMPND_PROGRAM_ID",
1263
+ "Defaults to CMPND_PROGRAM_ID. Required by every serve after self-host release 2026-001",
1270
1264
  )
1271
1265
  dp.add_argument(
1272
1266
  "--entrypoint",
@@ -1351,8 +1345,8 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
1351
1345
  dest="program_id",
1352
1346
  default=None,
1353
1347
  help="Program id (p_…) being optimized, from `cmpnd programs create`; defaults to "
1354
- "CMPND_PROGRAM_ID. Required when the file's program is deployed here, optional "
1355
- "with --deployment-id",
1348
+ "CMPND_PROGRAM_ID. Required by every serve after self-host release 2026-001 when "
1349
+ "the file's program is deployed here, optional with --deployment-id",
1356
1350
  )
1357
1351
  opt.add_argument(
1358
1352
  "--reflection-model",
@@ -490,8 +490,8 @@ def _make_namespace(client: api_client.ApiClient, endpoint: str) -> dict[str, An
490
490
  """Deploy authored Python from a file or ``"-"`` for stdin.
491
491
 
492
492
  ``program_id`` (``p_…``, from ``cmpnd programs create``) names the program
493
- the deployment belongs to; it defaults to ``CMPND_PROGRAM_ID`` and one of
494
- the two is required. A fresh child imports the staged bundle. The shell
493
+ the deployment belongs to; it defaults to ``CMPND_PROGRAM_ID``. serve
494
+ decides whether a deployment needs one. A fresh child imports the staged bundle. The shell
495
495
  does not execute the file in its own process. Returns the
496
496
  new Deployment entity so you can chain ``.watch()`` / ``.run({...})``
497
497
  directly; also invalidates ``deps`` so the new deployment shows up
@@ -499,7 +499,7 @@ def _make_namespace(client: api_client.ApiClient, endpoint: str) -> dict[str, An
499
499
  """
500
500
  from .. import authored, configuration, deployment
501
501
 
502
- declared = commands.top._program_id_arg(program_id, required=True)
502
+ declared = commands.top._program_id_arg(program_id)
503
503
  configuration.configure(api_key=client._api_key, endpoint=client._endpoint, program_id=declared)
504
504
  if source in ("-", None):
505
505
  prepared = authored.prepare_stdin(
@@ -346,21 +346,35 @@ def _resolve_mcp_tool_references(client: httpx.Client, config, references: list[
346
346
 
347
347
 
348
348
  MISSING_PROGRAM_ID = (
349
- "deploying needs a program id: pass program_id= (`cmpnd deploy --program-id`), or set "
349
+ "this server requires a program id: pass program_id= (`cmpnd deploy --program-id`), or set "
350
350
  "cmpnd.configure(program_id=...) or CMPND_PROGRAM_ID. Create one with `cmpnd programs create`."
351
351
  )
352
352
 
353
+ # The error serve answers, with a 400, to a create that names no program. A serve
354
+ # from before program ids accepts the create instead. serve pins this prefix in
355
+ # deploymentsapi.missingProgramIDError.
356
+ _SERVE_MISSING_PROGRAM_ID = "program_id is required"
353
357
 
354
- def require_program_id(explicit: str | None, config: configuration.Config) -> str:
358
+
359
+ def declared_program_id(explicit: str | None, config: configuration.Config) -> str | None:
355
360
  """The program a deployment is created under: the explicit id, else the configured one.
356
361
 
357
- A deployment always names its program, so a missing id is refused here, before
358
- anything is packaged or sent, rather than by the server after a create.
362
+ A declared id is checked for shape before anything is packaged or sent. serve
363
+ decides whether a deployment needs an id, so a missing one is not refused here.
359
364
  """
360
365
  chosen = explicit if explicit is not None else config.program_id
361
- if chosen is None:
362
- raise ValueError(MISSING_PROGRAM_ID)
363
- return identity.validate_program_id(chosen)
366
+ return identity.validate_program_id(chosen) if chosen is not None else None
367
+
368
+
369
+ def _refused_for_missing_program_id(response: httpx.Response, deploy_body: dict[str, Any]) -> bool:
370
+ """Whether serve refused a create that named no program because it needs one."""
371
+ if response.status_code != 400 or "program_id" in deploy_body:
372
+ return False
373
+ try:
374
+ error = response.json().get("error")
375
+ except (ValueError, AttributeError):
376
+ return False
377
+ return isinstance(error, str) and error.startswith(_SERVE_MISSING_PROGRAM_ID)
364
378
 
365
379
 
366
380
  def deploy(module, metric=None, timeout=300, metadata=None, *, program_id=None):
@@ -387,22 +401,24 @@ def deploy(module, metric=None, timeout=300, metadata=None, *, program_id=None):
387
401
  with string keys, within 64 KiB. Read it back via the deployment
388
402
  record (``cmpnd.status`` / the deployment GET API).
389
403
  program_id: The program (``p_…``) the deployment belongs to, as
390
- ``cmpnd programs create`` printed it. Required: when omitted, the
391
- configured id (``cmpnd.configure(program_id=...)`` or
392
- ``CMPND_PROGRAM_ID``) is used, and with neither the deploy is
393
- refused before anything is packaged.
404
+ ``cmpnd programs create`` printed it. When omitted, the configured
405
+ id (``cmpnd.configure(program_id=...)`` or ``CMPND_PROGRAM_ID``) is
406
+ used. With neither, the create names no program, and serve decides:
407
+ every serve with program ids refuses it, and a serve from before
408
+ them, such as self-host release 2026-001, accepts it.
394
409
 
395
410
  Returns:
396
411
  A DeployedProgram that can be called with the same inputs as the
397
412
  original module. Has a .deployment_id attribute and str() returns it.
398
413
 
399
414
  Raises:
400
- ValueError: If no program id is given or configured, or it is malformed.
415
+ ValueError: If the program id is malformed, or if serve requires one and
416
+ none was given or configured.
401
417
  RuntimeError: If cmpnd is not configured, or if deployment fails.
402
418
  TimeoutError: If the program isn't ready within timeout seconds.
403
419
  """
404
420
  config = configuration.require_config()
405
- declared_program_id = require_program_id(program_id, config)
421
+ program = declared_program_id(program_id, config)
406
422
 
407
423
  # Resolve the program's MCP tool references against the org's registry, before
408
424
  # anything is packaged and before a deployment row exists. A program with no MCP
@@ -423,12 +439,15 @@ def deploy(module, metric=None, timeout=300, metadata=None, *, program_id=None):
423
439
  program_signature_hash = identity.compute_program_hash(module) or 0
424
440
 
425
441
  deploy_body: dict[str, Any] = {
426
- "program_id": declared_program_id,
427
442
  "program_signature_hash": identity.format_signature_hash(program_signature_hash),
428
443
  # Declare the ZIP's exact length so the server can size-bound the upload
429
444
  # slot and reject an oversized deploy before signing it (Slice E).
430
445
  "declared_content_length": len(zip_bytes),
431
446
  }
447
+ # Left off when undeclared, so a serve from before program ids reads the body it
448
+ # always read and a newer serve answers with its own refusal.
449
+ if program is not None:
450
+ deploy_body["program_id"] = program
432
451
  # Carry the caller's metadata blob (validated and made jsonb-storable at the
433
452
  # call site) as a reserved top-level body key, committed with the deployment
434
453
  # record server-side.
@@ -496,13 +515,14 @@ def deploy_bundle(
496
515
  ) -> execution.DeployedProgram:
497
516
  """Upload a checked authored-source bundle without packaging a live module.
498
517
 
499
- ``program_id`` is required as it is for :func:`deploy`: the explicit id, else
500
- the configured one, else a ``ValueError`` before anything is sent.
518
+ ``program_id`` works as it does for :func:`deploy`: the explicit id, else the
519
+ configured one, else none, and serve decides whether a deployment needs one.
501
520
  """
502
521
  config = configuration.require_config()
503
- declared_program_id = require_program_id(program_id, config)
522
+ program = declared_program_id(program_id, config)
504
523
  deploy_body = facts.deploy_body()
505
- deploy_body["program_id"] = declared_program_id
524
+ if program is not None:
525
+ deploy_body["program_id"] = program
506
526
  deploy_body["source_format"] = "authored"
507
527
  deploy_body["declared_content_length"] = len(zip_bytes)
508
528
  if metadata is not None:
@@ -531,6 +551,8 @@ def _deploy_prepared(
531
551
  may_resend_with_no_answer=retry.never_left_the_client,
532
552
  may_resend_after_answer=lambda _: False,
533
553
  )
554
+ if _refused_for_missing_program_id(created, deploy_body):
555
+ raise ValueError(MISSING_PROGRAM_ID)
534
556
  _raise_http_error(created, "create deployment")
535
557
  answer = created.json()
536
558
  deployment_id = answer["deployment_id"]
@@ -38,7 +38,7 @@ from typing import TYPE_CHECKING, Any, Callable, cast
38
38
  import httpx
39
39
  from httpx import HTTPError
40
40
 
41
- from . import _deployments, _http, authored, configuration, deployment, execution, identity, packaging, retry
41
+ from . import _deployments, _http, authored, configuration, deployment, execution, packaging, retry
42
42
  from .deployment_metadata import DeploymentMetadataError, DeploymentSignatureResponse
43
43
 
44
44
  if TYPE_CHECKING:
@@ -831,10 +831,11 @@ def optimize(
831
831
  deployment_id: Skip the initial deploy by reusing an existing ready
832
832
  deployment as program A.
833
833
  program_id: The program (``p_…``) being optimized. Defaults to the
834
- configured id. Required when the program is deployed here (no
835
- ``deployment_id``), because a deployment always names its program;
836
- optional otherwise, since the deployment already names one. Sent on
837
- the first optimize step, the one that optimizes your deployment.
834
+ configured id. When the program is deployed here (no
835
+ ``deployment_id``), the deploy carries it, and serve decides whether
836
+ that deploy needs one. A reused deployment already names its program.
837
+ Sent on the first optimize step, the one that optimizes your
838
+ deployment.
838
839
  reflection_lm: **Required** — the model that proposes new instructions.
839
840
  The search is only as good as it is, so this is a strong model,
840
841
  typically not the one being optimized. Pass the ``dspy.LM`` you would
@@ -925,7 +926,7 @@ def optimize(
925
926
  _apply_budget(config, max_metric_calls=max_metric_calls, auto=auto)
926
927
 
927
928
  cfg = configuration.require_config()
928
- program_id = _declared_program_id(program_id, cfg, deploys=deployment_id is None)
929
+ program_id = deployment.declared_program_id(program_id, cfg)
929
930
 
930
931
  from . import lm_configuration
931
932
 
@@ -957,19 +958,6 @@ def optimize(
957
958
  return _deployed_program(best_deployment_id, cfg, score=best_score)
958
959
 
959
960
 
960
- def _declared_program_id(explicit: str | None, cfg: configuration.Config, *, deploys: bool) -> str | None:
961
- """The program an optimization names: the explicit id, else the configured one.
962
-
963
- Required when this call deploys the program, since a deployment always names its
964
- program, and refused before anything is sent. Optional when an existing
965
- deployment is reused, because that deployment already names one.
966
- """
967
- if deploys:
968
- return deployment.require_program_id(explicit, cfg)
969
- chosen = explicit if explicit is not None else cfg.program_id
970
- return identity.validate_program_id(chosen) if chosen is not None else None
971
-
972
-
973
961
  def _run_steps(
974
962
  cfg: configuration.Config,
975
963
  bundle: bytes,
@@ -1094,8 +1082,8 @@ def optimize_authored(
1094
1082
  one directory on the executor, so a path they disagree about is refused, and it is
1095
1083
  worth refusing here where both files are in reach.
1096
1084
 
1097
- ``program_id`` is :func:`optimize`'s: required when the file's program is deployed
1098
- here, optional when ``deployment_id`` names the program.
1085
+ ``program_id`` is :func:`optimize`'s: the deploy of the file's program carries it,
1086
+ and serve decides whether that deploy needs one.
1099
1087
  """
1100
1088
  check_optimizer(optimizer)
1101
1089
  config = dict(config or {})
@@ -1111,7 +1099,7 @@ def optimize_authored(
1111
1099
  _apply_budget(config, max_metric_calls=max_metric_calls, auto=auto)
1112
1100
 
1113
1101
  cfg = configuration.require_config()
1114
- program_id = _declared_program_id(program_id, cfg, deploys=prepared.program_bundle is not None)
1102
+ program_id = deployment.declared_program_id(program_id, cfg)
1115
1103
 
1116
1104
  facts = prepared.facts.program
1117
1105
  if prepared.program_bundle is not None:
@@ -19,6 +19,11 @@ from . import _http, configuration
19
19
 
20
20
  DEFAULT_ENDPOINT = "https://platform.cmpnd.ai"
21
21
 
22
+ # Self-host release 2026-001 answers a programs create with 405, because it routes
23
+ # GET requests under /api/v1/programs/ and has no create. Every newer serve accepts
24
+ # the create, so on this one route a 405 means a serve from before program ids.
25
+ UNSUPPORTED_STATUS = 405
26
+
22
27
 
23
28
  def create(
24
29
  name: str | None = None,
@@ -61,7 +66,18 @@ def send_create(
61
66
  body["name"] = name
62
67
  if optimized_from_run_id is not None:
63
68
  body["optimized_from_run_id"] = optimized_from_run_id
64
- return client.post("/api/v1/programs", json=body)
69
+ try:
70
+ return client.post("/api/v1/programs", json=body)
71
+ except _http.ApiError as exc:
72
+ if exc.status_code == UNSUPPORTED_STATUS:
73
+ raise _http.ApiError(
74
+ "This server does not support program ids. A serve from before program ids, such as "
75
+ "self-host release 2026-001, deploys without one: run `cmpnd deploy` without "
76
+ f"--program-id.\n {exc}",
77
+ exc.exit_code,
78
+ status_code=exc.status_code,
79
+ ) from exc
80
+ raise
65
81
 
66
82
 
67
83
  def _client(api_key: str | None, endpoint: str | None) -> _http.ApiClient:
@@ -19,10 +19,15 @@ PROGRAM_ID=$(cmpnd programs create --name triage)
19
19
  ```
20
20
 
21
21
  The command reports what it created on stderr, and `--json` prints the whole
22
- registry row instead. `cmpnd deploy` requires the id. `cmpnd optimize` takes
23
- it as `--program-id` too. It is required when the command deploys the file's
24
- program first, and optional with `--deployment-id`, because that deployment
25
- already names its program. Each call creates a new program. The command never
22
+ registry row instead. `cmpnd deploy` takes the id as `--program-id`, and so
23
+ does `cmpnd optimize` when it deploys the file's program first. With
24
+ `--deployment-id`, the id is optional because that deployment already names its
25
+ program. The CLI checks a given id's shape and leaves the requirement to serve.
26
+ Every serve after self-host release 2026-001 refuses a deploy with no id, and
27
+ the CLI then names `--program-id` and `cmpnd programs create`. Self-host release
28
+ 2026-001 has no program ids, so a deploy there omits `--program-id`, and
29
+ `cmpnd programs create` reports that the server does not support them. Each
30
+ call creates a new program. The command never
26
31
  retries, because a second send would create a second program.
27
32
  `cmpnd.programs.create(name=...)` makes the same call from Python and returns
28
33
  the row.
@@ -757,11 +757,14 @@ the key is left off the wire rather than sent as `null`, so an undeclared trace
757
757
  it did before ids existed. The finalize body carries no id, because it restates no
758
758
  creation field.
759
759
 
760
- A deployment always names its program. `deploy()` and `deploy_bundle()` take
761
- `program_id=` and fall back to the configured id; with neither they raise `ValueError`
762
- before anything is packaged or sent (`deployment.require_program_id`). `optimize()` and
763
- `optimize_authored()` need the id only when they deploy the program themselves. A reused
764
- `deployment_id` already names its program, so the id is optional there. Later steps
760
+ `deploy()` and `deploy_bundle()` take `program_id=` and fall back to the configured id
761
+ (`deployment.declared_program_id`). A given id is checked for shape before anything is
762
+ packaged or sent. With no id the create body leaves the key off, and serve decides.
763
+ Every serve after self-host release 2026-001 refuses that create with a `400`, which the
764
+ SDK raises as `ValueError` naming `cmpnd programs create`. Self-host release 2026-001
765
+ has no program ids and accepts it. `optimize()` and `optimize_authored()` pass the id to
766
+ the deploy when they deploy the program themselves. A reused `deployment_id` already
767
+ names its program, so the id is optional there. Later steps
765
768
  optimize a result deployment the platform created and whose program it allocated, so
766
769
  the caller's id rides only the steps on the caller's own deployment.
767
770
 
@@ -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.40.1"
8
+ version = "0.41.0"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -24,6 +24,7 @@ classifiers = [
24
24
  "Programming Language :: Python :: 3.12",
25
25
  "Programming Language :: Python :: 3.13",
26
26
  "Programming Language :: Python :: 3.14",
27
+ "Programming Language :: Python :: 3.15",
27
28
  "Topic :: Software Development :: Libraries :: Python Modules",
28
29
  ]
29
30
 
@@ -95,6 +96,12 @@ dev = [
95
96
  "ruff>=0.4.0",
96
97
  "basedpyright>=1.39",
97
98
  "jsonschema>=4.0",
99
+ # The tests import litellm (tests/e2e/fake_lm.py and the modules that reuse it) and
100
+ # gepa directly. dspy 3.4.1 installs both only below Python 3.15, because both declare
101
+ # requires-python <3.15. uv ignores that upper bound, so the 3.15 test leg still
102
+ # installs them.
103
+ "litellm",
104
+ "gepa",
98
105
  # litellm's bedrock/ provider imports boto3 in-process, so the client-side
99
106
  # Bedrock stress tests (committee twins) need it in the test env.
100
107
  "boto3>=1.34",
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # Install cmpnd with pip's own resolver and check that the installed package
3
+ # imports and instruments DSPy. Called by .github/workflows/ci-sdk.yml (the
4
+ # sdk-test leg for Python 3.15).
5
+ #
6
+ # `pip install cmpnd` is the install PyPI advertises. pip honours the
7
+ # requires-python upper bounds that uv ignores, so this check fails when no
8
+ # dspy release admits the Python version. PYO3_USE_ABI3_FORWARD_COMPATIBILITY
9
+ # is set to 0, so an extension without a wheel for this Python fails the
10
+ # install and does not build from source.
11
+ #
12
+ # Usage: scripts/check-pip-install.sh <python-version> <dspy-version>
13
+ # e.g. scripts/check-pip-install.sh 3.15 3.4.1
14
+ set -euo pipefail
15
+
16
+ if [ "$#" -ne 2 ]; then
17
+ echo "usage: $0 <python-version> <dspy-version>" >&2
18
+ exit 2
19
+ fi
20
+ python_version=$1
21
+ dspy_version=$2
22
+
23
+ sdk_dir=$(cd "$(dirname "$0")/.." && pwd)
24
+ venv_dir=$(mktemp -d)
25
+ trap 'rm -rf "$venv_dir"' EXIT
26
+
27
+ uv venv --seed --python "$python_version" "$venv_dir"
28
+ PYO3_USE_ABI3_FORWARD_COMPATIBILITY=0 \
29
+ "$venv_dir/bin/python" -m pip install "$sdk_dir" "dspy==$dspy_version"
30
+
31
+ # -I keeps the checkout off sys.path, so the import reads the installed
32
+ # package and not the source tree.
33
+ cd "$venv_dir"
34
+ "$venv_dir/bin/python" -I -c "import cmpnd; cmpnd.configure(api_key='ci'); cmpnd.auto_instrument(); print('cmpnd', cmpnd.__file__)"
@@ -639,17 +639,23 @@ class TestDeployHelper:
639
639
  prepare.assert_called_once()
640
640
  assert deploy.call_args.kwargs["program_id"] == "p_0123456789Ab"
641
641
 
642
- def test_a_deploy_without_a_program_id_stages_nothing(self, tmp_path, monkeypatch):
643
- from cmpnd import authored
642
+ def test_a_deploy_without_a_program_id_sends_none(self, tmp_path, monkeypatch):
643
+ from cmpnd import authored, deployment
644
644
 
645
645
  monkeypatch.delenv("CMPND_PROGRAM_ID", raising=False)
646
646
  ns = self._ns(tmp_path, monkeypatch)
647
- prepare = mock.Mock()
647
+ prepare = mock.create_autospec(authored.prepare_file)
648
+ prepare.return_value = mock.Mock(bundle=b"zip", facts=authored.ProgramFacts.minimal())
648
649
  monkeypatch.setattr(authored, "prepare_file", prepare)
650
+ deploy = mock.create_autospec(deployment.deploy_bundle)
651
+ deploy.return_value = mock.Mock(deployment_id="dep-1")
652
+ monkeypatch.setattr(deployment, "deploy_bundle", deploy)
653
+ client = ns["client"]
654
+ monkeypatch.setattr(client, "get", lambda _path: {"deployment_id": "dep-1", "status": "ready"})
655
+
656
+ ns["deploy"](str(tmp_path / "program.py"))
649
657
 
650
- with pytest.raises(shell.api_client.CliError, match="cmpnd programs create"):
651
- ns["deploy"](str(tmp_path / "program.py"))
652
- prepare.assert_not_called()
658
+ assert deploy.call_args.kwargs["program_id"] is None
653
659
 
654
660
 
655
661
  class TestReplFallback:
@@ -758,7 +758,7 @@ def _deploy_args(source: str, program_id: str | None = PROGRAM_ID) -> argparse.N
758
758
 
759
759
 
760
760
  class TestDeployNamesItsProgram:
761
- """`cmpnd deploy` refuses without a program id before staging or sending anything."""
761
+ """`cmpnd deploy` checks a program id's shape and leaves the requirement to serve."""
762
762
 
763
763
  def _stubs(self, monkeypatch):
764
764
  from cmpnd import deployment
@@ -771,17 +771,27 @@ class TestDeployNamesItsProgram:
771
771
  monkeypatch.setattr(deployment, "deploy_bundle", deploy)
772
772
  return prepare_file, prepare_stdin, deploy
773
773
 
774
- def test_a_missing_id_is_a_usage_error_naming_the_flag_and_the_create_command(self, monkeypatch):
775
- prepare_file, prepare_stdin, deploy = self._stubs(monkeypatch)
774
+ def test_a_missing_id_deploys_without_one(self, monkeypatch):
775
+ prepare_file, _, deploy = self._stubs(monkeypatch)
776
+ prepare_file.return_value = mock.Mock(bundle=b"zip", facts=authored.ProgramFacts.minimal())
777
+ deploy.return_value = _DeployedStub("d_0123456789ab")
778
+
779
+ top.cmd_deploy(_fake_client(), _deploy_args("program.py", program_id=None))
780
+
781
+ assert deploy.call_args.kwargs["program_id"] is None
782
+
783
+ def test_a_serve_refusal_for_a_missing_id_names_the_flag_and_the_create_command(self, monkeypatch):
784
+ from cmpnd import deployment
785
+
786
+ prepare_file, _, deploy = self._stubs(monkeypatch)
787
+ prepare_file.return_value = mock.Mock(bundle=b"zip", facts=authored.ProgramFacts.minimal())
788
+ deploy.side_effect = ValueError(deployment.MISSING_PROGRAM_ID)
776
789
 
777
790
  with pytest.raises(CliError) as refused:
778
791
  top.cmd_deploy(_fake_client(), _deploy_args("program.py", program_id=None))
779
792
 
780
793
  assert "--program-id" in str(refused.value)
781
794
  assert "cmpnd programs create" in str(refused.value)
782
- prepare_file.assert_not_called()
783
- prepare_stdin.assert_not_called()
784
- deploy.assert_not_called()
785
795
 
786
796
  def test_a_malformed_id_is_refused_before_staging(self, monkeypatch):
787
797
  prepare_file, _, deploy = self._stubs(monkeypatch)
@@ -808,17 +818,16 @@ class TestDeployNamesItsProgram:
808
818
 
809
819
 
810
820
  class TestOptimizeNamesItsProgram:
811
- def test_a_file_that_deploys_its_program_needs_an_id_before_staging(self, monkeypatch):
821
+ def test_a_file_that_deploys_its_program_without_an_id_sends_none(self, monkeypatch):
812
822
  monkeypatch.setattr("sys.stdin", _stdin(_OPTIMIZE_FILE))
813
823
  args = _parse_optimize(["optimize", "gepa", "-"])
814
824
  with (
815
- mock.patch("cmpnd.authored.prepare_optimize_stdin", autospec=True) as m_stage,
816
- mock.patch("cmpnd.optimization.optimize_authored", autospec=True) as m_opt,
817
- pytest.raises(CliError, match="cmpnd programs create"),
825
+ mock.patch("cmpnd.configuration.configure", autospec=True),
826
+ mock.patch("cmpnd.authored.prepare_optimize_stdin", autospec=True, return_value=_prepared()),
827
+ mock.patch("cmpnd.optimization.optimize_authored", autospec=True, return_value=_FakeResult()) as m_opt,
818
828
  ):
819
829
  top.cmd_optimize(_fake_client(), args)
820
- m_stage.assert_not_called()
821
- m_opt.assert_not_called()
830
+ assert m_opt.call_args.kwargs["program_id"] is None
822
831
 
823
832
  def test_the_id_reaches_the_loop(self, monkeypatch):
824
833
  monkeypatch.setattr("sys.stdin", _stdin(_OPTIMIZE_FILE))