cmpnd 0.45.0__tar.gz → 0.46.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.46.0}/PKG-INFO +1 -1
  2. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/__init__.py +4 -0
  3. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/_authored_import.py +2 -4
  4. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/_http.py +45 -1
  5. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/dataset_sync.py +2 -4
  6. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/datasets.py +2 -4
  7. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/deployment.py +20 -8
  8. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/deployment_runs.py +1 -1
  9. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/mcp_servers.py +2 -3
  10. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/optimization.py +18 -21
  11. {cmpnd-0.45.0 → cmpnd-0.46.0}/docs/cli.md +10 -0
  12. {cmpnd-0.45.0 → cmpnd-0.46.0}/docs/sdk-instrumentation.md +9 -0
  13. cmpnd-0.46.0/ingest/cmpnd_ingest/client_floor.py +101 -0
  14. {cmpnd-0.45.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/exporter_http.py +4 -3
  15. {cmpnd-0.45.0 → cmpnd-0.46.0}/pyproject.toml +1 -1
  16. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/integration/conftest.py +13 -12
  17. cmpnd-0.46.0/tests/test_client_floor.py +181 -0
  18. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_dataset_sync.py +5 -5
  19. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_deploy.py +48 -4
  20. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_optimize.py +93 -5
  21. cmpnd-0.46.0/tests/test_upload_auth_headers.py +59 -0
  22. {cmpnd-0.45.0 → cmpnd-0.46.0}/uv.lock +1 -1
  23. {cmpnd-0.45.0 → cmpnd-0.46.0}/.gitignore +0 -0
  24. {cmpnd-0.45.0 → cmpnd-0.46.0}/AGENTS.md +0 -0
  25. {cmpnd-0.45.0 → cmpnd-0.46.0}/CLAUDE.md +0 -0
  26. {cmpnd-0.45.0 → cmpnd-0.46.0}/README.md +0 -0
  27. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/_assessments.py +0 -0
  28. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/_deployments.py +0 -0
  29. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/_exporter_pool.py +0 -0
  30. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/_gepa_patch.py +0 -0
  31. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/_program_patch.py +0 -0
  32. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/_rlm_patch.py +0 -0
  33. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/_runtime_tools.py +0 -0
  34. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/_timespec.py +0 -0
  35. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/_traces.py +0 -0
  36. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/_valsets.py +0 -0
  37. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/authored.py +0 -0
  38. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/callback.py +0 -0
  39. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/__init__.py +0 -0
  40. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/api_client.py +0 -0
  41. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/__init__.py +0 -0
  42. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/admin.py +0 -0
  43. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/assessments.py +0 -0
  44. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/auth.py +0 -0
  45. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/datasets.py +0 -0
  46. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/deployments.py +0 -0
  47. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/evals.py +0 -0
  48. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/login.py +0 -0
  49. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/optimizations.py +0 -0
  50. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/org.py +0 -0
  51. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/programs.py +0 -0
  52. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  53. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/signatures.py +0 -0
  54. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  55. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/top.py +0 -0
  56. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/traces.py +0 -0
  57. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/commands/valsets.py +0 -0
  58. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/credentials.py +0 -0
  59. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/deprecation.py +0 -0
  60. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/pagination.py +0 -0
  61. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/shell.py +0 -0
  62. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/trace_render.py +0 -0
  63. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/tree_help.py +0 -0
  64. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/cli/verbs.py +0 -0
  65. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/client.py +0 -0
  66. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/configuration.py +0 -0
  67. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/context.py +0 -0
  68. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/decorators.py +0 -0
  69. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/deployment_metadata.py +0 -0
  70. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/eval_handler.py +0 -0
  71. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/execution.py +0 -0
  72. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/exporter.py +0 -0
  73. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/exporter_http.py +0 -0
  74. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/helpers.py +0 -0
  75. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/identity.py +0 -0
  76. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/imaging.py +0 -0
  77. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/ir/__init__.py +0 -0
  78. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/ir/decode.py +0 -0
  79. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/ir/encode.py +0 -0
  80. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/ir/hash.py +0 -0
  81. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/ir/program.py +0 -0
  82. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/ir/reflection.py +0 -0
  83. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/ir/types.py +0 -0
  84. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/ir/xxh64.py +0 -0
  85. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/lm_configuration.py +0 -0
  86. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/models.py +0 -0
  87. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/named_programs.py +0 -0
  88. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/optimizers/__init__.py +0 -0
  89. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/optimizers/gepa.py +0 -0
  90. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  91. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/optimizers/resume.py +0 -0
  92. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/optimizers/tracker.py +0 -0
  93. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/packaging.py +0 -0
  94. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/programs.py +0 -0
  95. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/references.py +0 -0
  96. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/retry.py +0 -0
  97. {cmpnd-0.45.0 → cmpnd-0.46.0}/cmpnd/traces.py +0 -0
  98. {cmpnd-0.45.0 → cmpnd-0.46.0}/docs/2026-04-02-code-review.md +0 -0
  99. {cmpnd-0.45.0 → cmpnd-0.46.0}/docs/client.md +0 -0
  100. {cmpnd-0.45.0 → cmpnd-0.46.0}/docs/custom-proposer.md +0 -0
  101. {cmpnd-0.45.0 → cmpnd-0.46.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  102. {cmpnd-0.45.0 → cmpnd-0.46.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  103. {cmpnd-0.45.0 → cmpnd-0.46.0}/docs/reading-traces.md +0 -0
  104. {cmpnd-0.45.0 → cmpnd-0.46.0}/examples/generalizing_proposer.py +0 -0
  105. {cmpnd-0.45.0 → cmpnd-0.46.0}/examples/local_ollama.py +0 -0
  106. {cmpnd-0.45.0 → cmpnd-0.46.0}/ingest/LINEAGE.md +0 -0
  107. {cmpnd-0.45.0 → cmpnd-0.46.0}/ingest/README.md +0 -0
  108. {cmpnd-0.45.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  109. {cmpnd-0.45.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  110. {cmpnd-0.45.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/program_names.py +0 -0
  111. {cmpnd-0.45.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  112. {cmpnd-0.45.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/py.typed +0 -0
  113. {cmpnd-0.45.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  114. {cmpnd-0.45.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/retry.py +0 -0
  115. {cmpnd-0.45.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  116. {cmpnd-0.45.0 → cmpnd-0.46.0}/ingest/pyproject.toml +0 -0
  117. {cmpnd-0.45.0 → cmpnd-0.46.0}/scripts/audit-sdist.sh +0 -0
  118. {cmpnd-0.45.0 → cmpnd-0.46.0}/scripts/capture_review_run.py +0 -0
  119. {cmpnd-0.45.0 → cmpnd-0.46.0}/scripts/check-pip-install.sh +0 -0
  120. {cmpnd-0.45.0 → cmpnd-0.46.0}/scripts/check_release_artifacts.py +0 -0
  121. {cmpnd-0.45.0 → cmpnd-0.46.0}/scripts/fixtures/review_run.json.gz +0 -0
  122. {cmpnd-0.45.0 → cmpnd-0.46.0}/scripts/gepa_resume_bug.py +0 -0
  123. {cmpnd-0.45.0 → cmpnd-0.46.0}/scripts/release-stress.sh +0 -0
  124. {cmpnd-0.45.0 → cmpnd-0.46.0}/scripts/require_version_bump.py +0 -0
  125. {cmpnd-0.45.0 → cmpnd-0.46.0}/scripts/seed_review_run.py +0 -0
  126. {cmpnd-0.45.0 → cmpnd-0.46.0}/scripts/test-in-linux.sh +0 -0
  127. {cmpnd-0.45.0 → cmpnd-0.46.0}/stubs/dspy/__init__.pyi +0 -0
  128. {cmpnd-0.45.0 → cmpnd-0.46.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  129. {cmpnd-0.45.0 → cmpnd-0.46.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  130. {cmpnd-0.45.0 → cmpnd-0.46.0}/stubs/dspy/utils/__init__.pyi +0 -0
  131. {cmpnd-0.45.0 → cmpnd-0.46.0}/stubs/dspy/utils/callback.pyi +0 -0
  132. {cmpnd-0.45.0 → cmpnd-0.46.0}/stubs/dspy/utils/dummies.pyi +0 -0
  133. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/__init__.py +0 -0
  134. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/_deployment_signature_helpers.py +0 -0
  135. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/_exporter_helpers.py +0 -0
  136. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/_fake_clock.py +0 -0
  137. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/_identity_helpers.py +0 -0
  138. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/_resolve_refs.py +0 -0
  139. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/_traced_run.py +0 -0
  140. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/conftest.py +0 -0
  141. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/__init__.py +0 -0
  142. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/committee_task.py +0 -0
  143. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/conftest.py +0 -0
  144. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  145. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  146. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/fake_lm.py +0 -0
  147. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/helpers.py +0 -0
  148. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/target_capabilities.py +0 -0
  149. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_assessments_cli.py +0 -0
  150. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_capture_prompts.py +0 -0
  151. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_composite_module.py +0 -0
  152. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  153. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_deploy_execute.py +0 -0
  154. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_deploy_pandas.py +0 -0
  155. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_deploy_programs.py +0 -0
  156. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_edge_cases.py +0 -0
  157. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_example_hash.py +0 -0
  158. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_gepa_optimization.py +0 -0
  159. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_interpreter_spans.py +0 -0
  160. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_lm_accuracy.py +0 -0
  161. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_module_tracing.py +0 -0
  162. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_optimization_lineage.py +0 -0
  163. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  164. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_optimize_failures.py +0 -0
  165. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_optimize_loop.py +0 -0
  166. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  167. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  168. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_parallel_export.py +0 -0
  169. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_project_attribution.py +0 -0
  170. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_react_tool_spans.py +0 -0
  171. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_save_load.py +0 -0
  172. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_trace_structure.py +0 -0
  173. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_traces_read.py +0 -0
  174. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_user_attribution.py +0 -0
  175. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/e2e/test_valsets_cli.py +0 -0
  176. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  177. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  178. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  179. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  180. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  181. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  182. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  183. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  184. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  185. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/10_react.json +0 -0
  186. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/11_refine.json +0 -0
  187. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/12_retrieve.json +0 -0
  188. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/13_knn.json +0 -0
  189. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/14_parallel.json +0 -0
  190. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/identity_vectors/README.md +0 -0
  191. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/ingest_conformance.py +0 -0
  192. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/integration/__init__.py +0 -0
  193. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/integration/authored_optimize/judge.py +0 -0
  194. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  195. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  196. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/integration/hosted_optimize_probe.py +0 -0
  197. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/integration/optimization_run_probe.py +0 -0
  198. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  199. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/integration/test_logs_renders_traces.py +0 -0
  200. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/integration/test_optimization_run.py +0 -0
  201. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/integration/test_optimizing_progress.py +0 -0
  202. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/integration/test_unhappy_path.py +0 -0
  203. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/load/__init__.py +0 -0
  204. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/load/__main__.py +0 -0
  205. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/load/drain.py +0 -0
  206. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/load/ingest.py +0 -0
  207. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/load/latency_transport.py +0 -0
  208. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/load/rlm_shape.py +0 -0
  209. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/load/rlm_size_report.py +0 -0
  210. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/load/sweep.py +0 -0
  211. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/load/test_rlm_shape.py +0 -0
  212. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/load/test_rlm_size_report.py +0 -0
  213. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/__init__.py +0 -0
  214. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/conftest.py +0 -0
  215. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/seed_parity.py +0 -0
  216. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/test_auth.py +0 -0
  217. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/test_datasets.py +0 -0
  218. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/test_deployments.py +0 -0
  219. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/test_evals.py +0 -0
  220. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/test_health.py +0 -0
  221. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/test_modules.py +0 -0
  222. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/test_optimizations.py +0 -0
  223. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/test_programs.py +0 -0
  224. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/test_signatures.py +0 -0
  225. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/test_trace_streaming.py +0 -0
  226. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/parity/test_traces.py +0 -0
  227. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/span_reference_fixture.py +0 -0
  228. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/__init__.py +0 -0
  229. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/_provider.py +0 -0
  230. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/_readiness.py +0 -0
  231. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/_reliability.py +0 -0
  232. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/_result.py +0 -0
  233. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/mint_bedrock_token.py +0 -0
  234. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_deploy_programs.py +0 -0
  235. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_multi_client_stress.py +0 -0
  236. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_optimize_client_committee.py +0 -0
  237. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_optimize_loop.py +0 -0
  238. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  239. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_optimize_stress.py +0 -0
  240. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_provider_roles.py +0 -0
  241. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_readiness.py +0 -0
  242. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_real_provider_twins.py +0 -0
  243. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_reliability.py +0 -0
  244. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_reliability_classify.py +0 -0
  245. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_rlm_wasm.py +0 -0
  246. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/stress/test_trace_search.py +0 -0
  247. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_adapter_reentrancy.py +0 -0
  248. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_assessments_api.py +0 -0
  249. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_authored.py +0 -0
  250. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_authored_bundle_collisions.py +0 -0
  251. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_authored_metric_contract.py +0 -0
  252. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_authored_named_models.py +0 -0
  253. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_authored_optimize.py +0 -0
  254. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_callback.py +0 -0
  255. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_candidate_lineage.py +0 -0
  256. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/__init__.py +0 -0
  257. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/conftest.py +0 -0
  258. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_admin.py +0 -0
  259. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_api_client.py +0 -0
  260. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_assessments.py +0 -0
  261. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_auth.py +0 -0
  262. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_credentials.py +0 -0
  263. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_datasets.py +0 -0
  264. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_deployment_runs.py +0 -0
  265. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_deployments.py +0 -0
  266. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_evals.py +0 -0
  267. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_login.py +0 -0
  268. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_main.py +0 -0
  269. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_optimizations.py +0 -0
  270. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_org.py +0 -0
  271. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_pagination.py +0 -0
  272. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_parity.py +0 -0
  273. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_programs.py +0 -0
  274. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_provider_credentials.py +0 -0
  275. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_schema_sample.py +0 -0
  276. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_shell.py +0 -0
  277. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_signatures.py +0 -0
  278. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_tool_servers.py +0 -0
  279. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_top.py +0 -0
  280. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_traces.py +0 -0
  281. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  282. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_tree_help.py +0 -0
  283. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cli/test_valsets.py +0 -0
  284. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_client.py +0 -0
  285. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_client_program_names.py +0 -0
  286. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_clustering/__init__.py +0 -0
  287. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_clustering/conftest.py +0 -0
  288. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_clustering/test_edge_cases.py +0 -0
  289. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  290. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_clustering/test_type_conversion.py +0 -0
  291. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_cmp551_grouping_identity.py +0 -0
  292. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_configuration.py +0 -0
  293. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_context.py +0 -0
  294. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_context_propagation.py +0 -0
  295. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_dataset_polling.py +0 -0
  296. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_datasets.py +0 -0
  297. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_decision_client_spans.py +0 -0
  298. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_decorators.py +0 -0
  299. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  300. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_deploy_program_id.py +0 -0
  301. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_deployment_metadata.py +0 -0
  302. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_deployment_runs.py +0 -0
  303. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_deployments_read.py +0 -0
  304. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_eval.py +0 -0
  305. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_eval_example_attribution.py +0 -0
  306. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_eval_start.py +0 -0
  307. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_eval_trace_grouping.py +0 -0
  308. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_exception_paths.py +0 -0
  309. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_execute.py +0 -0
  310. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_execute_run_identity.py +0 -0
  311. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_exporter.py +0 -0
  312. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_exporter_program_names.py +0 -0
  313. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_exporter_queue.py +0 -0
  314. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_exporter_real.py +0 -0
  315. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_fake_lm.py +0 -0
  316. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_gepa_callback.py +0 -0
  317. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_gepa_integration.py +0 -0
  318. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_gepa_output_normalization.py +0 -0
  319. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_gepa_resume.py +0 -0
  320. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_identity.py +0 -0
  321. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_imaging.py +0 -0
  322. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_import_weight.py +0 -0
  323. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_ingest_conformance.py +0 -0
  324. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_ingest_second_producer.py +0 -0
  325. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_integration.py +0 -0
  326. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_interpreter_callback.py +0 -0
  327. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_ir_encoder_properties.py +0 -0
  328. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_ir_golden_vectors.py +0 -0
  329. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_ir_roundtrip.py +0 -0
  330. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_lm15_native_calls.py +0 -0
  331. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_lm_configuration.py +0 -0
  332. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_lm_usage_shapes.py +0 -0
  333. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_mcp_tools.py +0 -0
  334. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_models.py +0 -0
  335. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_multi_client_stress_readiness.py +0 -0
  336. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_optimization_progress.py +0 -0
  337. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_optimization_start.py +0 -0
  338. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_optimize_budget.py +0 -0
  339. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_optimize_lift.py +0 -0
  340. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_optimize_lms.py +0 -0
  341. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_optimize_proposer.py +0 -0
  342. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_optimize_reflection_lm.py +0 -0
  343. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_optimize_terminal_failure.py +0 -0
  344. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_packaging.py +0 -0
  345. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_packaging_callable_source.py +0 -0
  346. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_packaging_mcp_tools.py +0 -0
  347. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_packaging_program_source.py +0 -0
  348. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_poll_until.py +0 -0
  349. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_program_identity.py +0 -0
  350. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_program_lineage_persist.py +0 -0
  351. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_programs_create.py +0 -0
  352. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_project_payload.py +0 -0
  353. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_real_provider_twins_readiness.py +0 -0
  354. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_reflector_properties.py +0 -0
  355. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_reflector_smoke.py +0 -0
  356. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_release_stress_script.py +0 -0
  357. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_repro_reactv2_bugs.py +0 -0
  358. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_require_version_bump.py +0 -0
  359. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_retry.py +0 -0
  360. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_reversible_failures.py +0 -0
  361. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_rlm_patch.py +0 -0
  362. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_run_attribution_paths.py +0 -0
  363. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_runtime_tools.py +0 -0
  364. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_schemas.py +0 -0
  365. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_seed_review_run.py +0 -0
  366. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_signature_inputs.py +0 -0
  367. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_span_references.py +0 -0
  368. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_span_sent_once.py +0 -0
  369. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_system_one.py +0 -0
  370. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_trace_context_concurrency.py +0 -0
  371. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_trace_fact_wire_contract.py +0 -0
  372. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_trace_facts.py +0 -0
  373. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_trace_render.py +0 -0
  374. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_traces_read.py +0 -0
  375. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_tracked_gepa.py +0 -0
  376. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_tracker_enrich.py +0 -0
  377. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_truncation.py +0 -0
  378. {cmpnd-0.45.0 → cmpnd-0.46.0}/tests/test_valsets_api.py +0 -0
  379. {cmpnd-0.45.0 → cmpnd-0.46.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.46.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,
@@ -17,7 +17,18 @@ from typing import Any
17
17
 
18
18
  import httpx
19
19
 
20
- from . import authored, configuration, deployment_metadata, execution, identity, mcp_servers, models, packaging, retry
20
+ from . import (
21
+ _http,
22
+ authored,
23
+ configuration,
24
+ deployment_metadata,
25
+ execution,
26
+ identity,
27
+ mcp_servers,
28
+ models,
29
+ packaging,
30
+ retry,
31
+ )
21
32
 
22
33
  logger = logging.getLogger(__name__)
23
34
 
@@ -427,7 +438,7 @@ def deploy(module, metric=None, timeout=300, metadata=None, *, program_id=None):
427
438
  references = _mcp_tool_references(module)
428
439
  pinned_mcp_tools: dict[tuple[str, str], Any] = {}
429
440
  if references:
430
- with httpx.Client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
441
+ with _http.new_client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
431
442
  pinned_mcp_tools = _resolve_mcp_tool_references(client, config, references)
432
443
 
433
444
  # Package the module
@@ -539,7 +550,7 @@ def _deploy_prepared(
539
550
  """Create, upload, and poll one fully prepared source bundle."""
540
551
  headers = {"X-API-Key": config.api_key, "Content-Type": "application/json"}
541
552
 
542
- with httpx.Client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
553
+ with _http.new_client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
543
554
  created = _send_retrying(
544
555
  lambda: client.post(
545
556
  f"{config.endpoint}/api/v1/deployments",
@@ -568,12 +579,13 @@ def _deploy_prepared(
568
579
  )
569
580
  for warning in answer.get("warnings") or []:
570
581
  logger.warning("deploy: %s", warning)
582
+ upload_url = answer["upload_url"]
583
+ upload_headers = {
584
+ "Content-Type": "application/zip",
585
+ **_http.upload_auth_headers(upload_url, config.endpoint, config.api_key),
586
+ }
571
587
  uploaded = _send_retrying(
572
- lambda: client.put(
573
- answer["upload_url"],
574
- content=zip_bytes,
575
- headers={"Content-Type": "application/zip"},
576
- ),
588
+ lambda: client.put(upload_url, content=zip_bytes, headers=upload_headers),
577
589
  "upload deployment ZIP",
578
590
  may_resend_with_no_answer=lambda _: True,
579
591
  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
@@ -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,26 +1414,21 @@ def _initiate_step(
1414
1414
  )
1415
1415
 
1416
1416
 
1417
- def _upload_bundle(client: httpx.Client, initiated: dict[str, Any], bundle: bytes) -> None:
1417
+ def _upload_bundle(
1418
+ client: httpx.Client, initiated: dict[str, Any], bundle: bytes, *, endpoint: str, api_key: str
1419
+ ) -> None:
1418
1420
  """Upload the optimization bundle to the slot the initiate step returned.
1419
1421
 
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.
1428
-
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 ``upload_url``, ``upload_method:
1423
+ "PUT"`` and empty ``upload_fields``, and the bundle goes out as the raw PUT body.
1424
+ Serve authenticates that route with the API key, which rides the PUT only when
1425
+ ``upload_url`` is on ``endpoint``'s origin (``_http.upload_auth_headers``). A
1426
+ non-empty ``upload_fields`` is a presigned form, which no current server sends.
1427
+ The bundle then rides as the ``file`` part of a multipart POST.
1432
1428
 
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.
1429
+ The slot pins its destination, so re-sending the same bytes writes the same
1430
+ object. That makes this call idempotent, so it rides through a transient answer
1431
+ and a transport blip alike.
1437
1432
  """
1438
1433
  upload_url = initiated.get("upload_url")
1439
1434
  if not upload_url:
@@ -1441,6 +1436,7 @@ def _upload_bundle(client: httpx.Client, initiated: dict[str, Any], bundle: byte
1441
1436
  upload_fields = initiated.get("upload_fields")
1442
1437
  method = (initiated.get("upload_method") or ("POST" if upload_fields else "PUT")).upper()
1443
1438
  what = "upload optimization bundle"
1439
+ auth = _http.upload_auth_headers(upload_url, endpoint, api_key)
1444
1440
 
1445
1441
  if upload_fields:
1446
1442
  resp = _request(
@@ -1448,6 +1444,7 @@ def _upload_bundle(client: httpx.Client, initiated: dict[str, Any], bundle: byte
1448
1444
  upload_url,
1449
1445
  data=dict(upload_fields),
1450
1446
  files={"file": ("data.zip", bundle, "application/zip")},
1447
+ headers=auth,
1451
1448
  ),
1452
1449
  what,
1453
1450
  )
@@ -1457,7 +1454,7 @@ def _upload_bundle(client: httpx.Client, initiated: dict[str, Any], bundle: byte
1457
1454
  method,
1458
1455
  upload_url,
1459
1456
  content=bundle,
1460
- headers={"Content-Type": "application/zip"},
1457
+ headers={"Content-Type": "application/zip", **auth},
1461
1458
  ),
1462
1459
  what,
1463
1460
  )
@@ -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.46.0"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"
@@ -27,6 +27,8 @@ import os
27
27
  import httpx
28
28
  import pytest
29
29
 
30
+ from cmpnd._http import upload_auth_headers
31
+
30
32
  CMPND_API_KEY = os.environ.get("CMPND_API_KEY")
31
33
  CMPND_ENDPOINT = os.environ.get("CMPND_ENDPOINT", "http://localhost:3000")
32
34
  CMPND_STUB_URL = os.environ.get("CMPND_STUB_URL", "http://localhost:9292")
@@ -77,18 +79,17 @@ def parser_running(stub_health: dict) -> None:
77
79
 
78
80
 
79
81
  def upload_deploy_zip(create_resp: dict, zip_bytes: bytes) -> None:
80
- """Upload a deploy ZIP via the single PUT slot the server returns.
82
+ """Upload a deploy ZIP to the PUT slot the create response names.
81
83
 
82
- Slice E: both postures advertise ``upload_method: "PUT"`` and one
83
- ``upload_url`` — a presigned S3 PUT (hosted) or the worker's own
84
- ``/programs/{id}/source`` (on-prem). Raw body, no form. Mirrors
85
- ``cmpnd.deploy``'s upload step so integration tests can drive it without
86
- the SDK's full deploy() wrapper.
84
+ Mirrors ``cmpnd.deploy``'s upload step, API key included when ``upload_url``
85
+ is on the endpoint's origin, so integration tests can drive it without the
86
+ SDK's full deploy() wrapper.
87
87
  """
88
- with httpx.Client(timeout=30.0) as anon:
89
- resp = anon.put(
90
- create_resp["upload_url"],
91
- content=zip_bytes,
92
- headers={"Content-Type": "application/zip"},
93
- )
88
+ upload_url = create_resp["upload_url"]
89
+ headers = {
90
+ "Content-Type": "application/zip",
91
+ **upload_auth_headers(upload_url, CMPND_ENDPOINT, CMPND_API_KEY or ""),
92
+ }
93
+ with httpx.Client(timeout=30.0) as client:
94
+ resp = client.put(upload_url, content=zip_bytes, headers=headers)
94
95
  resp.raise_for_status()