cmpnd 0.45.0__tar.gz → 0.47.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 (379) hide show
  1. {cmpnd-0.45.0 → cmpnd-0.47.0}/PKG-INFO +1 -1
  2. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/__init__.py +4 -0
  3. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/_authored_import.py +2 -4
  4. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/_http.py +45 -1
  5. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/dataset_sync.py +2 -4
  6. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/datasets.py +2 -4
  7. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/deployment.py +24 -13
  8. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/deployment_runs.py +1 -1
  9. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/mcp_servers.py +2 -3
  10. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/optimization.py +18 -45
  11. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/packaging.py +1 -1
  12. {cmpnd-0.45.0 → cmpnd-0.47.0}/docs/cli.md +10 -0
  13. {cmpnd-0.45.0 → cmpnd-0.47.0}/docs/sdk-instrumentation.md +9 -0
  14. cmpnd-0.47.0/ingest/cmpnd_ingest/client_floor.py +101 -0
  15. {cmpnd-0.45.0 → cmpnd-0.47.0}/ingest/cmpnd_ingest/exporter_http.py +4 -3
  16. {cmpnd-0.45.0 → cmpnd-0.47.0}/pyproject.toml +1 -1
  17. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_deploy_programs.py +5 -7
  18. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_optimize_server_owned_completion.py +7 -12
  19. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/integration/conftest.py +13 -12
  20. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/integration/test_logs_renders_traces.py +1 -2
  21. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/integration/test_unhappy_path.py +1 -1
  22. cmpnd-0.47.0/tests/test_client_floor.py +181 -0
  23. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_dataset_sync.py +5 -5
  24. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_deploy.py +52 -8
  25. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_optimize.py +123 -45
  26. cmpnd-0.47.0/tests/test_upload_auth_headers.py +59 -0
  27. {cmpnd-0.45.0 → cmpnd-0.47.0}/uv.lock +1 -1
  28. {cmpnd-0.45.0 → cmpnd-0.47.0}/.gitignore +0 -0
  29. {cmpnd-0.45.0 → cmpnd-0.47.0}/AGENTS.md +0 -0
  30. {cmpnd-0.45.0 → cmpnd-0.47.0}/CLAUDE.md +0 -0
  31. {cmpnd-0.45.0 → cmpnd-0.47.0}/README.md +0 -0
  32. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/_assessments.py +0 -0
  33. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/_deployments.py +0 -0
  34. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/_exporter_pool.py +0 -0
  35. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/_gepa_patch.py +0 -0
  36. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/_program_patch.py +0 -0
  37. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/_rlm_patch.py +0 -0
  38. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/_runtime_tools.py +0 -0
  39. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/_timespec.py +0 -0
  40. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/_traces.py +0 -0
  41. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/_valsets.py +0 -0
  42. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/authored.py +0 -0
  43. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/callback.py +0 -0
  44. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/__init__.py +0 -0
  45. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/api_client.py +0 -0
  46. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/__init__.py +0 -0
  47. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/admin.py +0 -0
  48. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/assessments.py +0 -0
  49. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/auth.py +0 -0
  50. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/datasets.py +0 -0
  51. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/deployments.py +0 -0
  52. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/evals.py +0 -0
  53. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/login.py +0 -0
  54. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/optimizations.py +0 -0
  55. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/org.py +0 -0
  56. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/programs.py +0 -0
  57. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  58. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/signatures.py +0 -0
  59. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  60. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/top.py +0 -0
  61. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/traces.py +0 -0
  62. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/commands/valsets.py +0 -0
  63. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/credentials.py +0 -0
  64. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/deprecation.py +0 -0
  65. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/pagination.py +0 -0
  66. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/shell.py +0 -0
  67. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/trace_render.py +0 -0
  68. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/tree_help.py +0 -0
  69. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/cli/verbs.py +0 -0
  70. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/client.py +0 -0
  71. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/configuration.py +0 -0
  72. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/context.py +0 -0
  73. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/decorators.py +0 -0
  74. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/deployment_metadata.py +0 -0
  75. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/eval_handler.py +0 -0
  76. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/execution.py +0 -0
  77. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/exporter.py +0 -0
  78. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/exporter_http.py +0 -0
  79. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/helpers.py +0 -0
  80. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/identity.py +0 -0
  81. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/imaging.py +0 -0
  82. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/ir/__init__.py +0 -0
  83. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/ir/decode.py +0 -0
  84. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/ir/encode.py +0 -0
  85. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/ir/hash.py +0 -0
  86. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/ir/program.py +0 -0
  87. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/ir/reflection.py +0 -0
  88. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/ir/types.py +0 -0
  89. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/ir/xxh64.py +0 -0
  90. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/lm_configuration.py +0 -0
  91. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/models.py +0 -0
  92. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/named_programs.py +0 -0
  93. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/optimizers/__init__.py +0 -0
  94. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/optimizers/gepa.py +0 -0
  95. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  96. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/optimizers/resume.py +0 -0
  97. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/optimizers/tracker.py +0 -0
  98. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/programs.py +0 -0
  99. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/references.py +0 -0
  100. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/retry.py +0 -0
  101. {cmpnd-0.45.0 → cmpnd-0.47.0}/cmpnd/traces.py +0 -0
  102. {cmpnd-0.45.0 → cmpnd-0.47.0}/docs/2026-04-02-code-review.md +0 -0
  103. {cmpnd-0.45.0 → cmpnd-0.47.0}/docs/client.md +0 -0
  104. {cmpnd-0.45.0 → cmpnd-0.47.0}/docs/custom-proposer.md +0 -0
  105. {cmpnd-0.45.0 → cmpnd-0.47.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  106. {cmpnd-0.45.0 → cmpnd-0.47.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  107. {cmpnd-0.45.0 → cmpnd-0.47.0}/docs/reading-traces.md +0 -0
  108. {cmpnd-0.45.0 → cmpnd-0.47.0}/examples/generalizing_proposer.py +0 -0
  109. {cmpnd-0.45.0 → cmpnd-0.47.0}/examples/local_ollama.py +0 -0
  110. {cmpnd-0.45.0 → cmpnd-0.47.0}/ingest/LINEAGE.md +0 -0
  111. {cmpnd-0.45.0 → cmpnd-0.47.0}/ingest/README.md +0 -0
  112. {cmpnd-0.45.0 → cmpnd-0.47.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  113. {cmpnd-0.45.0 → cmpnd-0.47.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  114. {cmpnd-0.45.0 → cmpnd-0.47.0}/ingest/cmpnd_ingest/program_names.py +0 -0
  115. {cmpnd-0.45.0 → cmpnd-0.47.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  116. {cmpnd-0.45.0 → cmpnd-0.47.0}/ingest/cmpnd_ingest/py.typed +0 -0
  117. {cmpnd-0.45.0 → cmpnd-0.47.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  118. {cmpnd-0.45.0 → cmpnd-0.47.0}/ingest/cmpnd_ingest/retry.py +0 -0
  119. {cmpnd-0.45.0 → cmpnd-0.47.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  120. {cmpnd-0.45.0 → cmpnd-0.47.0}/ingest/pyproject.toml +0 -0
  121. {cmpnd-0.45.0 → cmpnd-0.47.0}/scripts/audit-sdist.sh +0 -0
  122. {cmpnd-0.45.0 → cmpnd-0.47.0}/scripts/capture_review_run.py +0 -0
  123. {cmpnd-0.45.0 → cmpnd-0.47.0}/scripts/check-pip-install.sh +0 -0
  124. {cmpnd-0.45.0 → cmpnd-0.47.0}/scripts/check_release_artifacts.py +0 -0
  125. {cmpnd-0.45.0 → cmpnd-0.47.0}/scripts/fixtures/review_run.json.gz +0 -0
  126. {cmpnd-0.45.0 → cmpnd-0.47.0}/scripts/gepa_resume_bug.py +0 -0
  127. {cmpnd-0.45.0 → cmpnd-0.47.0}/scripts/release-stress.sh +0 -0
  128. {cmpnd-0.45.0 → cmpnd-0.47.0}/scripts/require_version_bump.py +0 -0
  129. {cmpnd-0.45.0 → cmpnd-0.47.0}/scripts/seed_review_run.py +0 -0
  130. {cmpnd-0.45.0 → cmpnd-0.47.0}/scripts/test-in-linux.sh +0 -0
  131. {cmpnd-0.45.0 → cmpnd-0.47.0}/stubs/dspy/__init__.pyi +0 -0
  132. {cmpnd-0.45.0 → cmpnd-0.47.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  133. {cmpnd-0.45.0 → cmpnd-0.47.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  134. {cmpnd-0.45.0 → cmpnd-0.47.0}/stubs/dspy/utils/__init__.pyi +0 -0
  135. {cmpnd-0.45.0 → cmpnd-0.47.0}/stubs/dspy/utils/callback.pyi +0 -0
  136. {cmpnd-0.45.0 → cmpnd-0.47.0}/stubs/dspy/utils/dummies.pyi +0 -0
  137. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/__init__.py +0 -0
  138. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/_deployment_signature_helpers.py +0 -0
  139. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/_exporter_helpers.py +0 -0
  140. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/_fake_clock.py +0 -0
  141. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/_identity_helpers.py +0 -0
  142. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/_resolve_refs.py +0 -0
  143. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/_traced_run.py +0 -0
  144. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/conftest.py +0 -0
  145. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/__init__.py +0 -0
  146. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/committee_task.py +0 -0
  147. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/conftest.py +0 -0
  148. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  149. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  150. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/fake_lm.py +0 -0
  151. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/helpers.py +0 -0
  152. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/target_capabilities.py +0 -0
  153. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_assessments_cli.py +0 -0
  154. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_capture_prompts.py +0 -0
  155. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_composite_module.py +0 -0
  156. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  157. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_deploy_execute.py +0 -0
  158. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_deploy_pandas.py +0 -0
  159. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_edge_cases.py +0 -0
  160. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_example_hash.py +0 -0
  161. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_gepa_optimization.py +0 -0
  162. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_interpreter_spans.py +0 -0
  163. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_lm_accuracy.py +0 -0
  164. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_module_tracing.py +0 -0
  165. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_optimization_lineage.py +0 -0
  166. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  167. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_optimize_failures.py +0 -0
  168. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_optimize_loop.py +0 -0
  169. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  170. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_parallel_export.py +0 -0
  171. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_project_attribution.py +0 -0
  172. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_react_tool_spans.py +0 -0
  173. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_save_load.py +0 -0
  174. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_trace_structure.py +0 -0
  175. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_traces_read.py +0 -0
  176. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_user_attribution.py +0 -0
  177. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/e2e/test_valsets_cli.py +0 -0
  178. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  179. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  180. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  181. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  182. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  183. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  184. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  185. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  186. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  187. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/10_react.json +0 -0
  188. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/11_refine.json +0 -0
  189. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/12_retrieve.json +0 -0
  190. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/13_knn.json +0 -0
  191. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/14_parallel.json +0 -0
  192. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/identity_vectors/README.md +0 -0
  193. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/ingest_conformance.py +0 -0
  194. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/integration/__init__.py +0 -0
  195. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/integration/authored_optimize/judge.py +0 -0
  196. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  197. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  198. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/integration/hosted_optimize_probe.py +0 -0
  199. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/integration/optimization_run_probe.py +0 -0
  200. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  201. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/integration/test_optimization_run.py +0 -0
  202. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/integration/test_optimizing_progress.py +0 -0
  203. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/load/__init__.py +0 -0
  204. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/load/__main__.py +0 -0
  205. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/load/drain.py +0 -0
  206. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/load/ingest.py +0 -0
  207. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/load/latency_transport.py +0 -0
  208. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/load/rlm_shape.py +0 -0
  209. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/load/rlm_size_report.py +0 -0
  210. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/load/sweep.py +0 -0
  211. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/load/test_rlm_shape.py +0 -0
  212. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/load/test_rlm_size_report.py +0 -0
  213. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/__init__.py +0 -0
  214. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/conftest.py +0 -0
  215. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/seed_parity.py +0 -0
  216. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/test_auth.py +0 -0
  217. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/test_datasets.py +0 -0
  218. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/test_deployments.py +0 -0
  219. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/test_evals.py +0 -0
  220. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/test_health.py +0 -0
  221. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/test_modules.py +0 -0
  222. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/test_optimizations.py +0 -0
  223. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/test_programs.py +0 -0
  224. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/test_signatures.py +0 -0
  225. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/test_trace_streaming.py +0 -0
  226. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/parity/test_traces.py +0 -0
  227. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/span_reference_fixture.py +0 -0
  228. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/__init__.py +0 -0
  229. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/_provider.py +0 -0
  230. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/_readiness.py +0 -0
  231. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/_reliability.py +0 -0
  232. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/_result.py +0 -0
  233. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/mint_bedrock_token.py +0 -0
  234. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_deploy_programs.py +0 -0
  235. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_multi_client_stress.py +0 -0
  236. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_optimize_client_committee.py +0 -0
  237. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_optimize_loop.py +0 -0
  238. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  239. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_optimize_stress.py +0 -0
  240. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_provider_roles.py +0 -0
  241. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_readiness.py +0 -0
  242. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_real_provider_twins.py +0 -0
  243. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_reliability.py +0 -0
  244. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_reliability_classify.py +0 -0
  245. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_rlm_wasm.py +0 -0
  246. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/stress/test_trace_search.py +0 -0
  247. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_adapter_reentrancy.py +0 -0
  248. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_assessments_api.py +0 -0
  249. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_authored.py +0 -0
  250. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_authored_bundle_collisions.py +0 -0
  251. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_authored_metric_contract.py +0 -0
  252. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_authored_named_models.py +0 -0
  253. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_authored_optimize.py +0 -0
  254. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_callback.py +0 -0
  255. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_candidate_lineage.py +0 -0
  256. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/__init__.py +0 -0
  257. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/conftest.py +0 -0
  258. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_admin.py +0 -0
  259. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_api_client.py +0 -0
  260. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_assessments.py +0 -0
  261. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_auth.py +0 -0
  262. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_credentials.py +0 -0
  263. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_datasets.py +0 -0
  264. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_deployment_runs.py +0 -0
  265. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_deployments.py +0 -0
  266. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_evals.py +0 -0
  267. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_login.py +0 -0
  268. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_main.py +0 -0
  269. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_optimizations.py +0 -0
  270. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_org.py +0 -0
  271. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_pagination.py +0 -0
  272. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_parity.py +0 -0
  273. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_programs.py +0 -0
  274. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_provider_credentials.py +0 -0
  275. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_schema_sample.py +0 -0
  276. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_shell.py +0 -0
  277. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_signatures.py +0 -0
  278. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_tool_servers.py +0 -0
  279. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_top.py +0 -0
  280. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_traces.py +0 -0
  281. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  282. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_tree_help.py +0 -0
  283. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cli/test_valsets.py +0 -0
  284. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_client.py +0 -0
  285. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_client_program_names.py +0 -0
  286. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_clustering/__init__.py +0 -0
  287. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_clustering/conftest.py +0 -0
  288. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_clustering/test_edge_cases.py +0 -0
  289. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  290. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_clustering/test_type_conversion.py +0 -0
  291. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_cmp551_grouping_identity.py +0 -0
  292. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_configuration.py +0 -0
  293. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_context.py +0 -0
  294. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_context_propagation.py +0 -0
  295. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_dataset_polling.py +0 -0
  296. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_datasets.py +0 -0
  297. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_decision_client_spans.py +0 -0
  298. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_decorators.py +0 -0
  299. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  300. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_deploy_program_id.py +0 -0
  301. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_deployment_metadata.py +0 -0
  302. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_deployment_runs.py +0 -0
  303. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_deployments_read.py +0 -0
  304. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_eval.py +0 -0
  305. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_eval_example_attribution.py +0 -0
  306. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_eval_start.py +0 -0
  307. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_eval_trace_grouping.py +0 -0
  308. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_exception_paths.py +0 -0
  309. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_execute.py +0 -0
  310. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_execute_run_identity.py +0 -0
  311. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_exporter.py +0 -0
  312. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_exporter_program_names.py +0 -0
  313. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_exporter_queue.py +0 -0
  314. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_exporter_real.py +0 -0
  315. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_fake_lm.py +0 -0
  316. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_gepa_callback.py +0 -0
  317. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_gepa_integration.py +0 -0
  318. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_gepa_output_normalization.py +0 -0
  319. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_gepa_resume.py +0 -0
  320. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_identity.py +0 -0
  321. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_imaging.py +0 -0
  322. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_import_weight.py +0 -0
  323. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_ingest_conformance.py +0 -0
  324. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_ingest_second_producer.py +0 -0
  325. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_integration.py +0 -0
  326. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_interpreter_callback.py +0 -0
  327. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_ir_encoder_properties.py +0 -0
  328. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_ir_golden_vectors.py +0 -0
  329. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_ir_roundtrip.py +0 -0
  330. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_lm15_native_calls.py +0 -0
  331. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_lm_configuration.py +0 -0
  332. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_lm_usage_shapes.py +0 -0
  333. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_mcp_tools.py +0 -0
  334. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_models.py +0 -0
  335. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_multi_client_stress_readiness.py +0 -0
  336. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_optimization_progress.py +0 -0
  337. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_optimization_start.py +0 -0
  338. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_optimize_budget.py +0 -0
  339. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_optimize_lift.py +0 -0
  340. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_optimize_lms.py +0 -0
  341. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_optimize_proposer.py +0 -0
  342. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_optimize_reflection_lm.py +0 -0
  343. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_optimize_terminal_failure.py +0 -0
  344. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_packaging.py +0 -0
  345. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_packaging_callable_source.py +0 -0
  346. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_packaging_mcp_tools.py +0 -0
  347. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_packaging_program_source.py +0 -0
  348. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_poll_until.py +0 -0
  349. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_program_identity.py +0 -0
  350. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_program_lineage_persist.py +0 -0
  351. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_programs_create.py +0 -0
  352. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_project_payload.py +0 -0
  353. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_real_provider_twins_readiness.py +0 -0
  354. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_reflector_properties.py +0 -0
  355. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_reflector_smoke.py +0 -0
  356. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_release_stress_script.py +0 -0
  357. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_repro_reactv2_bugs.py +0 -0
  358. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_require_version_bump.py +0 -0
  359. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_retry.py +0 -0
  360. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_reversible_failures.py +0 -0
  361. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_rlm_patch.py +0 -0
  362. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_run_attribution_paths.py +0 -0
  363. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_runtime_tools.py +0 -0
  364. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_schemas.py +0 -0
  365. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_seed_review_run.py +0 -0
  366. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_signature_inputs.py +0 -0
  367. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_span_references.py +0 -0
  368. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_span_sent_once.py +0 -0
  369. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_system_one.py +0 -0
  370. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_trace_context_concurrency.py +0 -0
  371. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_trace_fact_wire_contract.py +0 -0
  372. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_trace_facts.py +0 -0
  373. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_trace_render.py +0 -0
  374. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_traces_read.py +0 -0
  375. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_tracked_gepa.py +0 -0
  376. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_tracker_enrich.py +0 -0
  377. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_truncation.py +0 -0
  378. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_valsets_api.py +0 -0
  379. {cmpnd-0.45.0 → cmpnd-0.47.0}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.45.0
3
+ Version: 0.47.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,8 @@ Usage:
15
15
  import importlib.metadata
16
16
  from typing import TYPE_CHECKING
17
17
 
18
+ from cmpnd_ingest.client_floor import UpgradeRequiredWarning
19
+
18
20
  from . import authored, programs, traces
19
21
  from ._http import ApiError
20
22
  from .callback import CmpndCallback, trace_id
@@ -111,6 +113,8 @@ __all__ = [
111
113
  "program",
112
114
  "authored",
113
115
  "ApiError",
116
+ # Raised once when the server no longer supports this release
117
+ "UpgradeRequiredWarning",
114
118
  # Types
115
119
  "SpanType",
116
120
  "TraceStatus",
@@ -118,16 +118,14 @@ def _program_facts(
118
118
  ) -> dict[str, Any]:
119
119
  """Everything the deployment create call reads off the imported program."""
120
120
 
121
- from . import configuration, deployment, identity, mcp_servers, packaging
121
+ from . import _http, configuration, deployment, identity, mcp_servers, packaging
122
122
  from .authored import ProgramFacts
123
123
 
124
124
  references = deployment._mcp_tool_references(program)
125
125
  pinned: dict[tuple[str, str], Any] = {}
126
126
  if references:
127
- import httpx
128
-
129
127
  config = configuration.require_config()
130
- with httpx.Client(timeout=30) as client:
128
+ with _http.new_client(timeout=30) as client:
131
129
  pinned = deployment._resolve_mcp_tool_references(client, config, references)
132
130
  tool_references = [
133
131
  {
@@ -13,8 +13,10 @@ from __future__ import annotations
13
13
  import os
14
14
  import re
15
15
  from typing import Any
16
+ from urllib.parse import urlsplit
16
17
 
17
18
  import httpx
19
+ from cmpnd_ingest import client_floor
18
20
 
19
21
 
20
22
  class ApiError(Exception):
@@ -76,6 +78,48 @@ def server_detail(response: httpx.Response) -> str:
76
78
  return (response.text or "").strip()[:2000]
77
79
 
78
80
 
81
+ def new_client(**kwargs: Any) -> httpx.Client:
82
+ """An ``httpx.Client`` that warns when the server no longer supports this
83
+ release (:mod:`cmpnd_ingest.client_floor`).
84
+
85
+ Every client the SDK builds comes from here, so every answer the platform
86
+ sends gets the check. A caller's own ``event_hooks`` run after it.
87
+ """
88
+ hooks = client_floor.event_hooks()
89
+ for event, callers in (kwargs.pop("event_hooks", None) or {}).items():
90
+ hooks.setdefault(event, []).extend(callers)
91
+ return httpx.Client(event_hooks=hooks, **kwargs)
92
+
93
+
94
+ _DEFAULT_PORTS = {"http": 80, "https": 443}
95
+
96
+
97
+ def _origin(url: str) -> tuple[str, str, int] | None:
98
+ """A URL's (scheme, host, port), or None when it cannot be read as one."""
99
+ try:
100
+ parts = urlsplit(url)
101
+ port = parts.port
102
+ except ValueError:
103
+ return None
104
+ scheme = parts.scheme.lower()
105
+ if scheme not in _DEFAULT_PORTS or not parts.hostname:
106
+ return None
107
+ return scheme, parts.hostname.lower(), port if port is not None else _DEFAULT_PORTS[scheme]
108
+
109
+
110
+ def upload_auth_headers(upload_url: str, endpoint: str, api_key: str) -> dict[str, str]:
111
+ """The authentication header for a PUT to an ``upload_url`` serve advertised.
112
+
113
+ Serve authenticates its upload routes with the API key. The SDK sends the URL
114
+ as given, so the key goes only to a URL on the configured endpoint's origin:
115
+ the same scheme, host and port. Any other origin gets no key.
116
+ """
117
+ own = _origin(endpoint)
118
+ if not api_key or own is None or _origin(upload_url) != own:
119
+ return {}
120
+ return {"X-API-Key": api_key}
121
+
122
+
79
123
  class ApiClient:
80
124
  """Thin HTTP client for the cmpnd backend API."""
81
125
 
@@ -150,7 +194,7 @@ class ApiClient:
150
194
  url = f"{self._endpoint}{path}"
151
195
  timeout = self.configured_timeout()
152
196
  try:
153
- with httpx.Client(
197
+ with new_client(
154
198
  headers={"X-API-Key": self._api_key, "Content-Type": "application/json"},
155
199
  timeout=timeout,
156
200
  ) as client:
@@ -10,9 +10,7 @@ from __future__ import annotations
10
10
  import logging
11
11
  from typing import Any
12
12
 
13
- import httpx
14
-
15
- from . import configuration, identity, models
13
+ from . import _http, configuration, identity, models
16
14
 
17
15
  logger = logging.getLogger(__name__)
18
16
 
@@ -37,7 +35,7 @@ def check_dataset_exists(dataset_hash: str) -> bool:
37
35
  url = f"{config.endpoint}/api/v1/datasets/check/{dataset_hash}"
38
36
 
39
37
  try:
40
- with httpx.Client(timeout=5.0) as client:
38
+ with _http.new_client(timeout=5.0) as client:
41
39
  response = client.get(
42
40
  url,
43
41
  headers={"Authorization": f"Bearer {config.api_key}"},
@@ -8,9 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  from typing import Any
10
10
 
11
- import httpx
12
-
13
- from . import configuration
11
+ from . import _http, configuration
14
12
 
15
13
 
16
14
  class DatasetNotFoundError(Exception):
@@ -114,7 +112,7 @@ def _fetch_dataset(
114
112
  params["version"] = str(version)
115
113
 
116
114
  # Make request to backend
117
- with httpx.Client(timeout=30.0) as client:
115
+ with _http.new_client(timeout=30.0) as client:
118
116
  response = client.get(
119
117
  url,
120
118
  params=params,
@@ -2,9 +2,8 @@
2
2
 
3
3
  The deploy flow:
4
4
  1. Package the module into a source ZIP
5
- 2. POST to the server to create a deployment (returns a presigned upload slot)
6
- 3. PUT the ZIP to that slot (one path for both postures; bytes go direct to the
7
- worker, raw body, no form)
5
+ 2. POST to the server to create a deployment (returns an upload slot on serve)
6
+ 3. PUT the ZIP to that slot as the raw body
8
7
  4. Poll until the parser finishes (status becomes "ready" or "failed")
9
8
  5. Return the deployment_id
10
9
  """
@@ -17,7 +16,18 @@ from typing import Any
17
16
 
18
17
  import httpx
19
18
 
20
- from . import authored, configuration, deployment_metadata, execution, identity, mcp_servers, models, packaging, retry
19
+ from . import (
20
+ _http,
21
+ authored,
22
+ configuration,
23
+ deployment_metadata,
24
+ execution,
25
+ identity,
26
+ mcp_servers,
27
+ models,
28
+ packaging,
29
+ retry,
30
+ )
21
31
 
22
32
  logger = logging.getLogger(__name__)
23
33
 
@@ -427,7 +437,7 @@ def deploy(module, metric=None, timeout=300, metadata=None, *, program_id=None):
427
437
  references = _mcp_tool_references(module)
428
438
  pinned_mcp_tools: dict[tuple[str, str], Any] = {}
429
439
  if references:
430
- with httpx.Client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
440
+ with _http.new_client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
431
441
  pinned_mcp_tools = _resolve_mcp_tool_references(client, config, references)
432
442
 
433
443
  # Package the module
@@ -440,8 +450,8 @@ def deploy(module, metric=None, timeout=300, metadata=None, *, program_id=None):
440
450
 
441
451
  deploy_body: dict[str, Any] = {
442
452
  "program_signature_hash": identity.format_signature_hash(program_signature_hash),
443
- # Declare the ZIP's exact length so the server can size-bound the upload
444
- # slot and reject an oversized deploy before signing it (Slice E).
453
+ # Declare the ZIP's exact length so the server can reject an oversized
454
+ # deploy before the upload.
445
455
  "declared_content_length": len(zip_bytes),
446
456
  }
447
457
  # Left off when undeclared, so a serve from before program ids reads the body it
@@ -539,7 +549,7 @@ def _deploy_prepared(
539
549
  """Create, upload, and poll one fully prepared source bundle."""
540
550
  headers = {"X-API-Key": config.api_key, "Content-Type": "application/json"}
541
551
 
542
- with httpx.Client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
552
+ with _http.new_client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
543
553
  created = _send_retrying(
544
554
  lambda: client.post(
545
555
  f"{config.endpoint}/api/v1/deployments",
@@ -568,12 +578,13 @@ def _deploy_prepared(
568
578
  )
569
579
  for warning in answer.get("warnings") or []:
570
580
  logger.warning("deploy: %s", warning)
581
+ upload_url = answer["upload_url"]
582
+ upload_headers = {
583
+ "Content-Type": "application/zip",
584
+ **_http.upload_auth_headers(upload_url, config.endpoint, config.api_key),
585
+ }
571
586
  uploaded = _send_retrying(
572
- lambda: client.put(
573
- answer["upload_url"],
574
- content=zip_bytes,
575
- headers={"Content-Type": "application/zip"},
576
- ),
587
+ lambda: client.put(upload_url, content=zip_bytes, headers=upload_headers),
577
588
  "upload deployment ZIP",
578
589
  may_resend_with_no_answer=lambda _: True,
579
590
  may_resend_after_answer=_repeatable_answer,
@@ -327,7 +327,7 @@ class _RunClient:
327
327
  self.retry_after = 0.0
328
328
 
329
329
  def __enter__(self) -> _RunClient:
330
- self.client = httpx.Client(headers={"X-API-Key": self.api_key})
330
+ self.client = _http.new_client(headers={"X-API-Key": self.api_key})
331
331
  return self
332
332
 
333
333
  def __exit__(self, *_: object) -> None:
@@ -669,15 +669,14 @@ def _registered_tools(url: str, timeout_s: float) -> list[PinnedTool] | None | o
669
669
  the registry route, so notebook use keeps its existing direct connection.
670
670
  ``None`` means the configured registry answered and does not know this server.
671
671
  """
672
- from . import configuration
672
+ from . import _http, configuration
673
673
 
674
674
  config = configuration.get_config()
675
675
  if config is None:
676
676
  return _NO_PLATFORM_REGISTRY
677
- import httpx
678
677
 
679
678
  headers = {"X-API-Key": config.api_key}
680
- with httpx.Client(timeout=timeout_s) as client:
679
+ with _http.new_client(timeout=timeout_s) as client:
681
680
  listing = client.get(f"{config.endpoint}/api/v1/org/tool-servers", headers=headers)
682
681
  if listing.status_code == 404:
683
682
  return _NO_PLATFORM_REGISTRY
@@ -11,8 +11,8 @@ Flow per :func:`optimize` call:
11
11
  1. Build the optimization bundle (``trainset.ndjson`` + ``valset.ndjson`` +
12
12
  ``metric.py``) and, if no ``deployment_id`` was given, :func:`deploy` the
13
13
  program to materialize the starting deployment A.
14
- 2. Step 0 — ``data:"upload"``: POST to ``/optimize/{optimizer}`` to get a
15
- presigned upload, PUT the bundle, then poll until the step completes. The
14
+ 2. Step 0 — ``data:"upload"``: POST to ``/optimize/{optimizer}`` to get an
15
+ upload slot, PUT the bundle, then poll until the step completes. The
16
16
  result becomes a first-class child deployment B.
17
17
  3. Steps 1..n — ``data:"reuse"`` on the latest result deployment, advancing the
18
18
  seed each step (the key quality lever). Track the best score; stop after
@@ -1008,7 +1008,7 @@ def _run_steps(
1008
1008
  current_deployment_id = deployment_id
1009
1009
  non_improving = 0
1010
1010
 
1011
- with httpx.Client(timeout=_HTTP_TIMEOUT_SECONDS) as client:
1011
+ with _http.new_client(timeout=_HTTP_TIMEOUT_SECONDS) as client:
1012
1012
  for step in range(max_steps):
1013
1013
  data = "upload" if step == 0 else "reuse"
1014
1014
  step_config = dict(config)
@@ -1031,7 +1031,7 @@ def _run_steps(
1031
1031
  optimization_id = initiated["optimization_id"]
1032
1032
 
1033
1033
  if data == "upload":
1034
- _upload_bundle(client, initiated, bundle)
1034
+ _upload_bundle(client, initiated, bundle, endpoint=cfg.endpoint, api_key=cfg.api_key)
1035
1035
 
1036
1036
  status = _poll_until_terminal(
1037
1037
  client,
@@ -1243,7 +1243,7 @@ def _refuse_conflicting_members(
1243
1243
  headers = {"X-API-Key": cfg.api_key}
1244
1244
  conflicting: list[str] = []
1245
1245
  try:
1246
- with httpx.Client(timeout=_HTTP_TIMEOUT_SECONDS) as client:
1246
+ with _http.new_client(timeout=_HTTP_TIMEOUT_SECONDS) as client:
1247
1247
  listing = client.get(f"{cfg.endpoint}/api/v1/deployments/{deployment_id}/source", headers=headers)
1248
1248
  if not listing.is_success:
1249
1249
  return
@@ -1414,53 +1414,26 @@ def _initiate_step(
1414
1414
  )
1415
1415
 
1416
1416
 
1417
- def _upload_bundle(client: httpx.Client, initiated: dict[str, Any], bundle: bytes) -> None:
1418
- """Upload the optimization bundle to the slot the initiate step returned.
1419
-
1420
- Two postures, one path — keyed on ``upload_fields`` exactly as the deploy
1421
- upload is:
1422
-
1423
- - **Presigned POST** (the deploy Lambda stack): ``upload_fields`` is a
1424
- non-empty form the destination key is pinned by; the bundle rides as the
1425
- ``file`` part of a multipart POST.
1426
- - **PUT** (the on-prem worker-sandbox): no form fields — the URL *is* the
1427
- slot. The bundle is the raw PUT body.
1417
+ def _upload_bundle(
1418
+ client: httpx.Client, initiated: dict[str, Any], bundle: bytes, *, endpoint: str, api_key: str
1419
+ ) -> None:
1420
+ """PUT the optimization bundle, raw, to the slot the initiate step returned.
1428
1421
 
1429
- serve normalizes the awaiting_data response so both keys are always present
1430
- (``upload_method`` defaulting to PUT, ``upload_fields`` to ``{}``), so the
1431
- branch here is just "are there presigned fields to post."
1422
+ Serve answers an ``awaiting_data`` initiate with an ``upload_url`` on its own
1423
+ bundle route, and authenticates that route with the API key. The key rides
1424
+ the PUT only when ``upload_url`` is on ``endpoint``'s origin
1425
+ (``_http.upload_auth_headers``).
1432
1426
 
1433
- Both postures write to a slot the destination key is already pinned to, so
1434
- re-sending the same bytes overwrites the same object — this call is
1435
- idempotent by construction, and rides through a transient answer *and* a
1436
- transport blip alike.
1427
+ The slot pins its destination, so re-sending the same bytes writes the same
1428
+ object. That makes this call idempotent, so it rides through a transient answer
1429
+ and a transport blip alike.
1437
1430
  """
1438
1431
  upload_url = initiated.get("upload_url")
1439
1432
  if not upload_url:
1440
1433
  raise OptimizeError("upload step did not return an upload_url")
1441
- upload_fields = initiated.get("upload_fields")
1442
- method = (initiated.get("upload_method") or ("POST" if upload_fields else "PUT")).upper()
1443
1434
  what = "upload optimization bundle"
1444
-
1445
- if upload_fields:
1446
- resp = _request(
1447
- lambda: client.post(
1448
- upload_url,
1449
- data=dict(upload_fields),
1450
- files={"file": ("data.zip", bundle, "application/zip")},
1451
- ),
1452
- what,
1453
- )
1454
- else:
1455
- resp = _request(
1456
- lambda: client.request(
1457
- method,
1458
- upload_url,
1459
- content=bundle,
1460
- headers={"Content-Type": "application/zip"},
1461
- ),
1462
- what,
1463
- )
1435
+ headers = {"Content-Type": "application/zip", **_http.upload_auth_headers(upload_url, endpoint, api_key)}
1436
+ resp = _request(lambda: client.put(upload_url, content=bundle, headers=headers), what)
1464
1437
  if not resp.is_success:
1465
1438
  body = (resp.text or "").strip()[:400]
1466
1439
  raise OptimizeError(f"failed to {what}: HTTP {resp.status_code}: {body}")
@@ -1428,7 +1428,7 @@ def generate_source_zip(
1428
1428
  ) -> bytes:
1429
1429
  """Build a ZIP archive with program source, optional metric, and manifest.
1430
1430
 
1431
- The ZIP is suitable for upload to the deploy API via presigned S3 POST.
1431
+ The ZIP is the body the deploy API's source upload takes.
1432
1432
  """
1433
1433
  import dspy
1434
1434
 
@@ -32,6 +32,16 @@ retries, because a second send would create a second program.
32
32
  `cmpnd.programs.create(name=...)` makes the same call from Python and returns
33
33
  the row.
34
34
 
35
+ ## When the server no longer supports your release
36
+
37
+ Every answer from the platform names the oldest `cmpnd` release it supports.
38
+ When yours is older, the CLI and the SDK warn once per process and name the
39
+ upgrade, `pip install -U cmpnd`. The server still answers, so an older release
40
+ keeps working wherever nothing it relies on has changed. To stay on an older
41
+ release without the warning, set `CMPND_NO_UPGRADE_WARNING=1`. In code,
42
+ `warnings.filterwarnings("ignore", category=cmpnd.UpgradeRequiredWarning)`
43
+ does the same.
44
+
35
45
  ## Deploy flow (Docker-style)
36
46
 
37
47
  These commands print human-readable output by default. Each takes `--json` for
@@ -306,6 +306,10 @@ Two callers implement the waiting:
306
306
  - **`exporter_http._send_json_retrying`** waits on the shutdown event rather than sleeping, so a backoff can't hold the process open at exit (and the post-shutdown drain gets no retries at all, deliberately).
307
307
  - **`optimization._request`** clips every wait by the caller's `timeout` deadline, which stays the outer bound of `optimize()`. See [the optimize loop](#the-optimize-loop-and-transient-tolerance) below.
308
308
 
309
+ ### The server says when this release is too old
310
+
311
+ Every API answer names the oldest cmpnd release the server supports, in the `X-Cmpnd-Minimum-Client-Version` header. `cmpnd_ingest/client_floor.py` reads it in an httpx response hook. When the installed `cmpnd` is older, it raises an `UpgradeRequiredWarning` once per process that names both versions and the command `pip install -U cmpnd`. The server refuses nothing for a client's version, so a call below the floor still goes through, and the warning appears beside whatever it does. Every client the SDK builds comes from `cmpnd._http.new_client`, which installs the hook, and the exporter installs it in `_client_kwargs`. `tests/test_client_floor.py` fails on an `httpx.Client` built any other way in `cmpnd/`. An install of `cmpnd_ingest` without `cmpnd` has no version to compare and says nothing. `CMPND_NO_UPGRADE_WARNING=1` silences the warning for a user who stays on an older release on purpose, and the warning names that switch. `cmpnd.UpgradeRequiredWarning` is public, so a `warnings` filter silences it in code.
312
+
309
313
  ### The optimize loop and transient tolerance
310
314
 
311
315
  `optimization._poll_until_terminal` is what a caller blocks on for the length of a run, so one transient answer or one reset connection must not destroy a healthy run. It rides through transient answers on the shared budget, resetting after each successful poll, and never extends past the caller's `timeout` (default 600s per step) — a backoff wait is clipped by the deadline rather than overshooting it.
@@ -354,6 +358,11 @@ done.
354
358
  | `PUT upload_url` (the ZIP) | retried — the slot pins the key and the bytes are fixed, so a re-send overwrites the same object | retried, for the same reason |
355
359
  | `GET /api/v1/deployments/{id}` (poll) | retried on the next tick — a read is repeatable | retried on the next tick |
356
360
 
361
+ Serve authenticates its upload routes with the API key. The deploy upload and
362
+ the optimization bundle upload send `X-API-Key` when `upload_url` has the
363
+ configured endpoint's scheme, host and port (`_http.upload_auth_headers`). A URL
364
+ on any other origin gets no key.
365
+
357
366
  After the poll reports ready, the SDK reads
358
367
  `GET /api/v1/deployments/{id}/signature`. This bounded read returns persisted
359
368
  artifact metadata and never asks a worker or analytics store. A failed metadata
@@ -0,0 +1,101 @@
1
+ """Tell the user when the server no longer supports this cmpnd release.
2
+
3
+ The server names the oldest cmpnd release it supports on every API answer, in
4
+ the ``X-Cmpnd-Minimum-Client-Version`` header. A client older than that may
5
+ still work, and the server refuses nothing for it, but the server promises
6
+ nothing either. So the first answer that names a floor above this client's
7
+ version raises an :class:`UpgradeRequiredWarning` that says how to upgrade. It
8
+ appears once per process, beside whatever the call then does.
9
+
10
+ A user who stays on an older release on purpose silences the warning with
11
+ ``CMPND_NO_UPGRADE_WARNING=1`` in the environment, or in code with
12
+ ``warnings.filterwarnings("ignore", category=cmpnd.UpgradeRequiredWarning)``.
13
+
14
+ The check runs as an httpx response hook (:func:`event_hooks`) on every client
15
+ the SDK builds, so no call site has to remember it. It compares against the
16
+ installed ``cmpnd`` distribution and does nothing when ``cmpnd`` is not
17
+ installed, because the floor is about ``cmpnd``.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import importlib.metadata
23
+ import os
24
+ import re
25
+ import threading
26
+ import warnings
27
+ from collections.abc import Callable
28
+
29
+ import httpx
30
+
31
+ HEADER = "X-Cmpnd-Minimum-Client-Version"
32
+
33
+ # Set to "1" to silence the warning, matching the SDK's other switches.
34
+ SILENCE_ENV = "CMPND_NO_UPGRADE_WARNING"
35
+
36
+ _RELEASE = re.compile(r"(\d+)\.(\d+)\.(\d+)")
37
+
38
+
39
+ class UpgradeRequiredWarning(UserWarning):
40
+ """The server supports no cmpnd release older than the one it names."""
41
+
42
+
43
+ _warned: set[str] = set()
44
+ _lock = threading.Lock()
45
+
46
+
47
+ def _reset_lock_after_fork() -> None:
48
+ """Give a forked child a lock of its own.
49
+
50
+ A fork copies ``_lock`` as it stood, and a thread that held it does not exist
51
+ in the child, so the child's first check would block forever. The exporter
52
+ resets its own locks after a fork for the same reason.
53
+ """
54
+ global _lock
55
+ _lock = threading.Lock()
56
+
57
+
58
+ if hasattr(os, "register_at_fork"):
59
+ os.register_at_fork(after_in_child=_reset_lock_after_fork)
60
+
61
+
62
+ def _installed_version() -> str | None:
63
+ try:
64
+ return importlib.metadata.version("cmpnd")
65
+ except importlib.metadata.PackageNotFoundError:
66
+ return None
67
+
68
+
69
+ def _release(version: str) -> tuple[int, int, int] | None:
70
+ match = _RELEASE.match(version.strip())
71
+ if match is None:
72
+ return None
73
+ major, minor, patch = match.groups()
74
+ return int(major), int(minor), int(patch)
75
+
76
+
77
+ def check_client_floor(response: httpx.Response) -> None:
78
+ """Warn once when the answer names a floor above this client's version."""
79
+ floor = response.headers.get(HEADER)
80
+ if not floor or os.environ.get(SILENCE_ENV) == "1":
81
+ return
82
+ mine = _installed_version()
83
+ floor_release = _release(floor)
84
+ mine_release = _release(mine) if mine else None
85
+ if floor_release is None or mine_release is None or mine_release >= floor_release:
86
+ return
87
+ with _lock:
88
+ if floor in _warned:
89
+ return
90
+ _warned.add(floor)
91
+ warnings.warn(
92
+ f"cmpnd {mine} is older than the oldest release this server supports ({floor.strip()}). "
93
+ f"Upgrade with: pip install -U cmpnd (or set {SILENCE_ENV}=1 to stay on this release quietly)",
94
+ UpgradeRequiredWarning,
95
+ stacklevel=2,
96
+ )
97
+
98
+
99
+ def event_hooks() -> dict[str, list[Callable[[httpx.Response], None]]]:
100
+ """The ``event_hooks`` argument that installs the check on an httpx client."""
101
+ return {"response": [check_client_floor]}
@@ -34,7 +34,7 @@ from typing import Any, cast
34
34
 
35
35
  import httpx
36
36
 
37
- from . import program_names, request_log, retry
37
+ from . import client_floor, program_names, request_log, retry
38
38
  from .bodies import EncodedTraceBody, TraceBody
39
39
  from .protocols import (
40
40
  EvalRunRecord,
@@ -1029,8 +1029,8 @@ class BatchExporter:
1029
1029
 
1030
1030
  def _client_kwargs(self) -> dict[str, Any]:
1031
1031
  """Shared httpx.Client construction args for the background and sync
1032
- clients, so both carry the same base URL, auth header, and (in tests)
1033
- injected transport."""
1032
+ clients, so both carry the same base URL, auth header, client-floor
1033
+ check (``client_floor``) and, in tests, injected transport."""
1034
1034
  kwargs: dict[str, Any] = {
1035
1035
  "base_url": self._config.endpoint,
1036
1036
  "headers": {
@@ -1038,6 +1038,7 @@ class BatchExporter:
1038
1038
  "Content-Type": "application/json",
1039
1039
  },
1040
1040
  "timeout": self._http_timeout,
1041
+ "event_hooks": client_floor.event_hooks(),
1041
1042
  }
1042
1043
  if self._transport:
1043
1044
  kwargs["transport"] = self._transport
@@ -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.45.0"
8
+ version = "0.47.0"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"
@@ -212,11 +212,8 @@ class TestDeployBadImportRejected:
212
212
  headers = {"X-API-Key": config.api_key, "Content-Type": "application/json"}
213
213
 
214
214
  with httpx.Client(timeout=30.0) as client:
215
- # Step 1: create deployment. declared_content_length is required
216
- # (Slice E): the hosted presigned PUT is signed with the exact ZIP
217
- # length, so it must be declared up front — exactly as cmpnd/deploy.py
218
- # does, and a deployment names its program. This hand-crafted client
219
- # mirrors that contract.
215
+ # Step 1: create the deployment, declaring the ZIP's length and
216
+ # naming its program as cmpnd.deploy does.
220
217
  create_resp = client.post(
221
218
  f"{config.endpoint}/api/v1/deployments",
222
219
  json={
@@ -232,11 +229,12 @@ class TestDeployBadImportRejected:
232
229
  deployment_id = data["deployment_id"]
233
230
  upload_url = data["upload_url"]
234
231
 
235
- # Step 2: PUT the bad ZIP to the single upload slot (Slice E).
232
+ # Step 2: PUT the bad ZIP to the upload slot, which serve
233
+ # authenticates with the API key.
236
234
  upload_resp = client.put(
237
235
  upload_url,
238
236
  content=zip_bytes,
239
- headers={"Content-Type": "application/zip"},
237
+ headers={"X-API-Key": config.api_key, "Content-Type": "application/zip"},
240
238
  )
241
239
  upload_resp.raise_for_status()
242
240
 
@@ -106,18 +106,13 @@ def _initiate_and_abandon(deployment_id: str, trainset, valset, metric) -> str:
106
106
  resp.raise_for_status()
107
107
  initiated = resp.json()
108
108
 
109
- # serve normalizes the awaiting_data slot so upload_fields is always
110
- # present: a non-empty form means presigned POST, empty means raw PUT.
111
- upload_url = initiated["upload_url"]
112
- upload_fields = initiated.get("upload_fields") or {}
113
- if upload_fields:
114
- up = client.post(
115
- upload_url,
116
- data=dict(upload_fields),
117
- files={"file": ("data.zip", bundle, "application/zip")},
118
- )
119
- else:
120
- up = client.put(upload_url, content=bundle, headers={"Content-Type": "application/zip"})
109
+ # The slot is serve's bundle route, which takes the raw bundle as a PUT
110
+ # and authenticates it with the API key.
111
+ up = client.put(
112
+ initiated["upload_url"],
113
+ content=bundle,
114
+ headers={"X-API-Key": cfg.api_key, "Content-Type": "application/zip"},
115
+ )
121
116
  up.raise_for_status()
122
117
 
123
118
  # Deliberately NO status poll — the server-owned watcher must finish it.