cmpnd 0.18.2__tar.gz → 0.19.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 (299) hide show
  1. {cmpnd-0.18.2 → cmpnd-0.19.0}/PKG-INFO +1 -1
  2. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/__init__.py +5 -0
  3. cmpnd-0.18.2/cmpnd/cli/api_client.py → cmpnd-0.19.0/cmpnd/_http.py +25 -31
  4. cmpnd-0.19.0/cmpnd/_timespec.py +45 -0
  5. cmpnd-0.19.0/cmpnd/cli/api_client.py +42 -0
  6. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/traces.py +3 -37
  7. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/optimization.py +25 -2
  8. cmpnd-0.19.0/cmpnd/traces.py +136 -0
  9. cmpnd-0.19.0/docs/reading-traces.md +77 -0
  10. {cmpnd-0.18.2 → cmpnd-0.19.0}/pyproject.toml +1 -1
  11. cmpnd-0.19.0/tests/e2e/test_traces_read.py +41 -0
  12. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_deployments.py +1 -1
  13. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize_budget.py +36 -0
  14. cmpnd-0.19.0/tests/test_traces_read.py +163 -0
  15. {cmpnd-0.18.2 → cmpnd-0.19.0}/uv.lock +1 -1
  16. {cmpnd-0.18.2 → cmpnd-0.19.0}/.gitignore +0 -0
  17. {cmpnd-0.18.2 → cmpnd-0.19.0}/CLAUDE.md +0 -0
  18. {cmpnd-0.18.2 → cmpnd-0.19.0}/README.md +0 -0
  19. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/_gepa_patch.py +0 -0
  20. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/_program_patch.py +0 -0
  21. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/_rlm_patch.py +0 -0
  22. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/callback.py +0 -0
  23. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/__init__.py +0 -0
  24. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/__init__.py +0 -0
  25. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/admin.py +0 -0
  26. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/auth.py +0 -0
  27. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/datasets.py +0 -0
  28. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/deployments.py +0 -0
  29. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/evals.py +0 -0
  30. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/login.py +0 -0
  31. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/optimizations.py +0 -0
  32. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/org.py +0 -0
  33. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/programs.py +0 -0
  34. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  35. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/signatures.py +0 -0
  36. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  37. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/top.py +0 -0
  38. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/credentials.py +0 -0
  39. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/deprecation.py +0 -0
  40. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/pagination.py +0 -0
  41. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/shell.py +0 -0
  42. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/trace_render.py +0 -0
  43. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/verbs.py +0 -0
  44. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/configuration.py +0 -0
  45. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/context.py +0 -0
  46. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/dataset_sync.py +0 -0
  47. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/datasets.py +0 -0
  48. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/decorators.py +0 -0
  49. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/deployment.py +0 -0
  50. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/eval_handler.py +0 -0
  51. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/execution.py +0 -0
  52. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/exporter.py +0 -0
  53. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/exporter_http.py +0 -0
  54. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/helpers.py +0 -0
  55. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/identity.py +0 -0
  56. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/imaging.py +0 -0
  57. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/__init__.py +0 -0
  58. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/decode.py +0 -0
  59. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/encode.py +0 -0
  60. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/hash.py +0 -0
  61. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/program.py +0 -0
  62. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/reflection.py +0 -0
  63. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/types.py +0 -0
  64. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/xxh64.py +0 -0
  65. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/mcp_servers.py +0 -0
  66. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/models.py +0 -0
  67. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/optimizers/__init__.py +0 -0
  68. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/optimizers/gepa.py +0 -0
  69. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  70. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/optimizers/resume.py +0 -0
  71. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/optimizers/tracker.py +0 -0
  72. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/packaging.py +0 -0
  73. {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/retry.py +0 -0
  74. {cmpnd-0.18.2 → cmpnd-0.19.0}/docs/2026-04-02-code-review.md +0 -0
  75. {cmpnd-0.18.2 → cmpnd-0.19.0}/docs/cli.md +0 -0
  76. {cmpnd-0.18.2 → cmpnd-0.19.0}/docs/custom-proposer.md +0 -0
  77. {cmpnd-0.18.2 → cmpnd-0.19.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  78. {cmpnd-0.18.2 → cmpnd-0.19.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  79. {cmpnd-0.18.2 → cmpnd-0.19.0}/docs/sdk-instrumentation.md +0 -0
  80. {cmpnd-0.18.2 → cmpnd-0.19.0}/examples/generalizing_proposer.py +0 -0
  81. {cmpnd-0.18.2 → cmpnd-0.19.0}/examples/local_ollama.py +0 -0
  82. {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/LINEAGE.md +0 -0
  83. {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/README.md +0 -0
  84. {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  85. {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  86. {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  87. {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  88. {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/py.typed +0 -0
  89. {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  90. {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/retry.py +0 -0
  91. {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/pyproject.toml +0 -0
  92. {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/audit-sdist.sh +0 -0
  93. {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/capture_review_run.py +0 -0
  94. {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/check_release_artifacts.py +0 -0
  95. {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/fixtures/review_run.json.gz +0 -0
  96. {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/gepa_resume_bug.py +0 -0
  97. {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/require_version_bump.py +0 -0
  98. {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/seed_review_run.py +0 -0
  99. {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/test-in-linux.sh +0 -0
  100. {cmpnd-0.18.2 → cmpnd-0.19.0}/stubs/dspy/__init__.pyi +0 -0
  101. {cmpnd-0.18.2 → cmpnd-0.19.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  102. {cmpnd-0.18.2 → cmpnd-0.19.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  103. {cmpnd-0.18.2 → cmpnd-0.19.0}/stubs/dspy/utils/__init__.pyi +0 -0
  104. {cmpnd-0.18.2 → cmpnd-0.19.0}/stubs/dspy/utils/callback.pyi +0 -0
  105. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/__init__.py +0 -0
  106. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/_exporter_helpers.py +0 -0
  107. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/_fake_clock.py +0 -0
  108. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/_identity_helpers.py +0 -0
  109. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/conftest.py +0 -0
  110. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/__init__.py +0 -0
  111. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/committee_task.py +0 -0
  112. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/conftest.py +0 -0
  113. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  114. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  115. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/fake_lm.py +0 -0
  116. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/helpers.py +0 -0
  117. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/target_capabilities.py +0 -0
  118. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_capture_prompts.py +0 -0
  119. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_composite_module.py +0 -0
  120. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  121. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_deploy_execute.py +0 -0
  122. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_deploy_programs.py +0 -0
  123. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_edge_cases.py +0 -0
  124. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_example_hash.py +0 -0
  125. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_gepa_optimization.py +0 -0
  126. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_interpreter_spans.py +0 -0
  127. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_lm_accuracy.py +0 -0
  128. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_module_tracing.py +0 -0
  129. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_optimization_lineage.py +0 -0
  130. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  131. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_optimize_failures.py +0 -0
  132. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_optimize_loop.py +0 -0
  133. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  134. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  135. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_parallel_export.py +0 -0
  136. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_project_attribution.py +0 -0
  137. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_react_tool_spans.py +0 -0
  138. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_save_load.py +0 -0
  139. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_trace_structure.py +0 -0
  140. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_user_attribution.py +0 -0
  141. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  142. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  143. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  144. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  145. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  146. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  147. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  148. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  149. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  150. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/10_react.json +0 -0
  151. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/11_refine.json +0 -0
  152. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/12_retrieve.json +0 -0
  153. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/13_knn.json +0 -0
  154. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/14_parallel.json +0 -0
  155. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/README.md +0 -0
  156. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/ingest_conformance.py +0 -0
  157. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/__init__.py +0 -0
  158. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/conftest.py +0 -0
  159. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/hosted_optimize_probe.py +0 -0
  160. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/optimization_run_probe.py +0 -0
  161. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  162. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/test_logs_renders_traces.py +0 -0
  163. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/test_optimization_run.py +0 -0
  164. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/test_optimizing_progress.py +0 -0
  165. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/test_unhappy_path.py +0 -0
  166. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/__init__.py +0 -0
  167. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/__main__.py +0 -0
  168. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/drain.py +0 -0
  169. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/ingest.py +0 -0
  170. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/latency_transport.py +0 -0
  171. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/rlm_shape.py +0 -0
  172. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/sweep.py +0 -0
  173. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/test_rlm_shape.py +0 -0
  174. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/__init__.py +0 -0
  175. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/conftest.py +0 -0
  176. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/seed_parity.py +0 -0
  177. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_auth.py +0 -0
  178. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_datasets.py +0 -0
  179. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_deployments.py +0 -0
  180. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_evals.py +0 -0
  181. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_health.py +0 -0
  182. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_modules.py +0 -0
  183. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_optimizations.py +0 -0
  184. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_programs.py +0 -0
  185. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_signatures.py +0 -0
  186. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_trace_streaming.py +0 -0
  187. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_traces.py +0 -0
  188. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/__init__.py +0 -0
  189. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/_provider.py +0 -0
  190. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/_reliability.py +0 -0
  191. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/_result.py +0 -0
  192. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/mint_bedrock_token.py +0 -0
  193. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_deploy_programs.py +0 -0
  194. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_multi_client_stress.py +0 -0
  195. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_optimize_client_committee.py +0 -0
  196. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_optimize_loop.py +0 -0
  197. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  198. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_optimize_stress.py +0 -0
  199. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_provider_roles.py +0 -0
  200. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_real_provider_twins.py +0 -0
  201. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_reliability.py +0 -0
  202. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_reliability_classify.py +0 -0
  203. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_rlm_wasm.py +0 -0
  204. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_trace_search.py +0 -0
  205. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_callback.py +0 -0
  206. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_candidate_lineage.py +0 -0
  207. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/__init__.py +0 -0
  208. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/conftest.py +0 -0
  209. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_admin.py +0 -0
  210. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_api_client.py +0 -0
  211. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_auth.py +0 -0
  212. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_credentials.py +0 -0
  213. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_datasets.py +0 -0
  214. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_evals.py +0 -0
  215. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_login.py +0 -0
  216. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_main.py +0 -0
  217. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_optimizations.py +0 -0
  218. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_org.py +0 -0
  219. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_pagination.py +0 -0
  220. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_parity.py +0 -0
  221. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_programs.py +0 -0
  222. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_provider_credentials.py +0 -0
  223. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_schema_sample.py +0 -0
  224. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_shell.py +0 -0
  225. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_signatures.py +0 -0
  226. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_tool_servers.py +0 -0
  227. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_top.py +0 -0
  228. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_traces.py +0 -0
  229. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  230. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_clustering/__init__.py +0 -0
  231. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_clustering/conftest.py +0 -0
  232. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_clustering/test_edge_cases.py +0 -0
  233. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  234. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_clustering/test_type_conversion.py +0 -0
  235. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cmp551_grouping_identity.py +0 -0
  236. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_configuration.py +0 -0
  237. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_context.py +0 -0
  238. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_context_propagation.py +0 -0
  239. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_dataset_polling.py +0 -0
  240. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_dataset_sync.py +0 -0
  241. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_datasets.py +0 -0
  242. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_decorators.py +0 -0
  243. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_deploy.py +0 -0
  244. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  245. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_eval.py +0 -0
  246. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_eval_start.py +0 -0
  247. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_eval_trace_grouping.py +0 -0
  248. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_exception_paths.py +0 -0
  249. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_execute.py +0 -0
  250. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_exporter.py +0 -0
  251. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_exporter_queue.py +0 -0
  252. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_exporter_real.py +0 -0
  253. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_fake_lm.py +0 -0
  254. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_gepa_callback.py +0 -0
  255. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_gepa_integration.py +0 -0
  256. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_gepa_resume.py +0 -0
  257. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_identity.py +0 -0
  258. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_imaging.py +0 -0
  259. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_import_weight.py +0 -0
  260. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_ingest_conformance.py +0 -0
  261. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_ingest_second_producer.py +0 -0
  262. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_integration.py +0 -0
  263. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_interpreter_callback.py +0 -0
  264. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_ir_encoder_properties.py +0 -0
  265. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_ir_golden_vectors.py +0 -0
  266. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_ir_roundtrip.py +0 -0
  267. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_mcp_tools.py +0 -0
  268. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_models.py +0 -0
  269. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimization_progress.py +0 -0
  270. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimization_start.py +0 -0
  271. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize.py +0 -0
  272. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize_lift.py +0 -0
  273. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize_lms.py +0 -0
  274. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize_proposer.py +0 -0
  275. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize_reflection_lm.py +0 -0
  276. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize_terminal_failure.py +0 -0
  277. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_packaging.py +0 -0
  278. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_packaging_callable_source.py +0 -0
  279. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_packaging_mcp_tools.py +0 -0
  280. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_packaging_program_source.py +0 -0
  281. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_poll_until.py +0 -0
  282. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_program_lineage_persist.py +0 -0
  283. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_project_payload.py +0 -0
  284. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_reflector_properties.py +0 -0
  285. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_reflector_smoke.py +0 -0
  286. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_repro_reactv2_bugs.py +0 -0
  287. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_require_version_bump.py +0 -0
  288. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_retry.py +0 -0
  289. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_reversible_failures.py +0 -0
  290. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_rlm_patch.py +0 -0
  291. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_schemas.py +0 -0
  292. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_seed_review_run.py +0 -0
  293. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_stats_export.py +0 -0
  294. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_trace_render.py +0 -0
  295. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_tracked_gepa.py +0 -0
  296. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_tracker_enrich.py +0 -0
  297. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_truncation.py +0 -0
  298. {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_xxh64.py +0 -0
  299. {cmpnd-0.18.2 → cmpnd-0.19.0}/todo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.18.2
3
+ Version: 0.19.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>
@@ -14,6 +14,8 @@ Usage:
14
14
 
15
15
  import importlib.metadata
16
16
 
17
+ from . import traces
18
+ from ._http import ApiError
17
19
  from .callback import CmpndCallback, trace_id
18
20
  from .configuration import configure, get_config, require_config
19
21
  from .context import (
@@ -67,6 +69,9 @@ __all__ = [
67
69
  # Cleanup
68
70
  "flush_exporter",
69
71
  "shutdown_exporter",
72
+ # Programmatic backend reads (cmpnd.traces.list / cmpnd.traces.show)
73
+ "traces",
74
+ "ApiError",
70
75
  # Types
71
76
  "SpanType",
72
77
  "TraceStatus",
@@ -1,4 +1,12 @@
1
- """HTTP client for the cmpnd CLI."""
1
+ """Shared HTTP transport for the cmpnd backend API.
2
+
3
+ Two surfaces reach the backend: the CLI (``cmpnd.cli.api_client``) and the
4
+ programmatic library client (``cmpnd.Client``). Both send every request through
5
+ the one transport here, so they cannot drift on base-URL handling, the API-key
6
+ header, the timeout, or the error ladder. The CLI keeps its own credential
7
+ resolution and argparse glue on top; the library client keeps its own config
8
+ resolution; this module is only the wire.
9
+ """
2
10
 
3
11
  from __future__ import annotations
4
12
 
@@ -7,37 +15,23 @@ from typing import Any
7
15
 
8
16
  import httpx
9
17
 
10
- from . import credentials
11
18
 
19
+ class ApiError(Exception):
20
+ """An error from a cmpnd backend request.
12
21
 
13
- class CliError(Exception):
14
- """CLI error with an exit code."""
22
+ ``status_code`` is the HTTP status when the server answered one, or ``None``
23
+ for a transport-level failure (could not connect, timed out). ``exit_code``
24
+ is the process exit the CLI maps the error to; the library client ignores it.
25
+ """
15
26
 
16
- def __init__(self, message: str, exit_code: int = 1):
27
+ def __init__(self, message: str, exit_code: int = 1, *, status_code: int | None = None):
17
28
  super().__init__(message)
18
29
  self.exit_code = exit_code
19
-
20
-
21
- def resolve_endpoint(args: Any) -> str:
22
- return getattr(args, "endpoint", None) or os.getenv("CMPND_ENDPOINT", "https://platform.cmpnd.ai")
23
-
24
-
25
- def resolve_config(args: Any) -> tuple[str, str]:
26
- """Resolve API key and endpoint.
27
-
28
- Order: ``--api-key`` flag → ``CMPND_API_KEY`` env → stored credentials
29
- (keyed by endpoint). The stored-credentials fallback is what makes
30
- ``cmpnd login`` stick across shell sessions.
31
- """
32
- endpoint = resolve_endpoint(args)
33
- api_key = getattr(args, "api_key", None) or os.getenv("CMPND_API_KEY") or credentials.load(endpoint)
34
- if not api_key:
35
- raise CliError("Not logged in. Run `cmpnd login`, pass --api-key, or set CMPND_API_KEY.")
36
- return api_key, endpoint
30
+ self.status_code = status_code
37
31
 
38
32
 
39
33
  class ApiClient:
40
- """Thin HTTP client for the cmpnd observe API."""
34
+ """Thin HTTP client for the cmpnd backend API."""
41
35
 
42
36
  def __init__(self, api_key: str, endpoint: str):
43
37
  self._api_key = api_key
@@ -89,7 +83,7 @@ class ApiClient:
89
83
  try:
90
84
  timeout = float(raw_timeout)
91
85
  except ValueError:
92
- raise CliError(f"Invalid CMPND_HTTP_TIMEOUT={raw_timeout!r}; expected a number of seconds.")
86
+ raise ApiError(f"Invalid CMPND_HTTP_TIMEOUT={raw_timeout!r}; expected a number of seconds.")
93
87
  try:
94
88
  with httpx.Client(
95
89
  headers={"X-API-Key": self._api_key, "Content-Type": "application/json"},
@@ -110,14 +104,14 @@ class ApiClient:
110
104
  else:
111
105
  response = client.post(url, json=json)
112
106
  except httpx.ConnectError:
113
- raise CliError(f"Could not connect to {self._endpoint}. Check CMPND_ENDPOINT.")
107
+ raise ApiError(f"Could not connect to {self._endpoint}. Check CMPND_ENDPOINT.")
114
108
  except httpx.TimeoutException:
115
- raise CliError(f"Request timed out connecting to {self._endpoint}.")
109
+ raise ApiError(f"Request timed out connecting to {self._endpoint}.")
116
110
 
117
111
  if response.status_code == 401:
118
- raise CliError("Invalid or missing API key. Check CMPND_API_KEY or --api-key.")
112
+ raise ApiError("Invalid or missing API key. Check CMPND_API_KEY or --api-key.", status_code=401)
119
113
  if response.status_code == 404:
120
- raise CliError("Resource not found.")
114
+ raise ApiError("Resource not found.", status_code=404)
121
115
  if response.status_code >= 500:
122
116
  # 5xx usually means the server is up but blew up handling the
123
117
  # request — a database hiccup, an unhandled exception, etc.
@@ -129,13 +123,13 @@ class ApiClient:
129
123
  if body_excerpt:
130
124
  msg += f": {body_excerpt}"
131
125
  msg += "\n If you operate this server, check its logs for the underlying exception."
132
- raise CliError(msg, exit_code=2)
126
+ raise ApiError(msg, exit_code=2, status_code=response.status_code)
133
127
  if response.status_code >= 400:
134
128
  try:
135
129
  detail = response.json()
136
130
  except Exception:
137
131
  detail = response.text
138
- raise CliError(f"Request failed ({response.status_code}): {detail}")
132
+ raise ApiError(f"Request failed ({response.status_code}): {detail}", status_code=response.status_code)
139
133
 
140
134
  # A non-JSON route hands its body back verbatim. Checked before the
141
135
  # empty-body arm so such a route answers "" rather than None, which is
@@ -0,0 +1,45 @@
1
+ """Search time-bound sugar, shared by the CLI and the library read client.
2
+
3
+ A search lower/upper bound may be written three ways: an ISO 8601 timestamp, a
4
+ bare date (``YYYY-MM-DD``), or a duration back from now (``30d``, ``2w``,
5
+ ``12h``, ``90m``). Both surfaces resolve it the same way here, so ``--since 30d``
6
+ on the command line and ``since="30d"`` in the library mean one thing. The sugar
7
+ lives at the client edge rather than on the wire, so the API stays timestamp-only
8
+ — one grammar for every client, resolved once at the edge that knows what "now"
9
+ means to the user.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ from datetime import datetime, timedelta, timezone
16
+
17
+ _RELATIVE_TIME = re.compile(r"^(\d+)([mhdw])$")
18
+ _BARE_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
19
+
20
+ _RELATIVE_UNITS = {
21
+ "m": "minutes",
22
+ "h": "hours",
23
+ "d": "days",
24
+ "w": "weeks",
25
+ }
26
+
27
+
28
+ def resolve_search_time(value: str, *, end_of_day: bool = False) -> str:
29
+ """Resolve a search bound to an ISO 8601 timestamp.
30
+
31
+ A duration (``30d``, ``2w``, ``12h``, ``90m``) means that far back from now;
32
+ a bare date (``YYYY-MM-DD``) resolves to that whole day in UTC — start of day
33
+ normally, end of day (``23:59:59Z``) when ``end_of_day`` is set, so an upper
34
+ bound of ``2026-07-15`` includes all of the 15th; anything else passes through
35
+ for the server to parse.
36
+ """
37
+ text = value.strip()
38
+ if _BARE_DATE.match(text):
39
+ return f"{text}T23:59:59Z" if end_of_day else f"{text}T00:00:00Z"
40
+ m = _RELATIVE_TIME.match(text)
41
+ if not m:
42
+ return value
43
+ amount, unit = int(m.group(1)), m.group(2)
44
+ when = datetime.now(timezone.utc) - timedelta(**{_RELATIVE_UNITS[unit]: amount})
45
+ return when.replace(microsecond=0).isoformat().replace("+00:00", "Z")
@@ -0,0 +1,42 @@
1
+ """CLI credential resolution over the shared HTTP transport.
2
+
3
+ The transport itself (``ApiClient``) and its error (``ApiError``) live in
4
+ ``cmpnd._http`` so the CLI and the programmatic library client share one wire.
5
+ This module adds the CLI-only pieces on top: reading the endpoint and API key
6
+ from ``--api-key`` / ``--endpoint`` flags, the environment, and the stored login
7
+ credentials. ``CliError`` is the historical CLI name for the shared error and
8
+ stays as an alias so existing handlers keep raising and catching it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from typing import Any
15
+
16
+ from .. import _http
17
+ from . import credentials
18
+
19
+ # The shared transport and its error, re-exported under the names the CLI has
20
+ # always used. `CliError` is `ApiError` — the CLI names it for the process exit
21
+ # it carries; both are the same class.
22
+ ApiClient = _http.ApiClient
23
+ ApiError = _http.ApiError
24
+ CliError = _http.ApiError
25
+
26
+
27
+ def resolve_endpoint(args: Any) -> str:
28
+ return getattr(args, "endpoint", None) or os.getenv("CMPND_ENDPOINT", "https://platform.cmpnd.ai")
29
+
30
+
31
+ def resolve_config(args: Any) -> tuple[str, str]:
32
+ """Resolve API key and endpoint.
33
+
34
+ Order: ``--api-key`` flag → ``CMPND_API_KEY`` env → stored credentials
35
+ (keyed by endpoint). The stored-credentials fallback is what makes
36
+ ``cmpnd login`` stick across shell sessions.
37
+ """
38
+ endpoint = resolve_endpoint(args)
39
+ api_key = getattr(args, "api_key", None) or os.getenv("CMPND_API_KEY") or credentials.load(endpoint)
40
+ if not api_key:
41
+ raise CliError("Not logged in. Run `cmpnd login`, pass --api-key, or set CMPND_API_KEY.")
42
+ return api_key, endpoint
@@ -3,13 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- import re
7
6
  import sys
8
7
  import time
9
8
  from argparse import ArgumentParser, ArgumentTypeError, _SubParsersAction
10
- from datetime import datetime, timedelta, timezone
11
9
  from typing import Any
12
10
 
11
+ from ... import _timespec
13
12
  from .. import api_client, deprecation, pagination, trace_render
14
13
 
15
14
 
@@ -161,39 +160,6 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
161
160
  follow.set_defaults(func=handle_follow)
162
161
 
163
162
 
164
- _RELATIVE_TIME = re.compile(r"^(\d+)([mhdw])$")
165
- _BARE_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
166
-
167
- _RELATIVE_UNITS = {
168
- "m": "minutes",
169
- "h": "hours",
170
- "d": "days",
171
- "w": "weeks",
172
- }
173
-
174
-
175
- def resolve_search_time(value: str, *, end_of_day: bool = False) -> str:
176
- """Resolve a search bound to an ISO 8601 timestamp.
177
-
178
- A duration (``30d``, ``2w``, ``12h``, ``90m``) means that far back from now;
179
- a bare date (``YYYY-MM-DD``) resolves to that whole day in UTC — start of day
180
- normally, end of day (``23:59:59Z``) when ``end_of_day`` is set, so an upper
181
- bound of ``2026-07-15`` includes all of the 15th; anything else passes through
182
- for the server to parse. The sugar lives here rather than in the API so the
183
- wire stays timestamp-only — one grammar for every client, resolved once at the
184
- edge that knows what "now" means to the user.
185
- """
186
- text = value.strip()
187
- if _BARE_DATE.match(text):
188
- return f"{text}T23:59:59Z" if end_of_day else f"{text}T00:00:00Z"
189
- m = _RELATIVE_TIME.match(text)
190
- if not m:
191
- return value
192
- amount, unit = int(m.group(1)), m.group(2)
193
- when = datetime.now(timezone.utc) - timedelta(**{_RELATIVE_UNITS[unit]: amount})
194
- return when.replace(microsecond=0).isoformat().replace("+00:00", "Z")
195
-
196
-
197
163
  def handle_list(client: api_client.ApiClient, args: Any) -> Any:
198
164
  max_window = bool(getattr(args, "max_window", False))
199
165
  if max_window and (getattr(args, "start_time_from", None) or getattr(args, "start_time_to", None)):
@@ -234,9 +200,9 @@ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
234
200
  # The lower bound resolves to the start of a bare date, the upper bound to
235
201
  # the end of it, so `--until 2026-07-15` includes all of the 15th.
236
202
  if "start_time_from" in body:
237
- body["start_time_from"] = resolve_search_time(str(body["start_time_from"]))
203
+ body["start_time_from"] = _timespec.resolve_search_time(str(body["start_time_from"]))
238
204
  if "start_time_to" in body:
239
- body["start_time_to"] = resolve_search_time(str(body["start_time_to"]), end_of_day=True)
205
+ body["start_time_to"] = _timespec.resolve_search_time(str(body["start_time_to"]), end_of_day=True)
240
206
  resp = client.post("/api/v1/traces/search", json=body)
241
207
 
242
208
  # A window the caller did not ask for has to be visible: read as an org
@@ -704,6 +704,7 @@ def optimize(
704
704
  lms: dict[str, Any] | None = None,
705
705
  max_metric_calls: int | None = None,
706
706
  auto: str | None = None,
707
+ may_author_code: bool | None = None,
707
708
  max_steps: int = 10,
708
709
  patience: int = 2,
709
710
  seed: int = 0,
@@ -842,6 +843,16 @@ def optimize(
842
843
  ``"heavy"`` — sized from the program's predictor count and the
843
844
  valset, using GEPA's own settings. Easier to reach for than a call
844
845
  count when you do not yet know what a run costs.
846
+ may_author_code: Whether this run may rewrite the SOURCE of the program's
847
+ ``dspy.Flex`` submodules, rather than only their instructions. Left
848
+ unset, the platform's answer is yes — writing a ``dspy.Flex`` is
849
+ already asking for its source to be optimized.
850
+
851
+ Pass ``False`` for "improve my prompts, don't rewrite my code" on a
852
+ program that has both: a cheaper, lower-variance search that still
853
+ optimizes every ordinary predictor and leaves each ``dspy.Flex`` at
854
+ the source you deployed. A program with no ``dspy.Flex`` in it is
855
+ unaffected either way.
845
856
  max_steps: How many times to re-optimize the winner, each step a fresh
846
857
  ``optimize`` call with an advanced seed (default 10). Distinct from
847
858
  the budget above, which bounds ONE step's search: total spend is
@@ -901,7 +912,14 @@ def optimize(
901
912
  step_config["seed"] = seed
902
913
 
903
914
  initiated = _initiate_step(
904
- client, cfg.endpoint, headers, current_deployment_id, optimizer, data, step_config
915
+ client,
916
+ cfg.endpoint,
917
+ headers,
918
+ current_deployment_id,
919
+ optimizer,
920
+ data,
921
+ step_config,
922
+ may_author_code,
905
923
  )
906
924
  optimization_id = initiated["optimization_id"]
907
925
 
@@ -1040,6 +1058,7 @@ def _initiate_step(
1040
1058
  optimizer: str,
1041
1059
  data: str,
1042
1060
  config: dict[str, Any],
1061
+ may_author_code: bool | None = None,
1043
1062
  ) -> dict[str, Any]:
1044
1063
  """POST one optimize step to serve; return the 201 body. Raises
1045
1064
  OptimizeError carrying the server's error on any non-2xx (e.g. 409/422).
@@ -1058,7 +1077,11 @@ def _initiate_step(
1058
1077
  resp = _request(
1059
1078
  lambda: client.post(
1060
1079
  f"{endpoint}/api/v1/deployments/{deployment_id}/optimize/{optimizer}",
1061
- json={"data": data, "config": config},
1080
+ json=(
1081
+ {"data": data, "config": config}
1082
+ if may_author_code is None
1083
+ else {"data": data, "config": config, "may_author_code": may_author_code}
1084
+ ),
1062
1085
  headers=headers,
1063
1086
  ),
1064
1087
  what,
@@ -0,0 +1,136 @@
1
+ """Programmatic trace reads for the cmpnd backend.
2
+
3
+ ``import cmpnd`` already emits traces; this module reads them back. ``cmpnd.traces``
4
+ reaches the same two backend reads the CLI does — ``list`` and ``show`` — over the
5
+ same HTTP transport, so the library and the CLI cannot drift. It follows the SDK's
6
+ existing shape: module-level verbs over the credentials ``cmpnd.configure()`` set,
7
+ like ``cmpnd.deploy`` / ``cmpnd.execute`` / ``cmpnd.load_dataset``.
8
+
9
+ >>> import cmpnd
10
+ >>> cmpnd.configure(api_key="ct_xxx")
11
+ >>> page = cmpnd.traces.list(status="error", since="30d", limit=10)
12
+ >>> for row in page["traces"]:
13
+ ... detail = cmpnd.traces.show(row["trace_id"])
14
+
15
+ The verbs and filter names match the ``cmpnd traces`` CLI vocabulary — ``list`` and
16
+ ``show``, filtered by ``query``, ``program``, ``hash``, ``deployment``, ``since`` /
17
+ ``until``, ``all_history`` — so a user moves between the two surfaces without
18
+ relearning names. (The CLI's ``traces search`` spelling is a deprecated alias of
19
+ ``traces list``; this surface offers only ``list``.)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ from typing import Any
26
+
27
+ from . import _http, _timespec, configuration
28
+
29
+ DEFAULT_ENDPOINT = "https://platform.cmpnd.ai"
30
+
31
+ # The traces route paginates by page number; these verbs offer the friendlier
32
+ # limit/offset pair and map it, matching how the CLI presents pagination.
33
+ _DEFAULT_LIMIT = 50
34
+
35
+
36
+ def list(
37
+ *,
38
+ status: str | None = None,
39
+ query: str | None = None,
40
+ model: str | None = None,
41
+ program: str | None = None,
42
+ hash: str | None = None,
43
+ deployment: str | None = None,
44
+ metadata: dict[str, str] | None = None,
45
+ tags: list[str] | None = None,
46
+ since: str | None = None,
47
+ until: str | None = None,
48
+ all_history: bool = False,
49
+ limit: int = _DEFAULT_LIMIT,
50
+ offset: int = 0,
51
+ api_key: str | None = None,
52
+ endpoint: str | None = None,
53
+ ) -> dict[str, Any]:
54
+ """List and search traces, newest first.
55
+
56
+ Returns the raw response: ``{"traces": [...], "total": N, ...}``. It also
57
+ carries ``default_window_applied`` and ``start_time_from`` when no time filter
58
+ was given, so the caller can tell an org-wide total from a windowed one, and
59
+ ``ignored_fields`` naming any request field an older server did not understand.
60
+
61
+ The filters mirror ``cmpnd traces list``: ``query`` searches indexed span text;
62
+ ``program`` / ``hash`` name the program by name or signature hash;
63
+ ``deployment`` is a deployment id; ``metadata`` is matched by containment (every
64
+ pair must be present); ``tags`` matches any of the given tags. ``since`` /
65
+ ``until`` accept an ISO 8601 timestamp, a bare date (``YYYY-MM-DD``), or a
66
+ duration back from now (``30d``, ``2w``, ``12h``, ``90m``), resolved the same
67
+ way the CLI resolves ``--since`` / ``--until``. An upper bound needs a lower
68
+ one, and ``all_history`` (the CLI's ``--all``) cannot combine with either bound
69
+ — the backend enforces both.
70
+
71
+ ``api_key`` / ``endpoint`` override the configured credentials for this call.
72
+ """
73
+ body: dict[str, Any] = _pagination(limit, offset)
74
+ if all_history:
75
+ body["max_window"] = True
76
+ for wire_key, val in (
77
+ ("status", status),
78
+ ("q", query),
79
+ ("model", model),
80
+ ("program_name", program),
81
+ ("program_signature_hash", hash),
82
+ ("deployment_id", deployment),
83
+ ("tags", tags),
84
+ ):
85
+ if val is not None:
86
+ body[wire_key] = val
87
+ if metadata is not None:
88
+ body["metadata"] = dict(metadata)
89
+ if since is not None:
90
+ body["start_time_from"] = _timespec.resolve_search_time(since)
91
+ if until is not None:
92
+ body["start_time_to"] = _timespec.resolve_search_time(until, end_of_day=True)
93
+ return _client(api_key, endpoint).post("/api/v1/traces/search", json=body)
94
+
95
+
96
+ def show(trace_id: str, *, api_key: str | None = None, endpoint: str | None = None) -> dict[str, Any]:
97
+ """Fetch one trace and its spans: ``{"trace": {...}, "spans": [...]}``.
98
+
99
+ ``api_key`` / ``endpoint`` override the configured credentials for this call.
100
+ """
101
+ return _client(api_key, endpoint).get(f"/api/v1/traces/{trace_id}")
102
+
103
+
104
+ def _client(api_key: str | None, endpoint: str | None) -> _http.ApiClient:
105
+ """Build a transport from the resolved credentials.
106
+
107
+ Resolution order: an explicit ``api_key`` / ``endpoint`` argument, then whatever
108
+ ``cmpnd.configure()`` set, then the ``CMPND_API_KEY`` / ``CMPND_ENDPOINT``
109
+ environment. The endpoint falls back to the public platform when nothing names
110
+ one; a missing API key raises, since no read is possible without one.
111
+ """
112
+ cfg = configuration.get_config()
113
+ resolved_key = api_key or (cfg.api_key if cfg else None) or os.getenv("CMPND_API_KEY")
114
+ if not resolved_key:
115
+ raise _http.ApiError("No API key. Pass api_key=, call cmpnd.configure(api_key=...), or set CMPND_API_KEY.")
116
+ resolved_endpoint = endpoint or (cfg.endpoint if cfg else None) or os.getenv("CMPND_ENDPOINT") or DEFAULT_ENDPOINT
117
+ return _http.ApiClient(resolved_key, resolved_endpoint)
118
+
119
+
120
+ def _pagination(limit: int, offset: int) -> dict[str, Any]:
121
+ """Map the caller's limit/offset onto the route's page/page_size.
122
+
123
+ The route addresses rows by page, so the offset must be a whole multiple of the
124
+ limit — the same rule the CLI's pagination helper enforces.
125
+ """
126
+ if limit <= 0:
127
+ raise _http.ApiError("limit must be positive")
128
+ if offset < 0:
129
+ raise _http.ApiError("offset must not be negative")
130
+ if offset % limit != 0:
131
+ lower = (offset // limit) * limit
132
+ raise _http.ApiError(
133
+ f"offset must be a multiple of limit (the traces route paginates by page); "
134
+ f"try offset {lower} or {lower + limit}"
135
+ )
136
+ return {"page": offset // limit + 1, "page_size": limit}
@@ -0,0 +1,77 @@
1
+ # Reading traces from a program
2
+
3
+ The `cmpnd` SDK emits traces through `configure()` + `auto_instrument()`. It also
4
+ reads them back: `cmpnd.traces` is a programmatic view over the backend's trace
5
+ reads, so a program that builds an eval, a dashboard, or an agent lists and
6
+ fetches its own traces without shelling out to the CLI or hand-rolling httpx.
7
+ It follows the SDK's existing shape — module-level verbs over the credentials
8
+ `cmpnd.configure()` set, like `cmpnd.deploy` / `cmpnd.execute` / `cmpnd.load_dataset`.
9
+
10
+ ## Entry point
11
+
12
+ ```python
13
+ import cmpnd
14
+
15
+ cmpnd.configure(api_key="ct_...") # or set CMPND_API_KEY
16
+
17
+ page = cmpnd.traces.list(status="error", since="30d", limit=10)
18
+ for row in page["traces"]:
19
+ detail = cmpnd.traces.show(row["trace_id"])
20
+ ```
21
+
22
+ Each call resolves its API key and endpoint in this order: an explicit `api_key` /
23
+ `endpoint` argument, then whatever `cmpnd.configure()` set, then the
24
+ `CMPND_API_KEY` / `CMPND_ENDPOINT` environment. The endpoint falls back to the
25
+ public platform when nothing names one; a missing API key raises `cmpnd.ApiError`.
26
+
27
+ ## Verbs and filters match the CLI
28
+
29
+ The verb and argument names are the `cmpnd traces` CLI vocabulary, so a user moves
30
+ between the two surfaces without relearning names. (The CLI's `traces search`
31
+ spelling is a deprecated alias of `traces list`; this surface offers only `list`.)
32
+
33
+ ## `cmpnd.traces.list(...)`
34
+
35
+ Lists and searches traces, newest first, returning the raw response:
36
+ `{"traces": [...], "total": N, ...}`.
37
+
38
+ | Argument | CLI flag | Meaning |
39
+ | -- | -- | -- |
40
+ | `status` | `--status` | `ok`, `error`, `incomplete`, `in_progress` |
41
+ | `query` | `--query` / `-q` | Full-text search over indexed span text (prompts, completions, tool calls, errors) |
42
+ | `model` | `--model` | Exact model name, e.g. `openai/gpt-4o` |
43
+ | `program` | `--program` | Program name |
44
+ | `hash` | `--hash` | Program signature hash, as a string |
45
+ | `deployment` | `--deployment` | Deployment UUID |
46
+ | `metadata` | `--metadata` | `dict` matched by containment — every pair must be present on the trace |
47
+ | `tags` | `--tag` | `list` — matches any of the given project tags |
48
+ | `since` / `until` | `--since` / `--until` | Time bounds (see below) |
49
+ | `all_history` | `--all` | Widen to the route's longest window |
50
+ | `limit` / `offset` | `--limit` / `--offset` | Page size and start; `offset` must be a whole multiple of `limit` |
51
+
52
+ `since` / `until` accept an ISO 8601 timestamp, a bare date (`YYYY-MM-DD`), or a
53
+ duration back from now (`30d`, `2w`, `12h`, `90m`) — resolved by the same code the
54
+ CLI's `--since` / `--until` use. An upper bound needs a lower one, and
55
+ `all_history` cannot combine with either bound; the backend enforces both.
56
+
57
+ When no time filter is given the response carries `default_window_applied` and the
58
+ `start_time_from` the server chose, so a caller can tell an org-wide total from a
59
+ windowed one. `ignored_fields` names any request field an older server did not
60
+ understand.
61
+
62
+ `api_key` / `endpoint` keyword arguments override the configured credentials for a
63
+ single call, for a script that reads from more than one place.
64
+
65
+ ## `cmpnd.traces.show(trace_id)`
66
+
67
+ Fetches one trace and its spans: `{"trace": {...}, "spans": [...]}`. A missing
68
+ trace raises `cmpnd.ApiError`.
69
+
70
+ ## One transport, two surfaces
71
+
72
+ `cmpnd.traces` and the `cmpnd` CLI send every request through the same HTTP
73
+ transport (`cmpnd._http.ApiClient`), so the library and the CLI cannot drift on
74
+ base-URL handling, the API-key header, the timeout, or the error ladder. The CLI
75
+ adds its own credential resolution (`--api-key` / `--endpoint` flags, stored
76
+ login) on top; the `cmpnd traces list` / `cmpnd traces show` commands reach the
77
+ same two routes these functions do.
@@ -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.18.2"
8
+ version = "0.19.0"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -0,0 +1,41 @@
1
+ """End-to-end tests for the programmatic trace reads against a live server.
2
+
3
+ Drives ``cmpnd.traces`` — the library surface a user reaches through
4
+ ``import cmpnd`` — against a real backend: run a program, then list and show the
5
+ trace it produced. This is the level that catches a library↔API contract
6
+ mismatch a mocked transport cannot. Skipped unless CMPND_E2E_API_KEY is set
7
+ (see conftest).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import dspy
13
+ import pytest
14
+
15
+ import cmpnd
16
+ from tests.e2e.conftest import CMPND_E2E_API_KEY, E2E_ENDPOINT, flush_and_wait
17
+
18
+ pytestmark = pytest.mark.e2e
19
+
20
+
21
+ class TestTracesReadE2E:
22
+ def test_list_and_show_round_trip(self, server_client, trace_capture):
23
+ dspy.Predict("question -> answer")(question="Who represents District 4?")
24
+ flush_and_wait()
25
+ trace_id = trace_capture.last_trace_id
26
+ assert trace_id is not None, "no trace captured"
27
+ program_hash = server_client.get_trace(trace_id)["trace"]["program_signature_hash"]
28
+
29
+ # list by the program that produced it finds the trace.
30
+ page = cmpnd.traces.list(hash=str(program_hash), limit=50, api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
31
+ assert trace_id in [t["trace_id"] for t in page["traces"]]
32
+
33
+ # show returns the trace and its spans.
34
+ detail = cmpnd.traces.show(trace_id, api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
35
+ assert detail["trace"]["trace_id"] == trace_id
36
+ assert isinstance(detail["spans"], list)
37
+ assert len(detail["spans"]) >= 1
38
+
39
+ def test_show_missing_trace_raises(self):
40
+ with pytest.raises(cmpnd.ApiError):
41
+ cmpnd.traces.show("00000000-0000-0000-0000-000000000000", api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
@@ -95,7 +95,7 @@ def _transport(response: httpx.Response) -> mock._patch[mock.MagicMock]:
95
95
  """
96
96
  made = mock.MagicMock()
97
97
  made.__enter__.return_value.get.return_value = response
98
- return mock.patch("cmpnd.cli.api_client.httpx.Client", return_value=made)
98
+ return mock.patch("cmpnd._http.httpx.Client", return_value=made)
99
99
 
100
100
 
101
101
  class TestOpenAPI: