cmpnd 0.25.1__tar.gz → 0.26.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 (323) hide show
  1. {cmpnd-0.25.1 → cmpnd-0.26.0}/PKG-INFO +6 -1
  2. {cmpnd-0.25.1 → cmpnd-0.26.0}/README.md +5 -0
  3. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/__init__.py +4 -2
  4. cmpnd-0.26.0/cmpnd/_authored_import.py +201 -0
  5. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/_http.py +11 -3
  6. cmpnd-0.26.0/cmpnd/authored.py +328 -0
  7. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/top.py +66 -54
  8. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/shell.py +38 -56
  9. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/deployment.py +69 -90
  10. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/mcp_servers.py +118 -28
  11. {cmpnd-0.25.1 → cmpnd-0.26.0}/docs/cli.md +34 -26
  12. {cmpnd-0.25.1 → cmpnd-0.26.0}/docs/sdk-instrumentation.md +24 -15
  13. {cmpnd-0.25.1 → cmpnd-0.26.0}/pyproject.toml +1 -1
  14. cmpnd-0.26.0/tests/integration/hosted_mcp_tool_optimize_probe.py +321 -0
  15. cmpnd-0.26.0/tests/test_authored.py +231 -0
  16. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_api_client.py +8 -0
  17. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_shell.py +35 -4
  18. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_top.py +80 -78
  19. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_deploy.py +25 -0
  20. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_deploy_mcp_tool_references.py +14 -0
  21. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_mcp_tools.py +127 -1
  22. {cmpnd-0.25.1 → cmpnd-0.26.0}/uv.lock +1 -1
  23. {cmpnd-0.25.1 → cmpnd-0.26.0}/.gitignore +0 -0
  24. {cmpnd-0.25.1 → cmpnd-0.26.0}/AGENTS.md +0 -0
  25. {cmpnd-0.25.1 → cmpnd-0.26.0}/CLAUDE.md +0 -0
  26. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/_assessments.py +0 -0
  27. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/_exporter_pool.py +0 -0
  28. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/_gepa_patch.py +0 -0
  29. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/_program_patch.py +0 -0
  30. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/_rlm_patch.py +0 -0
  31. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/_timespec.py +0 -0
  32. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/_traces.py +0 -0
  33. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/_valsets.py +0 -0
  34. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/callback.py +0 -0
  35. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/__init__.py +0 -0
  36. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/api_client.py +0 -0
  37. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/__init__.py +0 -0
  38. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/admin.py +0 -0
  39. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/assessments.py +0 -0
  40. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/auth.py +0 -0
  41. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/datasets.py +0 -0
  42. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/deployments.py +0 -0
  43. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/evals.py +0 -0
  44. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/login.py +0 -0
  45. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/optimizations.py +0 -0
  46. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/org.py +0 -0
  47. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/programs.py +0 -0
  48. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  49. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/signatures.py +0 -0
  50. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  51. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/traces.py +0 -0
  52. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/commands/valsets.py +0 -0
  53. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/credentials.py +0 -0
  54. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/deprecation.py +0 -0
  55. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/pagination.py +0 -0
  56. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/snippet.py +0 -0
  57. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/trace_render.py +0 -0
  58. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/tree_help.py +0 -0
  59. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/cli/verbs.py +0 -0
  60. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/client.py +0 -0
  61. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/configuration.py +0 -0
  62. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/context.py +0 -0
  63. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/dataset_sync.py +0 -0
  64. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/datasets.py +0 -0
  65. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/decorators.py +0 -0
  66. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/eval_handler.py +0 -0
  67. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/execution.py +0 -0
  68. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/exporter.py +0 -0
  69. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/exporter_http.py +0 -0
  70. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/helpers.py +0 -0
  71. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/identity.py +0 -0
  72. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/imaging.py +0 -0
  73. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/ir/__init__.py +0 -0
  74. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/ir/decode.py +0 -0
  75. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/ir/encode.py +0 -0
  76. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/ir/hash.py +0 -0
  77. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/ir/program.py +0 -0
  78. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/ir/reflection.py +0 -0
  79. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/ir/types.py +0 -0
  80. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/ir/xxh64.py +0 -0
  81. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/models.py +0 -0
  82. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/optimization.py +0 -0
  83. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/optimizers/__init__.py +0 -0
  84. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/optimizers/gepa.py +0 -0
  85. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  86. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/optimizers/resume.py +0 -0
  87. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/optimizers/tracker.py +0 -0
  88. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/packaging.py +0 -0
  89. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/retry.py +0 -0
  90. {cmpnd-0.25.1 → cmpnd-0.26.0}/cmpnd/traces.py +0 -0
  91. {cmpnd-0.25.1 → cmpnd-0.26.0}/docs/2026-04-02-code-review.md +0 -0
  92. {cmpnd-0.25.1 → cmpnd-0.26.0}/docs/client.md +0 -0
  93. {cmpnd-0.25.1 → cmpnd-0.26.0}/docs/custom-proposer.md +0 -0
  94. {cmpnd-0.25.1 → cmpnd-0.26.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  95. {cmpnd-0.25.1 → cmpnd-0.26.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  96. {cmpnd-0.25.1 → cmpnd-0.26.0}/docs/reading-traces.md +0 -0
  97. {cmpnd-0.25.1 → cmpnd-0.26.0}/examples/generalizing_proposer.py +0 -0
  98. {cmpnd-0.25.1 → cmpnd-0.26.0}/examples/local_ollama.py +0 -0
  99. {cmpnd-0.25.1 → cmpnd-0.26.0}/ingest/LINEAGE.md +0 -0
  100. {cmpnd-0.25.1 → cmpnd-0.26.0}/ingest/README.md +0 -0
  101. {cmpnd-0.25.1 → cmpnd-0.26.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  102. {cmpnd-0.25.1 → cmpnd-0.26.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  103. {cmpnd-0.25.1 → cmpnd-0.26.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  104. {cmpnd-0.25.1 → cmpnd-0.26.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  105. {cmpnd-0.25.1 → cmpnd-0.26.0}/ingest/cmpnd_ingest/py.typed +0 -0
  106. {cmpnd-0.25.1 → cmpnd-0.26.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  107. {cmpnd-0.25.1 → cmpnd-0.26.0}/ingest/cmpnd_ingest/retry.py +0 -0
  108. {cmpnd-0.25.1 → cmpnd-0.26.0}/ingest/pyproject.toml +0 -0
  109. {cmpnd-0.25.1 → cmpnd-0.26.0}/scripts/audit-sdist.sh +0 -0
  110. {cmpnd-0.25.1 → cmpnd-0.26.0}/scripts/capture_review_run.py +0 -0
  111. {cmpnd-0.25.1 → cmpnd-0.26.0}/scripts/check_release_artifacts.py +0 -0
  112. {cmpnd-0.25.1 → cmpnd-0.26.0}/scripts/fixtures/review_run.json.gz +0 -0
  113. {cmpnd-0.25.1 → cmpnd-0.26.0}/scripts/gepa_resume_bug.py +0 -0
  114. {cmpnd-0.25.1 → cmpnd-0.26.0}/scripts/require_version_bump.py +0 -0
  115. {cmpnd-0.25.1 → cmpnd-0.26.0}/scripts/seed_review_run.py +0 -0
  116. {cmpnd-0.25.1 → cmpnd-0.26.0}/scripts/test-in-linux.sh +0 -0
  117. {cmpnd-0.25.1 → cmpnd-0.26.0}/stubs/dspy/__init__.pyi +0 -0
  118. {cmpnd-0.25.1 → cmpnd-0.26.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  119. {cmpnd-0.25.1 → cmpnd-0.26.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  120. {cmpnd-0.25.1 → cmpnd-0.26.0}/stubs/dspy/utils/__init__.pyi +0 -0
  121. {cmpnd-0.25.1 → cmpnd-0.26.0}/stubs/dspy/utils/callback.pyi +0 -0
  122. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/__init__.py +0 -0
  123. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/_exporter_helpers.py +0 -0
  124. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/_fake_clock.py +0 -0
  125. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/_identity_helpers.py +0 -0
  126. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/conftest.py +0 -0
  127. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/__init__.py +0 -0
  128. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/committee_task.py +0 -0
  129. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/conftest.py +0 -0
  130. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  131. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  132. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/fake_lm.py +0 -0
  133. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/helpers.py +0 -0
  134. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/target_capabilities.py +0 -0
  135. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_assessments_cli.py +0 -0
  136. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_capture_prompts.py +0 -0
  137. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_composite_module.py +0 -0
  138. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  139. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_deploy_execute.py +0 -0
  140. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_deploy_programs.py +0 -0
  141. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_edge_cases.py +0 -0
  142. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_example_hash.py +0 -0
  143. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_gepa_optimization.py +0 -0
  144. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_interpreter_spans.py +0 -0
  145. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_lm_accuracy.py +0 -0
  146. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_module_tracing.py +0 -0
  147. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_optimization_lineage.py +0 -0
  148. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  149. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_optimize_failures.py +0 -0
  150. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_optimize_loop.py +0 -0
  151. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  152. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  153. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_parallel_export.py +0 -0
  154. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_project_attribution.py +0 -0
  155. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_react_tool_spans.py +0 -0
  156. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_save_load.py +0 -0
  157. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_trace_structure.py +0 -0
  158. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_traces_read.py +0 -0
  159. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_user_attribution.py +0 -0
  160. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/e2e/test_valsets_cli.py +0 -0
  161. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  162. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  163. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  164. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  165. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  166. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  167. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  168. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  169. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  170. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/10_react.json +0 -0
  171. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/11_refine.json +0 -0
  172. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/12_retrieve.json +0 -0
  173. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/13_knn.json +0 -0
  174. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/14_parallel.json +0 -0
  175. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/identity_vectors/README.md +0 -0
  176. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/ingest_conformance.py +0 -0
  177. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/integration/__init__.py +0 -0
  178. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/integration/conftest.py +0 -0
  179. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/integration/hosted_optimize_probe.py +0 -0
  180. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/integration/optimization_run_probe.py +0 -0
  181. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  182. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/integration/test_logs_renders_traces.py +0 -0
  183. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/integration/test_optimization_run.py +0 -0
  184. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/integration/test_optimizing_progress.py +0 -0
  185. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/integration/test_unhappy_path.py +0 -0
  186. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/load/__init__.py +0 -0
  187. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/load/__main__.py +0 -0
  188. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/load/drain.py +0 -0
  189. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/load/ingest.py +0 -0
  190. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/load/latency_transport.py +0 -0
  191. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/load/rlm_shape.py +0 -0
  192. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/load/sweep.py +0 -0
  193. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/load/test_rlm_shape.py +0 -0
  194. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/__init__.py +0 -0
  195. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/conftest.py +0 -0
  196. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/seed_parity.py +0 -0
  197. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/test_auth.py +0 -0
  198. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/test_datasets.py +0 -0
  199. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/test_deployments.py +0 -0
  200. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/test_evals.py +0 -0
  201. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/test_health.py +0 -0
  202. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/test_modules.py +0 -0
  203. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/test_optimizations.py +0 -0
  204. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/test_programs.py +0 -0
  205. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/test_signatures.py +0 -0
  206. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/test_trace_streaming.py +0 -0
  207. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/parity/test_traces.py +0 -0
  208. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/__init__.py +0 -0
  209. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/_provider.py +0 -0
  210. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/_reliability.py +0 -0
  211. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/_result.py +0 -0
  212. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/mint_bedrock_token.py +0 -0
  213. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/test_deploy_programs.py +0 -0
  214. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/test_multi_client_stress.py +0 -0
  215. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/test_optimize_client_committee.py +0 -0
  216. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/test_optimize_loop.py +0 -0
  217. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  218. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/test_optimize_stress.py +0 -0
  219. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/test_provider_roles.py +0 -0
  220. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/test_real_provider_twins.py +0 -0
  221. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/test_reliability.py +0 -0
  222. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/test_reliability_classify.py +0 -0
  223. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/test_rlm_wasm.py +0 -0
  224. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/stress/test_trace_search.py +0 -0
  225. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_assessments_api.py +0 -0
  226. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_callback.py +0 -0
  227. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_candidate_lineage.py +0 -0
  228. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/__init__.py +0 -0
  229. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/conftest.py +0 -0
  230. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_admin.py +0 -0
  231. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_assessments.py +0 -0
  232. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_auth.py +0 -0
  233. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_credentials.py +0 -0
  234. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_datasets.py +0 -0
  235. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_deployments.py +0 -0
  236. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_evals.py +0 -0
  237. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_login.py +0 -0
  238. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_main.py +0 -0
  239. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_optimizations.py +0 -0
  240. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_org.py +0 -0
  241. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_pagination.py +0 -0
  242. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_parity.py +0 -0
  243. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_programs.py +0 -0
  244. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_provider_credentials.py +0 -0
  245. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_schema_sample.py +0 -0
  246. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_signatures.py +0 -0
  247. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_tool_servers.py +0 -0
  248. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_traces.py +0 -0
  249. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  250. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_tree_help.py +0 -0
  251. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cli/test_valsets.py +0 -0
  252. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_client.py +0 -0
  253. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_clustering/__init__.py +0 -0
  254. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_clustering/conftest.py +0 -0
  255. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_clustering/test_edge_cases.py +0 -0
  256. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  257. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_clustering/test_type_conversion.py +0 -0
  258. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_cmp551_grouping_identity.py +0 -0
  259. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_configuration.py +0 -0
  260. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_context.py +0 -0
  261. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_context_propagation.py +0 -0
  262. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_dataset_polling.py +0 -0
  263. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_dataset_sync.py +0 -0
  264. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_datasets.py +0 -0
  265. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_decorators.py +0 -0
  266. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_eval.py +0 -0
  267. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_eval_example_attribution.py +0 -0
  268. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_eval_start.py +0 -0
  269. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_eval_trace_grouping.py +0 -0
  270. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_exception_paths.py +0 -0
  271. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_execute.py +0 -0
  272. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_exporter.py +0 -0
  273. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_exporter_queue.py +0 -0
  274. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_exporter_real.py +0 -0
  275. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_fake_lm.py +0 -0
  276. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_gepa_callback.py +0 -0
  277. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_gepa_integration.py +0 -0
  278. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_gepa_resume.py +0 -0
  279. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_identity.py +0 -0
  280. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_imaging.py +0 -0
  281. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_import_weight.py +0 -0
  282. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_ingest_conformance.py +0 -0
  283. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_ingest_second_producer.py +0 -0
  284. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_integration.py +0 -0
  285. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_interpreter_callback.py +0 -0
  286. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_ir_encoder_properties.py +0 -0
  287. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_ir_golden_vectors.py +0 -0
  288. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_ir_roundtrip.py +0 -0
  289. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_lm_usage_shapes.py +0 -0
  290. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_models.py +0 -0
  291. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_optimization_progress.py +0 -0
  292. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_optimization_start.py +0 -0
  293. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_optimize.py +0 -0
  294. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_optimize_budget.py +0 -0
  295. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_optimize_lift.py +0 -0
  296. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_optimize_lms.py +0 -0
  297. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_optimize_proposer.py +0 -0
  298. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_optimize_reflection_lm.py +0 -0
  299. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_optimize_terminal_failure.py +0 -0
  300. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_packaging.py +0 -0
  301. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_packaging_callable_source.py +0 -0
  302. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_packaging_mcp_tools.py +0 -0
  303. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_packaging_program_source.py +0 -0
  304. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_poll_until.py +0 -0
  305. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_program_lineage_persist.py +0 -0
  306. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_project_payload.py +0 -0
  307. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_reflector_properties.py +0 -0
  308. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_reflector_smoke.py +0 -0
  309. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_repro_reactv2_bugs.py +0 -0
  310. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_require_version_bump.py +0 -0
  311. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_retry.py +0 -0
  312. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_reversible_failures.py +0 -0
  313. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_rlm_patch.py +0 -0
  314. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_schemas.py +0 -0
  315. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_seed_review_run.py +0 -0
  316. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_trace_render.py +0 -0
  317. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_traces_read.py +0 -0
  318. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_tracked_gepa.py +0 -0
  319. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_tracker_enrich.py +0 -0
  320. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_truncation.py +0 -0
  321. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_valsets_api.py +0 -0
  322. {cmpnd-0.25.1 → cmpnd-0.26.0}/tests/test_xxh64.py +0 -0
  323. {cmpnd-0.25.1 → cmpnd-0.26.0}/todo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.25.1
3
+ Version: 0.26.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>
@@ -137,6 +137,11 @@ print(result.answer)
137
137
 
138
138
  The SDK packages the module's source code into a ZIP, uploads it to the platform, and polls until the server-side parser finishes. Returns a `DeployedProgram` you can **call directly** to execute server-side; `str(deployed)` is its `deployment_id` and `deployed.deployment_id` exposes it explicitly.
139
139
 
140
+ For a source file, use `cmpnd deploy program.py`. The CLI uploads the file's
141
+ exact bytes instead of reconstructing it from a live object. Use
142
+ `--source-root` and repeated `--include` flags for sibling modules or data, and
143
+ `cmpnd inspect <id> --source` to read the deployed entry file back.
144
+
140
145
  ### With a metric (for optimization)
141
146
 
142
147
  ```python
@@ -89,6 +89,11 @@ print(result.answer)
89
89
 
90
90
  The SDK packages the module's source code into a ZIP, uploads it to the platform, and polls until the server-side parser finishes. Returns a `DeployedProgram` you can **call directly** to execute server-side; `str(deployed)` is its `deployment_id` and `deployed.deployment_id` exposes it explicitly.
91
91
 
92
+ For a source file, use `cmpnd deploy program.py`. The CLI uploads the file's
93
+ exact bytes instead of reconstructing it from a live object. Use
94
+ `--source-root` and repeated `--include` flags for sibling modules or data, and
95
+ `cmpnd inspect <id> --source` to read the deployed entry file back.
96
+
92
97
  ### With a metric (for optimization)
93
98
 
94
99
  ```python
@@ -14,7 +14,7 @@ Usage:
14
14
 
15
15
  import importlib.metadata
16
16
 
17
- from . import traces
17
+ from . import authored, traces
18
18
  from ._http import ApiError
19
19
  from .callback import CmpndCallback, trace_id
20
20
  from .client import Client
@@ -26,7 +26,7 @@ from .context import (
26
26
  )
27
27
  from .datasets import DatasetNotFoundError, VersionNotFoundError, load_dataset
28
28
  from .decorators import start_span, trace
29
- from .deployment import DeployFailed, deploy
29
+ from .deployment import DeployFailed, deploy, deploy_bundle
30
30
  from .execution import DeployedProgram, execute
31
31
  from .exporter import flush_exporter, shutdown_exporter
32
32
  from .mcp_servers import (
@@ -80,6 +80,7 @@ __all__ = [
80
80
  "shutdown_exporter",
81
81
  # Programmatic backend reads (cmpnd.traces.list / cmpnd.traces.show)
82
82
  "traces",
83
+ "authored",
83
84
  "ApiError",
84
85
  # Types
85
86
  "SpanType",
@@ -88,6 +89,7 @@ __all__ = [
88
89
  "TraceBuilder",
89
90
  # Deploy & execute
90
91
  "deploy",
92
+ "deploy_bundle",
91
93
  "DeployFailed",
92
94
  "execute",
93
95
  "DeployedProgram",
@@ -0,0 +1,201 @@
1
+ """Isolated import checker for authored deployment bundles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import importlib
7
+ import inspect
8
+ import json
9
+ import sys
10
+ import tempfile
11
+ import traceback
12
+ import zipfile
13
+ from dataclasses import asdict
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+
18
+ def _program(module: Any, entrypoint: str) -> tuple[Any, str]:
19
+ import dspy
20
+
21
+ if not hasattr(module, entrypoint):
22
+ raise ValueError(f"entrypoint {entrypoint!r} is not defined by the authored program")
23
+ value = getattr(module, entrypoint)
24
+ if isinstance(value, dspy.Module):
25
+ return value, "instance"
26
+ if inspect.isclass(value) and issubclass(value, dspy.Module):
27
+ _require_zero_arguments(value, f"entrypoint class {entrypoint!r}")
28
+ return value(), "class"
29
+ if callable(value):
30
+ _require_zero_arguments(value, f"entrypoint factory {entrypoint!r}")
31
+ built = value()
32
+ if isinstance(built, dspy.Module):
33
+ return built, "factory"
34
+ raise ValueError(
35
+ f"entrypoint {entrypoint!r} must be a dspy.Module instance, a dspy.Module class, or a zero-argument factory"
36
+ )
37
+
38
+
39
+ def _require_zero_arguments(value: Any, label: str) -> None:
40
+ """Refuse required parameters without mislabelling errors inside the call."""
41
+ try:
42
+ signature = inspect.signature(value)
43
+ except (TypeError, ValueError):
44
+ return
45
+ try:
46
+ signature.bind()
47
+ except TypeError as exc:
48
+ raise ValueError(f"{label} must accept no arguments: {exc}") from exc
49
+
50
+
51
+ def _check(request: dict[str, Any]) -> dict[str, Any]:
52
+ from . import configuration, deployment, identity, mcp_servers, packaging
53
+ from .authored import ProgramFacts
54
+
55
+ original_configure = configuration.configure
56
+ authored_limits: dict[str, int | None] = {"max_input_size": None, "max_output_size": None}
57
+ if request.get("api_key"):
58
+ original_configure(api_key=request["api_key"], endpoint=request.get("endpoint"))
59
+
60
+ def source_configure(api_key=None, endpoint=None, *args, **kwargs):
61
+ for name in authored_limits:
62
+ value = kwargs.get(name)
63
+ authored_limits[name] = value if isinstance(value, int) else None
64
+ return original_configure(
65
+ api_key=api_key or request["api_key"],
66
+ endpoint=endpoint or request.get("endpoint"),
67
+ *args,
68
+ **kwargs,
69
+ )
70
+
71
+ # The check has its own platform configuration but exposes no secret in
72
+ # the child environment. Let authored ``cmpnd.configure(max_...)`` keep
73
+ # that transport while it changes the settings the program owns.
74
+ configuration.configure = source_configure
75
+ setattr(sys.modules["cmpnd"], "configure", source_configure)
76
+ with tempfile.TemporaryDirectory(prefix="cmpnd-authored-") as directory:
77
+ root = Path(directory)
78
+ with zipfile.ZipFile(request["bundle"]) as archive:
79
+ archive.extractall(root)
80
+ manifest = json.loads((root / "manifest.json").read_text())
81
+ module_name = str(manifest["program"]).removesuffix(".py").replace("/", ".")
82
+ sys.path.insert(0, str(root))
83
+ try:
84
+ module = importlib.import_module(module_name)
85
+ finally:
86
+ sys.path.remove(str(root))
87
+ program, kind = _program(module, str(manifest["entrypoint"]))
88
+
89
+ references = deployment._mcp_tool_references(program)
90
+ deployment._refuse_mcp_tools_outside_rlm(program, references)
91
+ pinned: dict[tuple[str, str], Any] = {}
92
+ if references:
93
+ import httpx
94
+
95
+ config = configuration.require_config()
96
+ with httpx.Client(timeout=30) as client:
97
+ pinned = deployment._resolve_mcp_tool_references(client, config, references)
98
+ tool_references = [
99
+ {
100
+ # The child already checked an explicit credential name against the
101
+ # registered server binding. Serve resolves that binding again and
102
+ # never accepts a client-selected credential name in this declaration.
103
+ "server_url": item.server_url,
104
+ "tool": item.tool_name,
105
+ "schema_hash": item.schema_hash,
106
+ "parameters": mcp_servers.synthesized_signature(item).parameter_names,
107
+ }
108
+ for item in pinned.values()
109
+ ]
110
+ tool_declarations = [
111
+ {
112
+ "server_url": item.server_url,
113
+ "name": item.tool_name,
114
+ "schema_hash": item.schema_hash,
115
+ "input_schema": item.input_schema,
116
+ "description": item.description,
117
+ "parameters": mcp_servers.synthesized_signature(item).parameter_names,
118
+ }
119
+ for item in pinned.values()
120
+ ]
121
+
122
+ signature = packaging._get_module_signature(program)
123
+ fields: dict[str, Any] = {}
124
+ if signature is not None:
125
+ fields = {
126
+ "signature_name": packaging._signature_to_string(signature),
127
+ "input_fields": list(signature.input_fields),
128
+ "output_fields": list(signature.output_fields),
129
+ "input_field_types": [identity.get_type_string(f.annotation) for f in signature.input_fields.values()],
130
+ "output_field_types": [
131
+ identity.get_type_string(f.annotation) for f in signature.output_fields.values()
132
+ ],
133
+ }
134
+ import dspy
135
+
136
+ lm = dspy.settings.lm
137
+ lm_kwargs = getattr(lm, "kwargs", {}) if lm is not None else {}
138
+ facts = ProgramFacts(
139
+ program_signature_hash=identity.compute_program_hash(program),
140
+ entrypoint_kind=kind,
141
+ module_class=type(program).__name__,
142
+ litellm_model=getattr(lm, "model", None),
143
+ temperature=lm_kwargs.get("temperature"),
144
+ max_tokens=lm_kwargs.get("max_tokens"),
145
+ thinking_effort=lm_kwargs.get("thinking_effort"),
146
+ max_input_size=authored_limits["max_input_size"],
147
+ max_output_size=authored_limits["max_output_size"],
148
+ tool_references=tool_references,
149
+ tool_declarations=tool_declarations,
150
+ **fields,
151
+ )
152
+ return asdict(facts)
153
+
154
+
155
+ def main() -> int:
156
+ try:
157
+ request = json.loads(sys.stdin.read())
158
+ with contextlib.redirect_stdout(sys.stderr):
159
+ facts = _check(request)
160
+ print(json.dumps({"facts": facts}))
161
+ return 0
162
+ except ModuleNotFoundError as exc:
163
+ print(
164
+ json.dumps(
165
+ {
166
+ "error": (
167
+ f"the authored program imports {exc.name!r}, which the local import check cannot find. "
168
+ "If it is a sibling module, add its file or directory with --include; otherwise install "
169
+ "the dependency locally and ensure the worker supports it."
170
+ )
171
+ }
172
+ )
173
+ )
174
+ except KeyError as exc:
175
+ print(
176
+ json.dumps(
177
+ {
178
+ "error": (
179
+ f"authored source import failed with KeyError: {exc}. The check runs without your "
180
+ "environment because the worker has none. Do not read provider or MCP credentials at "
181
+ "module scope. Name the model without its key and use cmpnd.mcp_tools(url) for a "
182
+ "registered server.\n" + _concise_traceback()
183
+ )
184
+ }
185
+ )
186
+ )
187
+ except BaseException as exc:
188
+ print(
189
+ json.dumps(
190
+ {"error": (f"authored source import failed with {type(exc).__name__}: {exc}\n" + _concise_traceback())}
191
+ )
192
+ )
193
+ return 1
194
+
195
+
196
+ def _concise_traceback() -> str:
197
+ return "".join(traceback.format_exc(limit=6)).strip()
198
+
199
+
200
+ if __name__ == "__main__":
201
+ raise SystemExit(main())
@@ -62,7 +62,7 @@ class ApiClient:
62
62
  return self._request("GET", path, params=params)
63
63
 
64
64
  def get_text(self, path: str, params: dict[str, str] | None = None) -> str:
65
- """GET a route that does not answer JSON, returning the body undecoded.
65
+ """GET a route that does not answer JSON, returning its decoded text.
66
66
 
67
67
  Most of this API is JSON and `get` is right for it; a handful of routes
68
68
  serve another media type (a deployment's OpenAPI document is YAML), and
@@ -73,6 +73,10 @@ class ApiClient:
73
73
  """
74
74
  return self._request("GET", path, params=params, decode=False)
75
75
 
76
+ def get_bytes(self, path: str, params: dict[str, str] | None = None) -> bytes:
77
+ """GET a route whose response bytes must survive unchanged."""
78
+ return self._request("GET", path, params=params, decode=None)
79
+
76
80
  def post(self, path: str, json: dict[str, Any] | None = None) -> Any:
77
81
  return self._request("POST", path, json=json)
78
82
 
@@ -94,7 +98,7 @@ class ApiClient:
94
98
  path: str,
95
99
  params: dict[str, str] | None = None,
96
100
  json: dict[str, Any] | None = None,
97
- decode: bool = True,
101
+ decode: bool | None = True,
98
102
  ) -> Any:
99
103
  url = f"{self._endpoint}{path}"
100
104
  # 180s default: deploy/optimize/run wait in-band on an LM round-trip
@@ -157,7 +161,11 @@ class ApiClient:
157
161
  detail = response.text
158
162
  raise ApiError(f"Request failed ({response.status_code}): {detail}", status_code=response.status_code)
159
163
 
160
- # A non-JSON route hands its body back verbatim. Checked before the
164
+ # A bytes route hands the raw body back before httpx decodes it.
165
+ if decode is None:
166
+ return response.content
167
+
168
+ # A non-JSON text route hands its decoded body back. Checked before the
161
169
  # empty-body arm so such a route answers "" rather than None, which is
162
170
  # what lets a caller print it without a None guard.
163
171
  if not decode:
@@ -0,0 +1,328 @@
1
+ """Build and check source bundles for ``cmpnd deploy <file>``.
2
+
3
+ The CLI copies authored files into a deterministic ZIP. It never imports the
4
+ program in the CLI process. A short-lived child imports the exact staged ZIP,
5
+ resolves its declared entrypoint, and returns the facts the deployment API needs.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.metadata
11
+ import json
12
+ import math
13
+ import os
14
+ import platform
15
+ import re
16
+ import stat
17
+ import subprocess
18
+ import sys
19
+ import tempfile
20
+ import warnings
21
+ import zipfile
22
+ from dataclasses import dataclass, field
23
+ from io import BytesIO
24
+ from pathlib import Path, PurePosixPath
25
+ from typing import Any
26
+
27
+ _MAX_BUNDLE_BYTES = 50 * 1024 * 1024
28
+ _CHECK_TIMEOUT_SECONDS = 60.0
29
+ _CHECK_TIMEOUT_ENV = "CMPND_AUTHORED_IMPORT_TIMEOUT_SECONDS"
30
+ _WORKER_DSPY_BASELINE = (3, 3, 1)
31
+
32
+
33
+ class SourceCheckError(ValueError):
34
+ """The isolated authored-source import check refused a bundle."""
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ProgramFacts:
39
+ """Facts derived by importing the staged program without calling forward."""
40
+
41
+ program_signature_hash: int | None = None
42
+ entrypoint_kind: str = "instance"
43
+ module_class: str | None = None
44
+ signature_name: str | None = None
45
+ input_fields: list[str] = field(default_factory=list)
46
+ output_fields: list[str] = field(default_factory=list)
47
+ input_field_types: list[str] = field(default_factory=list)
48
+ output_field_types: list[str] = field(default_factory=list)
49
+ litellm_model: str | None = None
50
+ temperature: float | None = None
51
+ max_tokens: int | None = None
52
+ thinking_effort: str | None = None
53
+ max_input_size: int | None = None
54
+ max_output_size: int | None = None
55
+ tool_references: list[dict[str, Any]] = field(default_factory=list)
56
+ tool_declarations: list[dict[str, Any]] = field(default_factory=list)
57
+
58
+ @classmethod
59
+ def minimal(cls) -> ProgramFacts:
60
+ """Return a small fixture-friendly value."""
61
+ return cls()
62
+
63
+ @classmethod
64
+ def from_json(cls, value: dict[str, Any]) -> ProgramFacts:
65
+ return cls(**{name: value[name] for name in cls.__dataclass_fields__ if name in value})
66
+
67
+ def deploy_body(self) -> dict[str, Any]:
68
+ """Return fields the deployment create route accepts."""
69
+ from . import identity
70
+
71
+ body: dict[str, Any] = {}
72
+ if self.program_signature_hash is not None:
73
+ body["program_signature_hash"] = identity.format_signature_hash(self.program_signature_hash)
74
+ for name in (
75
+ "module_class",
76
+ "signature_name",
77
+ "litellm_model",
78
+ "temperature",
79
+ "max_tokens",
80
+ "thinking_effort",
81
+ "max_input_size",
82
+ "max_output_size",
83
+ ):
84
+ value = getattr(self, name)
85
+ if value is not None and value != "":
86
+ body[name] = value
87
+ if self.signature_name is not None:
88
+ for name in ("input_fields", "output_fields", "input_field_types", "output_field_types"):
89
+ body[name] = getattr(self, name)
90
+ if self.tool_references:
91
+ body["tool_references"] = self.tool_references
92
+ return body
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class PreparedSource:
97
+ bundle: bytes
98
+ facts: ProgramFacts
99
+ dependency_versions: dict[str, str]
100
+
101
+
102
+ def prepare_file(
103
+ source: str | Path,
104
+ *,
105
+ source_root: str | Path | None = None,
106
+ includes: list[str | Path] | None = None,
107
+ entrypoint: str = "program",
108
+ api_key: str | None = None,
109
+ endpoint: str | None = None,
110
+ ) -> PreparedSource:
111
+ """Stage one authored Python file and its explicit includes."""
112
+ source_path = Path(source).expanduser()
113
+ root = Path(source_root).expanduser() if source_root is not None else source_path.parent
114
+ root = _plain_directory(root, "source root")
115
+ source_path = _plain_file(source_path, "program")
116
+ members: dict[str, bytes] = {}
117
+ _add_file(members, root, source_path)
118
+ for included in includes or []:
119
+ _add_path(members, root, Path(included).expanduser())
120
+ program = _relative_name(root, source_path)
121
+ return _prepare(members, program, entrypoint, api_key=api_key, endpoint=endpoint)
122
+
123
+
124
+ def prepare_stdin(
125
+ source: bytes | str,
126
+ *,
127
+ entrypoint: str = "program",
128
+ api_key: str | None = None,
129
+ endpoint: str | None = None,
130
+ ) -> PreparedSource:
131
+ """Stage stdin as the single authored ``program.py`` member."""
132
+ raw = source.encode() if isinstance(source, str) else source
133
+ return _prepare({"program.py": raw}, "program.py", entrypoint, api_key=api_key, endpoint=endpoint)
134
+
135
+
136
+ def _prepare(
137
+ members: dict[str, bytes],
138
+ program: str,
139
+ entrypoint: str,
140
+ *,
141
+ api_key: str | None,
142
+ endpoint: str | None,
143
+ ) -> PreparedSource:
144
+ if not members.get(program, b"").strip():
145
+ raise ValueError("the authored program is empty")
146
+ if not entrypoint or not entrypoint.isidentifier():
147
+ raise ValueError("entrypoint must be a Python identifier")
148
+ versions = _dependency_versions()
149
+ local_dspy = _release_tuple(versions["dspy"])
150
+ if local_dspy is not None and local_dspy > _WORKER_DSPY_BASELINE:
151
+ worker_dspy = ".".join(str(part) for part in _WORKER_DSPY_BASELINE)
152
+ warnings.warn(
153
+ f"local DSPy {versions['dspy']} is newer than the worker's {worker_dspy} compatibility baseline. "
154
+ "The local import check can pass even when the worker rejects a newer DSPy spelling.",
155
+ RuntimeWarning,
156
+ stacklevel=2,
157
+ )
158
+ bundle_members = ["manifest.json", *sorted(members)]
159
+ manifest: dict[str, Any] = {
160
+ "program": program,
161
+ "entrypoint": entrypoint,
162
+ "members": bundle_members,
163
+ "authored": True,
164
+ "dependency_versions": versions,
165
+ }
166
+ bundle = _zip(manifest, members)
167
+ facts = _check_bundle(bundle, api_key=api_key, endpoint=endpoint)
168
+ if facts.tool_declarations:
169
+ # The child pinned these declarations while importing this exact source.
170
+ # Serve checks each schema hash against the registry again on create, so
171
+ # adding the declarations to the manifest does not need a second import.
172
+ manifest["tools"] = facts.tool_declarations
173
+ bundle = _zip(manifest, members)
174
+ return PreparedSource(bundle=bundle, facts=facts, dependency_versions=versions)
175
+
176
+
177
+ def _dependency_versions() -> dict[str, str]:
178
+ def version(name: str) -> str:
179
+ try:
180
+ return importlib.metadata.version(name)
181
+ except importlib.metadata.PackageNotFoundError:
182
+ return "unknown"
183
+
184
+ return {"python": platform.python_version(), "dspy": version("dspy"), "cmpnd": version("cmpnd")}
185
+
186
+
187
+ def _release_tuple(value: str) -> tuple[int, int, int] | None:
188
+ """Return the numeric release prefix needed for the compatibility warning."""
189
+ matched = re.match(r"^(\d+)\.(\d+)\.(\d+)", value)
190
+ if matched is None:
191
+ return None
192
+ return (int(matched.group(1)), int(matched.group(2)), int(matched.group(3)))
193
+
194
+
195
+ def _zip(manifest: dict[str, Any], members: dict[str, bytes]) -> bytes:
196
+ manifest_bytes = json.dumps(manifest, indent=2, sort_keys=True).encode() + b"\n"
197
+ uncompressed_size = len(manifest_bytes) + sum(len(contents) for contents in members.values())
198
+ if uncompressed_size > _MAX_BUNDLE_BYTES:
199
+ raise ValueError(
200
+ f"source bundle is {uncompressed_size} bytes uncompressed; the limit is {_MAX_BUNDLE_BYTES} bytes"
201
+ )
202
+ output = BytesIO()
203
+ with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as archive:
204
+ _write_member(archive, "manifest.json", manifest_bytes)
205
+ for name in sorted(members):
206
+ _write_member(archive, name, members[name])
207
+ bundle = output.getvalue()
208
+ if len(bundle) > _MAX_BUNDLE_BYTES:
209
+ raise ValueError(f"source bundle is {len(bundle)} bytes; the limit is {_MAX_BUNDLE_BYTES} bytes")
210
+ return bundle
211
+
212
+
213
+ def _write_member(archive: zipfile.ZipFile, name: str, contents: bytes) -> None:
214
+ info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
215
+ info.compress_type = zipfile.ZIP_DEFLATED
216
+ info.external_attr = (stat.S_IFREG | 0o644) << 16
217
+ archive.writestr(info, contents)
218
+
219
+
220
+ def _plain_directory(path: Path, label: str) -> Path:
221
+ if path.is_symlink():
222
+ raise ValueError(f"{label} may not be a symbolic link: {path}")
223
+ resolved = path.resolve()
224
+ if not resolved.is_dir():
225
+ raise ValueError(f"{label} is not a directory: {path}")
226
+ return resolved
227
+
228
+
229
+ def _plain_file(path: Path, label: str) -> Path:
230
+ if path.is_symlink():
231
+ raise ValueError(f"{label} may not be a symbolic link: {path}")
232
+ resolved = path.resolve()
233
+ if not resolved.is_file():
234
+ raise ValueError(f"{label} is not a regular file: {path}")
235
+ return resolved
236
+
237
+
238
+ def _relative_name(root: Path, path: Path) -> str:
239
+ try:
240
+ relative = path.relative_to(root)
241
+ except ValueError as exc:
242
+ raise ValueError(f"{path} is outside source root {root}") from exc
243
+ name = PurePosixPath(*relative.parts).as_posix()
244
+ if name in ("", ".") or name == "manifest.json":
245
+ raise ValueError(f"{path} cannot become a source-bundle member")
246
+ return name
247
+
248
+
249
+ def _add_file(members: dict[str, bytes], root: Path, path: Path) -> None:
250
+ path = _plain_file(path, "included path")
251
+ name = _relative_name(root, path)
252
+ if name in members:
253
+ raise ValueError(f"source bundle member is included more than once: {name}")
254
+ members[name] = path.read_bytes()
255
+ source_size = sum(len(value) for value in members.values())
256
+ if source_size > _MAX_BUNDLE_BYTES:
257
+ raise ValueError(f"source members are {source_size} bytes; the limit is {_MAX_BUNDLE_BYTES} bytes")
258
+
259
+
260
+ def _add_path(members: dict[str, bytes], root: Path, path: Path) -> None:
261
+ if path.is_symlink():
262
+ raise ValueError(f"included path may not be a symbolic link: {path}")
263
+ resolved = path.resolve()
264
+ _relative_name(root, resolved)
265
+ if resolved.is_file():
266
+ _add_file(members, root, resolved)
267
+ return
268
+ if not resolved.is_dir():
269
+ raise ValueError(f"included path is not a regular file or directory: {path}")
270
+ for directory, directory_names, file_names in os.walk(resolved, followlinks=False):
271
+ base = Path(directory)
272
+ for name in directory_names:
273
+ child = base / name
274
+ if child.is_symlink():
275
+ raise ValueError(f"included path may not contain a symbolic link: {child}")
276
+ for name in file_names:
277
+ _add_file(members, root, base / name)
278
+
279
+
280
+ def _check_bundle(bundle: bytes, *, api_key: str | None, endpoint: str | None) -> ProgramFacts:
281
+ """Import the exact ZIP in a fresh child with no user environment."""
282
+ timeout = _check_timeout()
283
+ with tempfile.TemporaryDirectory(prefix="cmpnd-source-check-") as directory:
284
+ bundle_path = Path(directory) / "source.zip"
285
+ bundle_path.write_bytes(bundle)
286
+ request = json.dumps({"bundle": str(bundle_path), "api_key": api_key, "endpoint": endpoint})
287
+ environment = {
288
+ key: value
289
+ for key, value in os.environ.items()
290
+ if key in {"PATH", "SYSTEMROOT", "TMPDIR", "TEMP", "TMP", "LANG", "LC_ALL"}
291
+ }
292
+ environment.update({"PYTHONNOUSERSITE": "1", "PYTHONDONTWRITEBYTECODE": "1"})
293
+ try:
294
+ checked = subprocess.run(
295
+ [sys.executable, "-m", "cmpnd._authored_import"],
296
+ input=request,
297
+ text=True,
298
+ capture_output=True,
299
+ env=environment,
300
+ timeout=timeout,
301
+ check=False,
302
+ )
303
+ except subprocess.TimeoutExpired as exc:
304
+ raise SourceCheckError(
305
+ f"the authored program did not finish importing within {timeout:g}s. "
306
+ f"Raise {_CHECK_TIMEOUT_ENV} if module-scope setup needs longer."
307
+ ) from exc
308
+ try:
309
+ answer = json.loads(checked.stdout)
310
+ except json.JSONDecodeError as exc:
311
+ detail = checked.stderr.strip() or "the import checker returned no readable answer"
312
+ raise SourceCheckError(detail) from exc
313
+ if checked.returncode != 0 or answer.get("error"):
314
+ raise SourceCheckError(str(answer.get("error") or checked.stderr.strip()))
315
+ return ProgramFacts.from_json(answer["facts"])
316
+
317
+
318
+ def _check_timeout() -> float:
319
+ raw = os.environ.get(_CHECK_TIMEOUT_ENV)
320
+ if raw is None:
321
+ return _CHECK_TIMEOUT_SECONDS
322
+ try:
323
+ timeout = float(raw)
324
+ except ValueError as exc:
325
+ raise ValueError(f"{_CHECK_TIMEOUT_ENV} must be a positive number of seconds") from exc
326
+ if not math.isfinite(timeout) or timeout <= 0:
327
+ raise ValueError(f"{_CHECK_TIMEOUT_ENV} must be a positive number of seconds")
328
+ return timeout