cmpnd 0.18.3__tar.gz → 0.20.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 (305) hide show
  1. {cmpnd-0.18.3 → cmpnd-0.20.0}/PKG-INFO +1 -1
  2. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/__init__.py +5 -0
  3. cmpnd-0.18.3/cmpnd/cli/api_client.py → cmpnd-0.20.0/cmpnd/_http.py +25 -31
  4. cmpnd-0.20.0/cmpnd/_timespec.py +45 -0
  5. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/__init__.py +11 -0
  6. cmpnd-0.20.0/cmpnd/cli/api_client.py +42 -0
  7. cmpnd-0.20.0/cmpnd/cli/commands/assessments.py +67 -0
  8. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/traces.py +3 -37
  9. cmpnd-0.20.0/cmpnd/cli/commands/valsets.py +101 -0
  10. cmpnd-0.20.0/cmpnd/traces.py +136 -0
  11. {cmpnd-0.18.3 → cmpnd-0.20.0}/docs/cli.md +47 -0
  12. cmpnd-0.20.0/docs/reading-traces.md +77 -0
  13. {cmpnd-0.18.3 → cmpnd-0.20.0}/pyproject.toml +1 -1
  14. cmpnd-0.20.0/tests/e2e/test_assessments_cli.py +68 -0
  15. cmpnd-0.20.0/tests/e2e/test_traces_read.py +41 -0
  16. cmpnd-0.20.0/tests/e2e/test_valsets_cli.py +102 -0
  17. cmpnd-0.20.0/tests/test_cli/test_assessments.py +84 -0
  18. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_deployments.py +1 -1
  19. cmpnd-0.20.0/tests/test_cli/test_valsets.py +122 -0
  20. cmpnd-0.20.0/tests/test_traces_read.py +163 -0
  21. {cmpnd-0.18.3 → cmpnd-0.20.0}/uv.lock +1 -1
  22. {cmpnd-0.18.3 → cmpnd-0.20.0}/.gitignore +0 -0
  23. {cmpnd-0.18.3 → cmpnd-0.20.0}/CLAUDE.md +0 -0
  24. {cmpnd-0.18.3 → cmpnd-0.20.0}/README.md +0 -0
  25. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/_gepa_patch.py +0 -0
  26. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/_program_patch.py +0 -0
  27. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/_rlm_patch.py +0 -0
  28. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/callback.py +0 -0
  29. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/__init__.py +0 -0
  30. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/admin.py +0 -0
  31. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/auth.py +0 -0
  32. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/datasets.py +0 -0
  33. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/deployments.py +0 -0
  34. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/evals.py +0 -0
  35. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/login.py +0 -0
  36. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/optimizations.py +0 -0
  37. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/org.py +0 -0
  38. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/programs.py +0 -0
  39. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  40. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/signatures.py +0 -0
  41. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  42. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/top.py +0 -0
  43. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/credentials.py +0 -0
  44. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/deprecation.py +0 -0
  45. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/pagination.py +0 -0
  46. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/shell.py +0 -0
  47. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/trace_render.py +0 -0
  48. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/verbs.py +0 -0
  49. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/configuration.py +0 -0
  50. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/context.py +0 -0
  51. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/dataset_sync.py +0 -0
  52. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/datasets.py +0 -0
  53. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/decorators.py +0 -0
  54. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/deployment.py +0 -0
  55. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/eval_handler.py +0 -0
  56. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/execution.py +0 -0
  57. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/exporter.py +0 -0
  58. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/exporter_http.py +0 -0
  59. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/helpers.py +0 -0
  60. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/identity.py +0 -0
  61. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/imaging.py +0 -0
  62. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/__init__.py +0 -0
  63. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/decode.py +0 -0
  64. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/encode.py +0 -0
  65. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/hash.py +0 -0
  66. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/program.py +0 -0
  67. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/reflection.py +0 -0
  68. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/types.py +0 -0
  69. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/xxh64.py +0 -0
  70. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/mcp_servers.py +0 -0
  71. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/models.py +0 -0
  72. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/optimization.py +0 -0
  73. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/optimizers/__init__.py +0 -0
  74. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/optimizers/gepa.py +0 -0
  75. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  76. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/optimizers/resume.py +0 -0
  77. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/optimizers/tracker.py +0 -0
  78. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/packaging.py +0 -0
  79. {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/retry.py +0 -0
  80. {cmpnd-0.18.3 → cmpnd-0.20.0}/docs/2026-04-02-code-review.md +0 -0
  81. {cmpnd-0.18.3 → cmpnd-0.20.0}/docs/custom-proposer.md +0 -0
  82. {cmpnd-0.18.3 → cmpnd-0.20.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  83. {cmpnd-0.18.3 → cmpnd-0.20.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  84. {cmpnd-0.18.3 → cmpnd-0.20.0}/docs/sdk-instrumentation.md +0 -0
  85. {cmpnd-0.18.3 → cmpnd-0.20.0}/examples/generalizing_proposer.py +0 -0
  86. {cmpnd-0.18.3 → cmpnd-0.20.0}/examples/local_ollama.py +0 -0
  87. {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/LINEAGE.md +0 -0
  88. {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/README.md +0 -0
  89. {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  90. {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  91. {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  92. {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  93. {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/py.typed +0 -0
  94. {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  95. {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/retry.py +0 -0
  96. {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/pyproject.toml +0 -0
  97. {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/audit-sdist.sh +0 -0
  98. {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/capture_review_run.py +0 -0
  99. {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/check_release_artifacts.py +0 -0
  100. {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/fixtures/review_run.json.gz +0 -0
  101. {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/gepa_resume_bug.py +0 -0
  102. {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/require_version_bump.py +0 -0
  103. {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/seed_review_run.py +0 -0
  104. {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/test-in-linux.sh +0 -0
  105. {cmpnd-0.18.3 → cmpnd-0.20.0}/stubs/dspy/__init__.pyi +0 -0
  106. {cmpnd-0.18.3 → cmpnd-0.20.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  107. {cmpnd-0.18.3 → cmpnd-0.20.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  108. {cmpnd-0.18.3 → cmpnd-0.20.0}/stubs/dspy/utils/__init__.pyi +0 -0
  109. {cmpnd-0.18.3 → cmpnd-0.20.0}/stubs/dspy/utils/callback.pyi +0 -0
  110. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/__init__.py +0 -0
  111. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/_exporter_helpers.py +0 -0
  112. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/_fake_clock.py +0 -0
  113. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/_identity_helpers.py +0 -0
  114. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/conftest.py +0 -0
  115. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/__init__.py +0 -0
  116. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/committee_task.py +0 -0
  117. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/conftest.py +0 -0
  118. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  119. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  120. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/fake_lm.py +0 -0
  121. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/helpers.py +0 -0
  122. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/target_capabilities.py +0 -0
  123. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_capture_prompts.py +0 -0
  124. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_composite_module.py +0 -0
  125. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  126. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_deploy_execute.py +0 -0
  127. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_deploy_programs.py +0 -0
  128. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_edge_cases.py +0 -0
  129. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_example_hash.py +0 -0
  130. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_gepa_optimization.py +0 -0
  131. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_interpreter_spans.py +0 -0
  132. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_lm_accuracy.py +0 -0
  133. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_module_tracing.py +0 -0
  134. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_optimization_lineage.py +0 -0
  135. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  136. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_optimize_failures.py +0 -0
  137. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_optimize_loop.py +0 -0
  138. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  139. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  140. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_parallel_export.py +0 -0
  141. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_project_attribution.py +0 -0
  142. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_react_tool_spans.py +0 -0
  143. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_save_load.py +0 -0
  144. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_trace_structure.py +0 -0
  145. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_user_attribution.py +0 -0
  146. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  147. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  148. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  149. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  150. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  151. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  152. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  153. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  154. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  155. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/10_react.json +0 -0
  156. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/11_refine.json +0 -0
  157. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/12_retrieve.json +0 -0
  158. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/13_knn.json +0 -0
  159. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/14_parallel.json +0 -0
  160. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/README.md +0 -0
  161. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/ingest_conformance.py +0 -0
  162. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/__init__.py +0 -0
  163. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/conftest.py +0 -0
  164. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/hosted_optimize_probe.py +0 -0
  165. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/optimization_run_probe.py +0 -0
  166. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  167. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/test_logs_renders_traces.py +0 -0
  168. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/test_optimization_run.py +0 -0
  169. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/test_optimizing_progress.py +0 -0
  170. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/test_unhappy_path.py +0 -0
  171. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/__init__.py +0 -0
  172. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/__main__.py +0 -0
  173. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/drain.py +0 -0
  174. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/ingest.py +0 -0
  175. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/latency_transport.py +0 -0
  176. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/rlm_shape.py +0 -0
  177. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/sweep.py +0 -0
  178. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/test_rlm_shape.py +0 -0
  179. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/__init__.py +0 -0
  180. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/conftest.py +0 -0
  181. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/seed_parity.py +0 -0
  182. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_auth.py +0 -0
  183. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_datasets.py +0 -0
  184. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_deployments.py +0 -0
  185. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_evals.py +0 -0
  186. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_health.py +0 -0
  187. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_modules.py +0 -0
  188. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_optimizations.py +0 -0
  189. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_programs.py +0 -0
  190. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_signatures.py +0 -0
  191. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_trace_streaming.py +0 -0
  192. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_traces.py +0 -0
  193. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/__init__.py +0 -0
  194. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/_provider.py +0 -0
  195. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/_reliability.py +0 -0
  196. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/_result.py +0 -0
  197. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/mint_bedrock_token.py +0 -0
  198. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_deploy_programs.py +0 -0
  199. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_multi_client_stress.py +0 -0
  200. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_optimize_client_committee.py +0 -0
  201. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_optimize_loop.py +0 -0
  202. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  203. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_optimize_stress.py +0 -0
  204. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_provider_roles.py +0 -0
  205. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_real_provider_twins.py +0 -0
  206. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_reliability.py +0 -0
  207. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_reliability_classify.py +0 -0
  208. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_rlm_wasm.py +0 -0
  209. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_trace_search.py +0 -0
  210. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_callback.py +0 -0
  211. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_candidate_lineage.py +0 -0
  212. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/__init__.py +0 -0
  213. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/conftest.py +0 -0
  214. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_admin.py +0 -0
  215. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_api_client.py +0 -0
  216. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_auth.py +0 -0
  217. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_credentials.py +0 -0
  218. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_datasets.py +0 -0
  219. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_evals.py +0 -0
  220. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_login.py +0 -0
  221. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_main.py +0 -0
  222. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_optimizations.py +0 -0
  223. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_org.py +0 -0
  224. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_pagination.py +0 -0
  225. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_parity.py +0 -0
  226. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_programs.py +0 -0
  227. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_provider_credentials.py +0 -0
  228. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_schema_sample.py +0 -0
  229. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_shell.py +0 -0
  230. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_signatures.py +0 -0
  231. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_tool_servers.py +0 -0
  232. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_top.py +0 -0
  233. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_traces.py +0 -0
  234. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  235. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_clustering/__init__.py +0 -0
  236. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_clustering/conftest.py +0 -0
  237. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_clustering/test_edge_cases.py +0 -0
  238. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  239. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_clustering/test_type_conversion.py +0 -0
  240. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cmp551_grouping_identity.py +0 -0
  241. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_configuration.py +0 -0
  242. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_context.py +0 -0
  243. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_context_propagation.py +0 -0
  244. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_dataset_polling.py +0 -0
  245. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_dataset_sync.py +0 -0
  246. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_datasets.py +0 -0
  247. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_decorators.py +0 -0
  248. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_deploy.py +0 -0
  249. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  250. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_eval.py +0 -0
  251. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_eval_start.py +0 -0
  252. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_eval_trace_grouping.py +0 -0
  253. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_exception_paths.py +0 -0
  254. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_execute.py +0 -0
  255. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_exporter.py +0 -0
  256. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_exporter_queue.py +0 -0
  257. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_exporter_real.py +0 -0
  258. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_fake_lm.py +0 -0
  259. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_gepa_callback.py +0 -0
  260. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_gepa_integration.py +0 -0
  261. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_gepa_resume.py +0 -0
  262. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_identity.py +0 -0
  263. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_imaging.py +0 -0
  264. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_import_weight.py +0 -0
  265. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_ingest_conformance.py +0 -0
  266. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_ingest_second_producer.py +0 -0
  267. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_integration.py +0 -0
  268. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_interpreter_callback.py +0 -0
  269. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_ir_encoder_properties.py +0 -0
  270. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_ir_golden_vectors.py +0 -0
  271. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_ir_roundtrip.py +0 -0
  272. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_mcp_tools.py +0 -0
  273. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_models.py +0 -0
  274. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimization_progress.py +0 -0
  275. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimization_start.py +0 -0
  276. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize.py +0 -0
  277. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize_budget.py +0 -0
  278. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize_lift.py +0 -0
  279. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize_lms.py +0 -0
  280. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize_proposer.py +0 -0
  281. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize_reflection_lm.py +0 -0
  282. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize_terminal_failure.py +0 -0
  283. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_packaging.py +0 -0
  284. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_packaging_callable_source.py +0 -0
  285. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_packaging_mcp_tools.py +0 -0
  286. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_packaging_program_source.py +0 -0
  287. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_poll_until.py +0 -0
  288. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_program_lineage_persist.py +0 -0
  289. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_project_payload.py +0 -0
  290. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_reflector_properties.py +0 -0
  291. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_reflector_smoke.py +0 -0
  292. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_repro_reactv2_bugs.py +0 -0
  293. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_require_version_bump.py +0 -0
  294. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_retry.py +0 -0
  295. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_reversible_failures.py +0 -0
  296. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_rlm_patch.py +0 -0
  297. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_schemas.py +0 -0
  298. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_seed_review_run.py +0 -0
  299. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_stats_export.py +0 -0
  300. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_trace_render.py +0 -0
  301. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_tracked_gepa.py +0 -0
  302. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_tracker_enrich.py +0 -0
  303. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_truncation.py +0 -0
  304. {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_xxh64.py +0 -0
  305. {cmpnd-0.18.3 → cmpnd-0.20.0}/todo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.18.3
3
+ Version: 0.20.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")
@@ -13,6 +13,7 @@ import httpx
13
13
  from . import api_client, deprecation
14
14
  from .commands import (
15
15
  admin,
16
+ assessments,
16
17
  auth,
17
18
  datasets,
18
19
  deployments,
@@ -26,6 +27,7 @@ from .commands import (
26
27
  tool_servers,
27
28
  top,
28
29
  traces,
30
+ valsets,
29
31
  )
30
32
 
31
33
 
@@ -141,6 +143,15 @@ def main() -> None:
141
143
  datasets.register(subparsers)
142
144
  deployments.register(subparsers)
143
145
 
146
+ # Assessments and valsets CLIs are still in development, hidden behind
147
+ # CMPND_CLI_DEV=1 (mirroring the CMPND_CLI_ADMIN gate above). The env var
148
+ # only hides the commands from customers; the server still gates each action
149
+ # on the assessments:* / valsets:* scope, so this is discoverability, not
150
+ # enforcement.
151
+ if os.environ.get("CMPND_CLI_DEV") == "1":
152
+ assessments.register(subparsers)
153
+ valsets.register(subparsers)
154
+
144
155
  # Top-level Docker-style shortcuts (human-readable by default; --json opts in)
145
156
  top.register(subparsers)
146
157
 
@@ -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
@@ -0,0 +1,67 @@
1
+ """Assessments commands.
2
+
3
+ An assessment is a person's judgement of a trace: an up/down/neutral
4
+ conclusion, a prose body, and per-span annotations. See the assessments API.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from argparse import ArgumentParser, _SubParsersAction
10
+ from typing import Any
11
+
12
+ from .. import api_client
13
+
14
+
15
+ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
16
+ parser = subparsers.add_parser("assessments", help="Record and read judgements of traces")
17
+ sub = parser.add_subparsers(dest="action")
18
+
19
+ create = sub.add_parser("create", help="Record a judgement of a trace")
20
+ create.add_argument("trace_id", help="The trace to assess (its SDK trace id)")
21
+ conclusion = create.add_mutually_exclusive_group(required=True)
22
+ conclusion.add_argument(
23
+ "--up", dest="conclusion", action="store_const", const="up", help="Affirm the trace's output"
24
+ )
25
+ conclusion.add_argument(
26
+ "--down", dest="conclusion", action="store_const", const="down", help="Dispute the trace's output"
27
+ )
28
+ conclusion.add_argument(
29
+ "--neutral", dest="conclusion", action="store_const", const="neutral", help="Neither affirm nor dispute"
30
+ )
31
+ create.add_argument("-m", "--message", dest="body", help="The prose body of the judgement")
32
+ create.add_argument(
33
+ "--annotate",
34
+ dest="annotations",
35
+ action="append",
36
+ nargs=2,
37
+ metavar=("SPAN_ID", "NOTE"),
38
+ help="Attach a note to one span in the trace (repeat for multiple)",
39
+ )
40
+ create.set_defaults(func=handle_create)
41
+
42
+ ls = sub.add_parser("list", help="List a program's assessments")
43
+ ls.add_argument(
44
+ "--program", required=True, help="Program signature hash (16-hex, as `cmpnd signatures list` shows)"
45
+ )
46
+ ls.set_defaults(func=handle_list)
47
+
48
+ show = sub.add_parser("show", help="Show one assessment with its annotations")
49
+ show.add_argument("id", help="Assessment id")
50
+ show.set_defaults(func=handle_show)
51
+
52
+
53
+ def handle_create(client: api_client.ApiClient, args: Any) -> Any:
54
+ body: dict[str, Any] = {"trace_id": args.trace_id, "conclusion": args.conclusion}
55
+ if args.body:
56
+ body["body"] = args.body
57
+ if args.annotations:
58
+ body["annotations"] = [{"span_id": span, "note": note} for span, note in args.annotations]
59
+ return client.post("/api/v1/assessments", json=body)
60
+
61
+
62
+ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
63
+ return client.get("/api/v1/assessments", params={"program": args.program})
64
+
65
+
66
+ def handle_show(client: api_client.ApiClient, args: Any) -> Any:
67
+ return client.get(f"/api/v1/assessments/{args.id}")
@@ -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
@@ -0,0 +1,101 @@
1
+ """Valsets commands.
2
+
3
+ A valset is a program-owned validation set: a name, the field shape derived
4
+ from the owning program's signature, and its entries. Entries carry the
5
+ inputs and expected outputs of one validation example.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from argparse import ArgumentParser, _SubParsersAction
12
+ from typing import Any
13
+
14
+ from .. import api_client
15
+
16
+
17
+ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
18
+ parser = subparsers.add_parser("valsets", help="Manage program-owned validation sets")
19
+ sub = parser.add_subparsers(dest="action")
20
+
21
+ create = sub.add_parser("create", help="Create a validation set owned by a program")
22
+ create.add_argument("program_hash", help="Owning program signature hash (16-hex, as `cmpnd signatures list` shows)")
23
+ create.add_argument("--name", required=True, help="Validation-set name (unique within the program)")
24
+ create.set_defaults(func=handle_create)
25
+
26
+ ls = sub.add_parser("list", help="List a program's validation sets")
27
+ ls.add_argument("--program", required=True, help="Program signature hash (16-hex)")
28
+ ls.set_defaults(func=handle_list)
29
+
30
+ show = sub.add_parser("show", help="Show a validation set")
31
+ show.add_argument("id", help="Valset id")
32
+ show.set_defaults(func=handle_show)
33
+
34
+ entries = sub.add_parser("entries", help="Manage a validation set's entries")
35
+ esub = entries.add_subparsers(dest="entries_action")
36
+
37
+ elist = esub.add_parser("list", help="List a valset's active entries")
38
+ elist.add_argument("valset_id", help="Valset id")
39
+ elist.set_defaults(func=handle_entries_list)
40
+
41
+ ecreate = esub.add_parser("create", help="Add an entry from JSON content")
42
+ ecreate.add_argument("valset_id", help="Valset id")
43
+ ecreate.add_argument(
44
+ "--inputs", required=True, metavar="JSON", help="Inputs object; its keys are the valset's input fields"
45
+ )
46
+ ecreate.add_argument(
47
+ "--expected-outputs",
48
+ dest="expected_outputs",
49
+ required=True,
50
+ metavar="JSON",
51
+ help="Expected-outputs object; its keys are the valset's output fields",
52
+ )
53
+ ecreate.add_argument(
54
+ "--from-assessment", dest="from_assessment", help="Source assessment id, recorded as the entry's provenance"
55
+ )
56
+ ecreate.set_defaults(func=handle_entries_create)
57
+
58
+ edelete = esub.add_parser("delete", help="Retire an entry (versioned, not a hard delete)")
59
+ edelete.add_argument("entry_id", help="Entry id")
60
+ edelete.set_defaults(func=handle_entries_delete)
61
+
62
+
63
+ def handle_create(client: api_client.ApiClient, args: Any) -> Any:
64
+ return client.post("/api/v1/valsets", json={"program": args.program_hash, "name": args.name})
65
+
66
+
67
+ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
68
+ return client.get("/api/v1/valsets", params={"program": args.program})
69
+
70
+
71
+ def handle_show(client: api_client.ApiClient, args: Any) -> Any:
72
+ return client.get(f"/api/v1/valsets/{args.id}")
73
+
74
+
75
+ def handle_entries_list(client: api_client.ApiClient, args: Any) -> Any:
76
+ return client.get(f"/api/v1/valsets/{args.valset_id}/entries")
77
+
78
+
79
+ def handle_entries_create(client: api_client.ApiClient, args: Any) -> Any:
80
+ inputs = _parse_json_flag("--inputs", args.inputs)
81
+ expected = _parse_json_flag("--expected-outputs", args.expected_outputs)
82
+ body: dict[str, Any] = {"inputs": inputs, "expected_outputs": expected}
83
+ if args.from_assessment:
84
+ body["from_assessment"] = args.from_assessment
85
+ return client.post(f"/api/v1/valsets/{args.valset_id}/entries", json=body)
86
+
87
+
88
+ def handle_entries_delete(client: api_client.ApiClient, args: Any) -> Any:
89
+ client.delete(f"/api/v1/valsets/entries/{args.entry_id}")
90
+ # The route returns 204; echo a confirmation so the CLI does not print null.
91
+ return {"deleted": args.entry_id}
92
+
93
+
94
+ def _parse_json_flag(flag: str, value: str) -> dict[str, Any]:
95
+ try:
96
+ parsed = json.loads(value)
97
+ except json.JSONDecodeError as exc:
98
+ raise api_client.CliError(f"{flag} JSON is invalid: {exc}") from exc
99
+ if not isinstance(parsed, dict):
100
+ raise api_client.CliError(f"{flag} JSON must be an object")
101
+ return parsed
@@ -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}