cmpnd 0.85.0__tar.gz → 0.85.2__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 (415) hide show
  1. {cmpnd-0.85.0 → cmpnd-0.85.2}/PKG-INFO +1 -1
  2. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_http.py +12 -6
  3. {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/sdk-instrumentation.md +14 -0
  4. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/exporter_http.py +73 -10
  5. cmpnd-0.85.2/ingest/cmpnd_ingest/user_agent.py +52 -0
  6. {cmpnd-0.85.0 → cmpnd-0.85.2}/pyproject.toml +1 -1
  7. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/seed_review_run.py +19 -3
  8. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_project_attribution.py +0 -73
  9. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_traces_read.py +3 -1
  10. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_trace_search.py +7 -1
  11. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_exporter.py +123 -0
  12. cmpnd-0.85.2/tests/test_user_agent.py +109 -0
  13. {cmpnd-0.85.0 → cmpnd-0.85.2}/uv.lock +1 -1
  14. {cmpnd-0.85.0 → cmpnd-0.85.2}/.gitignore +0 -0
  15. {cmpnd-0.85.0 → cmpnd-0.85.2}/AGENTS.md +0 -0
  16. {cmpnd-0.85.0 → cmpnd-0.85.2}/CLAUDE.md +0 -0
  17. {cmpnd-0.85.0 → cmpnd-0.85.2}/README.md +0 -0
  18. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/__init__.py +0 -0
  19. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_assessments.py +0 -0
  20. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_authored_import.py +0 -0
  21. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_deployments.py +0 -0
  22. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_exporter_pool.py +0 -0
  23. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_gepa_patch.py +0 -0
  24. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_program_patch.py +0 -0
  25. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_rlm_patch.py +0 -0
  26. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_runtime_tools.py +0 -0
  27. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_timespec.py +0 -0
  28. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_traces.py +0 -0
  29. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_valsets.py +0 -0
  30. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/authored.py +0 -0
  31. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/callback.py +0 -0
  32. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/__init__.py +0 -0
  33. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/api_client.py +0 -0
  34. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/__init__.py +0 -0
  35. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/admin.py +0 -0
  36. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/assessments.py +0 -0
  37. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/auth.py +0 -0
  38. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/datasets.py +0 -0
  39. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/deployments.py +0 -0
  40. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/evals.py +0 -0
  41. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/login.py +0 -0
  42. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/optimizations.py +0 -0
  43. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/org.py +0 -0
  44. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/programs.py +0 -0
  45. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/provider_credentials.py +0 -0
  46. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/tool_servers.py +0 -0
  47. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/top.py +0 -0
  48. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/traces.py +0 -0
  49. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/valsets.py +0 -0
  50. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/credentials.py +0 -0
  51. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/deprecation.py +0 -0
  52. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/pagination.py +0 -0
  53. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/shell.py +0 -0
  54. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/trace_render.py +0 -0
  55. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/tree_help.py +0 -0
  56. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/verbs.py +0 -0
  57. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/views.py +0 -0
  58. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/client.py +0 -0
  59. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/configuration.py +0 -0
  60. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/context.py +0 -0
  61. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/credentials.py +0 -0
  62. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/dataset_sync.py +0 -0
  63. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/datasets.py +0 -0
  64. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/decorators.py +0 -0
  65. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/deployment.py +0 -0
  66. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/deployment_metadata.py +0 -0
  67. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/deployment_runs.py +0 -0
  68. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/eval_handler.py +0 -0
  69. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/execution.py +0 -0
  70. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/exporter.py +0 -0
  71. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/exporter_http.py +0 -0
  72. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/helpers.py +0 -0
  73. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/identity.py +0 -0
  74. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/imaging.py +0 -0
  75. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/__init__.py +0 -0
  76. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/decode.py +0 -0
  77. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/encode.py +0 -0
  78. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/hash.py +0 -0
  79. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/program.py +0 -0
  80. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/reflection.py +0 -0
  81. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/types.py +0 -0
  82. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/xxh64.py +0 -0
  83. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/lm_configuration.py +0 -0
  84. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/mcp_servers.py +0 -0
  85. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/models.py +0 -0
  86. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/named_programs.py +0 -0
  87. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/optimization.py +0 -0
  88. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/optimizers/__init__.py +0 -0
  89. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/optimizers/gepa.py +0 -0
  90. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/optimizers/gepa_callback.py +0 -0
  91. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/optimizers/resume.py +0 -0
  92. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/optimizers/tracker.py +0 -0
  93. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/packaging.py +0 -0
  94. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/program_naming.py +0 -0
  95. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/programs.py +0 -0
  96. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/references.py +0 -0
  97. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/retry.py +0 -0
  98. {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/traces.py +0 -0
  99. {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/2026-04-02-code-review.md +0 -0
  100. {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/cli.md +0 -0
  101. {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/client.md +0 -0
  102. {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/custom-proposer.md +0 -0
  103. {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  104. {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  105. {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/reading-traces.md +0 -0
  106. {cmpnd-0.85.0 → cmpnd-0.85.2}/examples/generalizing_proposer.py +0 -0
  107. {cmpnd-0.85.0 → cmpnd-0.85.2}/examples/local_ollama.py +0 -0
  108. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/LINEAGE.md +0 -0
  109. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/README.md +0 -0
  110. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/__init__.py +0 -0
  111. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/bodies.py +0 -0
  112. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/client_floor.py +0 -0
  113. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/program_names.py +0 -0
  114. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/protocols.py +0 -0
  115. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/py.typed +0 -0
  116. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/request_log.py +0 -0
  117. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/retry.py +0 -0
  118. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/span_context.py +0 -0
  119. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  120. {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/pyproject.toml +0 -0
  121. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/audit-sdist.sh +0 -0
  122. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/capture_review_run.py +0 -0
  123. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/check-pip-install.sh +0 -0
  124. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/check_release_artifacts.py +0 -0
  125. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/fixtures/review_run.json.gz +0 -0
  126. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/gepa_resume_bug.py +0 -0
  127. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/install-test-env.sh +0 -0
  128. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/release-gate.sh +0 -0
  129. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/release-real-models.sh +0 -0
  130. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/require_version_bump.py +0 -0
  131. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/selfhost_compat.py +0 -0
  132. {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/test-in-linux.sh +0 -0
  133. {cmpnd-0.85.0 → cmpnd-0.85.2}/stubs/dspy/__init__.pyi +0 -0
  134. {cmpnd-0.85.0 → cmpnd-0.85.2}/stubs/dspy/primitives/__init__.pyi +0 -0
  135. {cmpnd-0.85.0 → cmpnd-0.85.2}/stubs/dspy/primitives/prediction.pyi +0 -0
  136. {cmpnd-0.85.0 → cmpnd-0.85.2}/stubs/dspy/utils/__init__.pyi +0 -0
  137. {cmpnd-0.85.0 → cmpnd-0.85.2}/stubs/dspy/utils/callback.pyi +0 -0
  138. {cmpnd-0.85.0 → cmpnd-0.85.2}/stubs/dspy/utils/dummies.pyi +0 -0
  139. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/__init__.py +0 -0
  140. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_credential_route.py +0 -0
  141. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_deployment_signature_helpers.py +0 -0
  142. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_exporter_helpers.py +0 -0
  143. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_fake_clock.py +0 -0
  144. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_identity_helpers.py +0 -0
  145. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_resolve_refs.py +0 -0
  146. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_traced_run.py +0 -0
  147. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/conftest.py +0 -0
  148. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/__init__.py +0 -0
  149. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/committee_task.py +0 -0
  150. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/conftest.py +0 -0
  151. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/continuation_task.py +0 -0
  152. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  153. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  154. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/fake_lm.py +0 -0
  155. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/helpers.py +0 -0
  156. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/target_capabilities.py +0 -0
  157. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_assessments_cli.py +0 -0
  158. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_capture_prompts.py +0 -0
  159. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_composite_module.py +0 -0
  160. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_dataset_auto_creation.py +0 -0
  161. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_deploy_execute.py +0 -0
  162. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_deploy_pandas.py +0 -0
  163. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_deploy_programs.py +0 -0
  164. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_edge_cases.py +0 -0
  165. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_example_hash.py +0 -0
  166. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_gepa_optimization.py +0 -0
  167. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_interpreter_spans.py +0 -0
  168. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_lm_accuracy.py +0 -0
  169. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_module_tracing.py +0 -0
  170. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimization_lineage.py +0 -0
  171. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimization_metric_identity.py +0 -0
  172. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_authored_metric.py +0 -0
  173. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_continuation.py +0 -0
  174. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_custom_proposer.py +0 -0
  175. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_failures.py +0 -0
  176. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_loop.py +0 -0
  177. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_review_committee.py +0 -0
  178. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  179. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_parallel_export.py +0 -0
  180. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_react_tool_spans.py +0 -0
  181. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_save_load.py +0 -0
  182. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_trace_structure.py +0 -0
  183. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_user_attribution.py +0 -0
  184. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_valsets_cli.py +0 -0
  185. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/01_predict_qa.json +0 -0
  186. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  187. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  188. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  189. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  190. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  191. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/07_program_of_thought.json +0 -0
  192. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/08_opaque_module.json +0 -0
  193. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  194. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/10_react.json +0 -0
  195. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/11_refine.json +0 -0
  196. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/12_retrieve.json +0 -0
  197. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/13_knn.json +0 -0
  198. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/14_parallel.json +0 -0
  199. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/README.md +0 -0
  200. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/ingest_conformance.py +0 -0
  201. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/__init__.py +0 -0
  202. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/authored_optimize/judge.py +0 -0
  203. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  204. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/conftest.py +0 -0
  205. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  206. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/hosted_optimize_probe.py +0 -0
  207. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/optimization_run_probe.py +0 -0
  208. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  209. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/test_logs_renders_traces.py +0 -0
  210. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/test_optimization_run.py +0 -0
  211. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/test_optimizing_progress.py +0 -0
  212. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/test_unhappy_path.py +0 -0
  213. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/lm15_prompt_fixture.py +0 -0
  214. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/lm15_prompts/four_turn_predict.json +0 -0
  215. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/lm15_prompts/rlm_six_turns.json +0 -0
  216. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/__init__.py +0 -0
  217. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/__main__.py +0 -0
  218. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/drain.py +0 -0
  219. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/ingest.py +0 -0
  220. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/latency_transport.py +0 -0
  221. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/rlm_shape.py +0 -0
  222. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/rlm_size_report.py +0 -0
  223. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/sweep.py +0 -0
  224. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/test_rlm_shape.py +0 -0
  225. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/test_rlm_size_report.py +0 -0
  226. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/__init__.py +0 -0
  227. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/conftest.py +0 -0
  228. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/seed_parity.py +0 -0
  229. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_auth.py +0 -0
  230. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_datasets.py +0 -0
  231. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_deployments.py +0 -0
  232. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_evals.py +0 -0
  233. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_health.py +0 -0
  234. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_modules.py +0 -0
  235. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_optimizations.py +0 -0
  236. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_programs.py +0 -0
  237. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_signatures.py +0 -0
  238. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_trace_streaming.py +0 -0
  239. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_traces.py +0 -0
  240. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/__init__.py +0 -0
  241. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/_provider.py +0 -0
  242. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/_readiness.py +0 -0
  243. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/_reliability.py +0 -0
  244. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/_result.py +0 -0
  245. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/mint_bedrock_token.py +0 -0
  246. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_bedrock_converse.py +0 -0
  247. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_deploy_programs.py +0 -0
  248. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_multi_client_stress.py +0 -0
  249. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_optimize_client_committee.py +0 -0
  250. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_optimize_continuation.py +0 -0
  251. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_optimize_loop.py +0 -0
  252. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_optimize_loop_wasm.py +0 -0
  253. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_optimize_stress.py +0 -0
  254. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_provider_roles.py +0 -0
  255. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_readiness.py +0 -0
  256. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_real_provider_twins.py +0 -0
  257. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_reliability.py +0 -0
  258. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_reliability_classify.py +0 -0
  259. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_rlm_wasm.py +0 -0
  260. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/span_reference_fixture.py +0 -0
  261. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_adapter_reentrancy.py +0 -0
  262. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_assessments_api.py +0 -0
  263. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_authored.py +0 -0
  264. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_authored_binding_credentials.py +0 -0
  265. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_authored_bundle_collisions.py +0 -0
  266. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_authored_metric_contract.py +0 -0
  267. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_authored_named_models.py +0 -0
  268. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_authored_optimize.py +0 -0
  269. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_callback.py +0 -0
  270. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_candidate_lineage.py +0 -0
  271. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/__init__.py +0 -0
  272. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/conftest.py +0 -0
  273. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_admin.py +0 -0
  274. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_api_client.py +0 -0
  275. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_assessments.py +0 -0
  276. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_auth.py +0 -0
  277. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_credentials.py +0 -0
  278. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_datasets.py +0 -0
  279. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_deployment_runs.py +0 -0
  280. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_deployments.py +0 -0
  281. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_evals.py +0 -0
  282. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_login.py +0 -0
  283. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_main.py +0 -0
  284. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_optimizations.py +0 -0
  285. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_org.py +0 -0
  286. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_pagination.py +0 -0
  287. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_parity.py +0 -0
  288. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_programs.py +0 -0
  289. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_provider_credentials.py +0 -0
  290. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_schema_sample.py +0 -0
  291. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_shell.py +0 -0
  292. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_tool_servers.py +0 -0
  293. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_top.py +0 -0
  294. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_traces.py +0 -0
  295. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_traces_program_and_module.py +0 -0
  296. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_traces_search_wire.py +0 -0
  297. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_tree_help.py +0 -0
  298. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_valsets.py +0 -0
  299. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_client.py +0 -0
  300. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_client_floor.py +0 -0
  301. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_client_program_names.py +0 -0
  302. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_clustering/__init__.py +0 -0
  303. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_clustering/conftest.py +0 -0
  304. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_clustering/test_edge_cases.py +0 -0
  305. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_clustering/test_hash_consistency.py +0 -0
  306. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_clustering/test_type_conversion.py +0 -0
  307. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cmp551_grouping_identity.py +0 -0
  308. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_configuration.py +0 -0
  309. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_context.py +0 -0
  310. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_context_propagation.py +0 -0
  311. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_credential_kwargs.py +0 -0
  312. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_credential_selection.py +0 -0
  313. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_credential_values.py +0 -0
  314. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_dataset_polling.py +0 -0
  315. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_dataset_sync.py +0 -0
  316. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_datasets.py +0 -0
  317. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_decision_client_spans.py +0 -0
  318. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_decorators.py +0 -0
  319. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_deploy.py +0 -0
  320. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_deploy_mcp_tool_references.py +0 -0
  321. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_deploy_program_id.py +0 -0
  322. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_deployment_metadata.py +0 -0
  323. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_deployment_runs.py +0 -0
  324. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_deployments_read.py +0 -0
  325. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_eval.py +0 -0
  326. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_eval_example_attribution.py +0 -0
  327. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_eval_program_id.py +0 -0
  328. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_eval_start.py +0 -0
  329. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_eval_trace_grouping.py +0 -0
  330. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_exception_paths.py +0 -0
  331. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_execute.py +0 -0
  332. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_execute_run_identity.py +0 -0
  333. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_exporter_program_names.py +0 -0
  334. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_exporter_queue.py +0 -0
  335. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_exporter_real.py +0 -0
  336. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_fake_lm.py +0 -0
  337. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_gepa_callback.py +0 -0
  338. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_gepa_integration.py +0 -0
  339. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_gepa_output_normalization.py +0 -0
  340. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_gepa_resume.py +0 -0
  341. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_identity.py +0 -0
  342. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_imaging.py +0 -0
  343. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_import_weight.py +0 -0
  344. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_ingest_conformance.py +0 -0
  345. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_ingest_second_producer.py +0 -0
  346. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_integration.py +0 -0
  347. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_interpreter_callback.py +0 -0
  348. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_ir_encoder_properties.py +0 -0
  349. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_ir_golden_vectors.py +0 -0
  350. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_ir_roundtrip.py +0 -0
  351. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_lm15_native_calls.py +0 -0
  352. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_lm15_prompt_fixtures.py +0 -0
  353. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_lm15_prompt_references.py +0 -0
  354. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_lm_configuration.py +0 -0
  355. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_lm_usage_shapes.py +0 -0
  356. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_mcp_tools.py +0 -0
  357. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_models.py +0 -0
  358. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_multi_client_stress_readiness.py +0 -0
  359. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_no_signature_hash_on_the_wire.py +0 -0
  360. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimization_phase_callback.py +0 -0
  361. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimization_program_names.py +0 -0
  362. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimization_progress.py +0 -0
  363. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimization_start.py +0 -0
  364. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize.py +0 -0
  365. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize_budget.py +0 -0
  366. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize_lift.py +0 -0
  367. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize_lms.py +0 -0
  368. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize_proposer.py +0 -0
  369. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize_reflection_lm.py +0 -0
  370. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize_terminal_failure.py +0 -0
  371. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_packaging.py +0 -0
  372. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_packaging_callable_source.py +0 -0
  373. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_packaging_mcp_tools.py +0 -0
  374. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_packaging_program_source.py +0 -0
  375. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_poll_until.py +0 -0
  376. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_program_id_deprecation.py +0 -0
  377. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_program_identity.py +0 -0
  378. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_program_lineage_persist.py +0 -0
  379. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_program_names_in_deploy.py +0 -0
  380. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_program_naming.py +0 -0
  381. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_programs_create.py +0 -0
  382. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_project_payload.py +0 -0
  383. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_real_provider_twins_readiness.py +0 -0
  384. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_reflector_properties.py +0 -0
  385. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_reflector_smoke.py +0 -0
  386. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_release_gate_script.py +0 -0
  387. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_release_real_models_script.py +0 -0
  388. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_replaced_names.py +0 -0
  389. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_repro_reactv2_bugs.py +0 -0
  390. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_require_version_bump.py +0 -0
  391. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_retry.py +0 -0
  392. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_reversible_failures.py +0 -0
  393. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_rlm_patch.py +0 -0
  394. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_run_attribution_paths.py +0 -0
  395. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_runtime_tools.py +0 -0
  396. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_schemas.py +0 -0
  397. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_seal_lm_credential.py +0 -0
  398. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_seed_review_run.py +0 -0
  399. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_signature_inputs.py +0 -0
  400. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_span_references.py +0 -0
  401. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_span_sent_once.py +0 -0
  402. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_span_submission.py +0 -0
  403. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_span_submission_wire_contract.py +0 -0
  404. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_system_one.py +0 -0
  405. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_trace_context_concurrency.py +0 -0
  406. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_trace_fact_wire_contract.py +0 -0
  407. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_trace_facts.py +0 -0
  408. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_trace_render.py +0 -0
  409. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_traces_read.py +0 -0
  410. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_tracked_gepa.py +0 -0
  411. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_tracker_enrich.py +0 -0
  412. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_truncation.py +0 -0
  413. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_upload_auth_headers.py +0 -0
  414. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_valsets_api.py +0 -0
  415. {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.85.0
3
+ Version: 0.85.2
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>
@@ -16,7 +16,7 @@ from typing import Any
16
16
  from urllib.parse import urlsplit
17
17
 
18
18
  import httpx
19
- from cmpnd_ingest import client_floor
19
+ from cmpnd_ingest import client_floor, user_agent
20
20
 
21
21
 
22
22
  class ApiError(Exception):
@@ -79,16 +79,22 @@ def server_detail(response: httpx.Response) -> str:
79
79
 
80
80
 
81
81
  def new_client(**kwargs: Any) -> httpx.Client:
82
- """An ``httpx.Client`` that warns when the server no longer supports this
83
- release (:mod:`cmpnd_ingest.client_floor`).
82
+ """An ``httpx.Client`` that names this release to the server and warns when
83
+ the server no longer supports it (:mod:`cmpnd_ingest.client_floor`).
84
84
 
85
- Every client the SDK builds comes from here, so every answer the platform
86
- sends gets the check. A caller's own ``event_hooks`` run after it.
85
+ Every client the SDK builds comes from here, so every request carries the
86
+ release's ``User-Agent`` (:mod:`cmpnd_ingest.user_agent`) and every answer
87
+ the platform sends gets the check. A caller's own ``headers`` override the
88
+ ``User-Agent``, and its own ``event_hooks`` run after the check.
87
89
  """
88
90
  hooks = client_floor.event_hooks()
89
91
  for event, callers in (kwargs.pop("event_hooks", None) or {}).items():
90
92
  hooks.setdefault(event, []).extend(callers)
91
- return httpx.Client(event_hooks=hooks, **kwargs)
93
+ # httpx.Headers matches names without regard to case, so a caller's `user-agent`
94
+ # replaces the default rather than travelling beside it.
95
+ headers = httpx.Headers(user_agent.headers())
96
+ headers.update(kwargs.pop("headers", None) or {})
97
+ return httpx.Client(event_hooks=hooks, headers=headers, **kwargs)
92
98
 
93
99
 
94
100
  _DEFAULT_PORTS = {"http": 80, "https": 443}
@@ -331,6 +331,18 @@ Each `_send_*` method routes a failed delivery to one of two counters: a `429` (
331
331
 
332
332
  Tolerating is never silent. A send that recovered logs a warning naming what it rode through (`rode through 3 transient responses over 12.4s (503=3) to POST /api/v1/traces`), and an exhausted budget raises `ExportRetriesExhausted` with the same count and window in its message. `BatchExporter.failure_summary()` reports the distribution of non-2xx statuses observed, how many transient answers were ridden through, and a sample error body — that's what the stress lane prints when it fails, so a red lane says *what* the wire answered rather than only how many errors there were.
333
333
 
334
+ Trace delivery failures log at ERROR on `cmpnd.exporter`. The message names the
335
+ operation and the number of records the exporter will not resend. HTTP failures
336
+ include the status, route and serve's string `error`, `error_code`, `upgrade` and
337
+ `field` values when present. The exporter bounds each value and escapes control
338
+ characters. It omits other body fields and non-JSON responses. Exhausted retries
339
+ include the last server message as well as the attempt count and elapsed time.
340
+
341
+ A non-retryable 422 discards the submission. Upgrade the SDK when serve asks for
342
+ it, then emit fresh telemetry. An upgrade cannot recover discarded records.
343
+ Background failures keep the application running and increment the existing
344
+ error counters. They do not raise an exception in the instrumented program.
345
+
334
346
  ### The retryable-error contract
335
347
 
336
348
  The server tells a client when a failed write is worth re-sending, in three signals carrying one fact:
@@ -361,6 +373,8 @@ Two callers implement the waiting:
361
373
 
362
374
  Every API answer names the oldest cmpnd release the server supports, in the `X-Cmpnd-Minimum-Client-Version` header. `cmpnd_ingest/client_floor.py` reads it in an httpx response hook. When the installed `cmpnd` is older, it raises an `UpgradeRequiredWarning` once per process that names both versions and the command `pip install -U cmpnd`. The server refuses nothing for a client's version, so a call below the floor still goes through, and the warning appears beside whatever it does. Every client the SDK builds comes from `cmpnd._http.new_client`, which installs the hook, and the exporter installs it in `_client_kwargs`. `tests/test_client_floor.py` fails on an `httpx.Client` built any other way in `cmpnd/`. An install of `cmpnd_ingest` without `cmpnd` has no version to compare and says nothing. `CMPND_NO_UPGRADE_WARNING=1` silences the warning for a user who stays on an older release on purpose, and the warning names that switch. `cmpnd.UpgradeRequiredWarning` is public, so a `warnings` filter silences it in code.
363
375
 
376
+ Every request names its release in the other direction, so the server can count which releases are in use. `cmpnd_ingest/user_agent.py` builds the `User-Agent` value, such as `cmpnd/0.85.2 python/3.12.13 httpx/0.28.1 dspy/3.4.1`. The first product is `cmpnd` for the SDK and `cmpnd-ingest` for a producer that installs only the transport, such as the optimizer's search process. A library that is not installed is left out. `cmpnd._http.new_client` and the exporter's `_client_kwargs` both send it. A caller that passes its own `User-Agent` to `new_client` keeps it. `tests/test_user_agent.py` checks the value that reaches the wire.
377
+
364
378
  ### The optimize loop and transient tolerance
365
379
 
366
380
  `optimization._poll_until_terminal` is what a caller blocks on for the length of a run, so one transient answer or one reset connection must not destroy a healthy run. It rides through transient answers on the shared budget, resetting after each successful poll, and never extends past the caller's `timeout` (default 600s per step) — a backoff wait is clipped by the deadline rather than overshooting it.
@@ -34,7 +34,7 @@ from typing import Any, cast
34
34
 
35
35
  import httpx
36
36
 
37
- from . import client_floor, program_names, request_log, retry
37
+ from . import client_floor, program_names, request_log, retry, user_agent
38
38
  from .bodies import EncodedTraceBody, SpanBody, TraceBody
39
39
  from .protocols import (
40
40
  EvalRunRecord,
@@ -59,6 +59,31 @@ logger = logging.getLogger("cmpnd.exporter")
59
59
  _GZIP_HEADERS = {"Content-Encoding": "gzip"}
60
60
 
61
61
 
62
+ def _export_error_detail(exc: Exception) -> str:
63
+ """Keep delivery errors useful in background logs without dumping response bodies."""
64
+
65
+ def text(value: str, limit: int = 300) -> str:
66
+ return json.dumps(value[:limit], ensure_ascii=True)[1:-1]
67
+
68
+ cause = exc.__cause__ if isinstance(exc, ExportRetriesExhausted) else exc
69
+ if not isinstance(cause, httpx.HTTPStatusError):
70
+ return text(str(exc), 600)
71
+ response = cause.response
72
+ detail = f"HTTP {response.status_code} for {cause.request.method} {cause.request.url.path}"
73
+ if cause is not exc:
74
+ detail = f"{text(str(exc), 600)}; {detail}"
75
+ try:
76
+ body = response.json()
77
+ except (ValueError, httpx.ResponseNotRead):
78
+ return detail
79
+ if isinstance(body, dict):
80
+ for key in ("error", "error_code", "upgrade", "field"):
81
+ value = body.get(key)
82
+ if isinstance(value, str) and value:
83
+ detail += f"; {key}={text(value)}"
84
+ return detail
85
+
86
+
62
87
  def _send_json(client: httpx.Client, method: str, path: str, payload: Any) -> httpx.Response:
63
88
  """POST/PATCH/PUT ``payload`` as gzipped JSON.
64
89
 
@@ -1081,11 +1106,13 @@ class BatchExporter:
1081
1106
 
1082
1107
  def _client_kwargs(self) -> dict[str, Any]:
1083
1108
  """Shared httpx.Client construction args for the background and sync
1084
- clients, so both carry the same base URL, auth header, client-floor
1085
- check (``client_floor``) and, in tests, injected transport."""
1109
+ clients, so both carry the same base URL, auth header, release
1110
+ ``User-Agent`` (``user_agent``), client-floor check (``client_floor``)
1111
+ and, in tests, injected transport."""
1086
1112
  kwargs: dict[str, Any] = {
1087
1113
  "base_url": self._config.endpoint,
1088
1114
  "headers": {
1115
+ **user_agent.headers(),
1089
1116
  "X-API-Key": self._config.api_key,
1090
1117
  "Content-Type": "application/json",
1091
1118
  },
@@ -1581,15 +1608,30 @@ class BatchExporter:
1581
1608
  # drop call for opposite responses from the caller.
1582
1609
  self._record_drop("trace", "server_shed", count=len(traces_to_send), log=False)
1583
1610
  else:
1584
- logger.error(f"Failed to export traces batch: {e}")
1611
+ logger.error(
1612
+ "Failed to export traces batch. Records will not be resent (count=%s, traces=%s, spans=%s): %s",
1613
+ len(traces_to_send) + total_spans,
1614
+ len(traces_to_send),
1615
+ total_spans,
1616
+ _export_error_detail(e),
1617
+ )
1585
1618
  with self._stats_lock:
1586
1619
  self._error_count += len(traces_to_send)
1587
1620
  except Exception as e:
1588
- logger.error(f"Failed to export traces batch: {e}")
1621
+ logger.error(
1622
+ "Failed to export traces batch. Records will not be resent (count=%s, traces=%s, spans=%s): %s",
1623
+ len(traces_to_send) + total_spans,
1624
+ len(traces_to_send),
1625
+ total_spans,
1626
+ _export_error_detail(e),
1627
+ )
1589
1628
  with self._stats_lock:
1590
1629
  self._error_count += len(traces_to_send)
1591
1630
 
1592
1631
  def _send_encoded_trace(self, trace: EncodedTraceBody) -> None:
1632
+ # The body stays compressed end to end, so the exporter never learns how
1633
+ # many spans it embeds. A failure names the one trace and says the span
1634
+ # count is unknown, rather than understating the loss as one record.
1593
1635
  client = self._require_client()
1594
1636
  try:
1595
1637
  self._send_retrying(
@@ -1601,11 +1643,19 @@ class BatchExporter:
1601
1643
  if e.response.status_code == 429:
1602
1644
  self._record_drop("trace", "server_shed", count=1)
1603
1645
  else:
1604
- logger.error(f"Failed to export encoded trace {trace.trace_id}: {e}")
1646
+ logger.error(
1647
+ "Failed to export encoded trace %s. Records will not be resent (traces=1, spans=unknown): %s",
1648
+ trace.trace_id,
1649
+ _export_error_detail(e),
1650
+ )
1605
1651
  with self._stats_lock:
1606
1652
  self._error_count += 1
1607
1653
  except Exception as e:
1608
- logger.error(f"Failed to export encoded trace {trace.trace_id}: {e}")
1654
+ logger.error(
1655
+ "Failed to export encoded trace %s. Records will not be resent (traces=1, spans=unknown): %s",
1656
+ trace.trace_id,
1657
+ _export_error_detail(e),
1658
+ )
1609
1659
  with self._stats_lock:
1610
1660
  self._error_count += 1
1611
1661
 
@@ -1619,7 +1669,11 @@ class BatchExporter:
1619
1669
  self._exported_count += 1
1620
1670
  logger.debug(f"Started trace {trace_id}")
1621
1671
  except Exception as e:
1622
- logger.error(f"Failed to start trace {trace_id}: {e}")
1672
+ logger.error(
1673
+ "Failed to start trace %s. Records will not be resent (count=1): %s",
1674
+ trace_id,
1675
+ _export_error_detail(e),
1676
+ )
1623
1677
  with self._stats_lock:
1624
1678
  self._error_count += 1
1625
1679
 
@@ -1651,7 +1705,12 @@ class BatchExporter:
1651
1705
  self._exported_count += len(span_list)
1652
1706
  logger.debug(f"Exported {len(span_list)} spans for trace {trace_id}")
1653
1707
  except Exception as e:
1654
- logger.error(f"Failed to export spans for trace {trace_id}: {e}")
1708
+ logger.error(
1709
+ "Failed to export spans for trace %s. Records will not be resent (count=%s): %s",
1710
+ trace_id,
1711
+ len(span_list),
1712
+ _export_error_detail(e),
1713
+ )
1655
1714
  with self._stats_lock:
1656
1715
  self._error_count += len(span_list)
1657
1716
 
@@ -1667,7 +1726,11 @@ class BatchExporter:
1667
1726
  self._exported_count += 1
1668
1727
  logger.debug(f"Finalized trace {trace_id}")
1669
1728
  except Exception as e:
1670
- logger.error(f"Failed to finalize trace {trace_id}: {e}")
1729
+ logger.error(
1730
+ "Failed to finalize trace %s. Records will not be resent (count=1): %s",
1731
+ trace_id,
1732
+ _export_error_detail(e),
1733
+ )
1671
1734
  with self._stats_lock:
1672
1735
  self._error_count += 1
1673
1736
 
@@ -0,0 +1,52 @@
1
+ """The ``User-Agent`` every client the SDK builds sends, so the server can count releases.
2
+
3
+ The value names the installed distribution first, then the runtime it runs on:
4
+
5
+ cmpnd/0.85.2 python/3.12.13 httpx/0.28.1 dspy/3.4.1
6
+
7
+ The first product is ``cmpnd`` for the SDK and ``cmpnd-ingest`` for a producer that
8
+ installs only the transport, so the two never count as one. A distribution that is not
9
+ installed, such as ``dspy`` for a transport-only producer, is left out. A value that
10
+ cannot be read becomes ``unknown`` rather than failing a request.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import functools
16
+ import importlib.metadata
17
+ import platform
18
+
19
+ # The distributions that provide this package, in the order the first one found names
20
+ # the client. Only one is installed at a time (ingest/README.md).
21
+ _CLIENTS = ("cmpnd", "cmpnd-ingest")
22
+ # Runtime libraries whose version shapes what a client sends.
23
+ _LIBRARIES = ("httpx", "dspy")
24
+
25
+
26
+ def _version(distribution: str) -> str | None:
27
+ """The installed version, or None when it is not installed or cannot be read.
28
+
29
+ Any failure to read metadata counts as absent, because a broken install's
30
+ metadata must not stop a client from being built.
31
+ """
32
+ try:
33
+ return importlib.metadata.version(distribution)
34
+ except Exception:
35
+ return None
36
+
37
+
38
+ @functools.cache
39
+ def user_agent() -> str:
40
+ """The ``User-Agent`` header value for this process."""
41
+ client = next(
42
+ (f"{name}/{version}" for name in _CLIENTS if (version := _version(name)) is not None),
43
+ "cmpnd/unknown",
44
+ )
45
+ products = [client, f"python/{platform.python_version()}"]
46
+ products += [f"{name}/{version}" for name in _LIBRARIES if (version := _version(name)) is not None]
47
+ return " ".join(products)
48
+
49
+
50
+ def headers() -> dict[str, str]:
51
+ """The headers that identify this client, to merge into a client's own."""
52
+ return {"User-Agent": user_agent()}
@@ -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.85.0"
8
+ version = "0.85.2"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"
@@ -46,11 +46,15 @@ import logging
46
46
  import os
47
47
  import pathlib
48
48
  import re
49
+ import sys
49
50
  import urllib.error
50
51
  import urllib.parse
51
52
  import urllib.request
52
53
  import uuid
53
54
 
55
+ # The captured fixtures already depend on this repository's serve scripts.
56
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "serve" / "scripts" / "seed_fixtures"))
57
+
54
58
  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
55
59
  log = logging.getLogger("seed_review_run")
56
60
 
@@ -673,6 +677,14 @@ def _ensure_program(endpoint: str, api_key: str, name: str) -> str | None:
673
677
 
674
678
 
675
679
  def _post(endpoint: str, path: str, payload, api_key: str, method: str = "POST"):
680
+ if path.startswith("/api/v1/traces"):
681
+ from submission import prepare_trace_submission
682
+
683
+ if path == "/api/v1/traces/batch":
684
+ for trace in payload:
685
+ prepare_trace_submission(trace)
686
+ elif path.startswith("/api/v1/traces/") and path.endswith("/spans"):
687
+ prepare_trace_submission(payload, path.split("/")[-2])
676
688
  body = json.dumps(payload).encode("utf-8")
677
689
  req = urllib.request.Request(
678
690
  endpoint + path,
@@ -680,9 +692,13 @@ def _post(endpoint: str, path: str, payload, api_key: str, method: str = "POST")
680
692
  method=method,
681
693
  headers={"Content-Type": "application/json", "X-API-Key": api_key},
682
694
  )
683
- with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310 - fixed https endpoint from env
684
- raw = resp.read()
685
- return resp.status, (json.loads(raw) if raw else {})
695
+ try:
696
+ with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310 - fixed https endpoint from env
697
+ raw = resp.read()
698
+ return resp.status, (json.loads(raw) if raw else {})
699
+ except urllib.error.HTTPError as exc:
700
+ detail = exc.read().decode("utf-8", "replace")[:400]
701
+ raise RuntimeError(f"{method} {path} returned HTTP {exc.code}: {detail!r}") from exc
686
702
 
687
703
 
688
704
  def _reserve_optimization(endpoint: str, api_key: str) -> str | None:
@@ -10,13 +10,11 @@ block so subsequent tests in the same pytest invocation are not affected.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- import datetime
14
13
  import uuid
15
14
  from collections.abc import Callable
16
15
  from typing import Any
17
16
 
18
17
  import dspy
19
- import httpx
20
18
  import pytest
21
19
 
22
20
  import cmpnd
@@ -121,77 +119,6 @@ def test_project_stamped_on_trace_and_spans(server_client, trace_capture):
121
119
  _restore_session_config()
122
120
 
123
121
 
124
- def test_streaming_spans_inherit_full_project_tags(server_client):
125
- """Spans added via the streaming path (POST /traces/:id/spans) inherit all
126
- project_tags from the parent trace, not just the first tag.
127
-
128
- This bypasses the SDK exporter and calls the API directly to simulate
129
- an old SDK that sends project but not project_tags on spans. Serve must
130
- preserve both tags from the parent trace.
131
- """
132
- trace_id = str(uuid.uuid4())
133
- tag_a = f"e2e-stream-a-{uuid.uuid4().hex[:8]}"
134
- tag_b = f"e2e-stream-b-{uuid.uuid4().hex[:8]}"
135
- now = datetime.datetime.now(datetime.timezone.utc).isoformat()
136
- headers = {
137
- "X-API-Key": CMPND_E2E_API_KEY or "",
138
- "Content-Type": "application/json",
139
- }
140
-
141
- # 1. Start trace with two tags
142
- start_resp = httpx.post(
143
- f"{E2E_ENDPOINT}/api/v1/traces/{trace_id}/start",
144
- headers=headers,
145
- json={
146
- "module_class": "StreamTest",
147
- "start_time": now,
148
- "project": tag_a,
149
- "project_tags": [tag_a, tag_b],
150
- },
151
- timeout=30.0,
152
- )
153
- assert start_resp.status_code == 202, f"start_trace failed: {start_resp.status_code} {start_resp.text}"
154
-
155
- # 2. Add a span WITHOUT project_tags — simulates old SDK behavior
156
- span_id = str(uuid.uuid4())
157
- spans_resp = httpx.post(
158
- f"{E2E_ENDPOINT}/api/v1/traces/{trace_id}/spans",
159
- headers=headers,
160
- json={
161
- "spans": [
162
- {
163
- "span_id": span_id,
164
- "trace_id": trace_id,
165
- "name": "predict",
166
- "span_type": "predict",
167
- "start_time": now,
168
- "end_time": now,
169
- # project and project_tags intentionally omitted
170
- }
171
- ]
172
- },
173
- timeout=30.0,
174
- )
175
- assert spans_resp.status_code == 200, f"add_spans failed: {spans_resp.status_code} {spans_resp.text}"
176
-
177
- # 3. Poll for ingestion, then fetch the trace. The trace row and its spans
178
- # become visible independently and asynchronously. Read the endpoint
179
- # directly to keep the wait bounded by one retry loop (CMP-843).
180
- def _spans():
181
- resp = server_client._client.get(f"/api/v1/traces/{trace_id}")
182
- return resp.json().get("spans", []) if resp.status_code == 200 else []
183
-
184
- spans = poll_until(_spans, timeout=60.0, interval=3.0)
185
-
186
- assert spans, "trace has no spans after streaming add"
187
- for span in spans:
188
- span_tags = span.get("project_tags", [])
189
- assert tag_a in span_tags and tag_b in span_tags, (
190
- f"span {span.get('span_id')} should inherit both tags from trace, "
191
- f"got {span_tags!r} — expected {[tag_a, tag_b]!r}"
192
- )
193
-
194
-
195
122
  def test_project_null_when_unset(server_client, trace_capture):
196
123
  """Without configure(project=...), the trace and spans land with NULL project."""
197
124
  try:
@@ -29,7 +29,9 @@ class TestTracesReadE2E:
29
29
  trace_id = trace_capture.last_trace_id
30
30
  assert trace_id is not None, "no trace captured"
31
31
 
32
- # Detail reads the ingested source without waiting for collection projection.
32
+ # Trace headers and spans publish independently. Wait until both are visible
33
+ # before checking the SDK's detail read.
34
+ server_client.get_trace(trace_id)
33
35
  detail = cmpnd.traces.show(trace_id, api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
34
36
  assert detail["trace"]["trace_id"] == trace_id
35
37
  assert isinstance(detail["spans"], list)
@@ -78,8 +78,11 @@ def test_cross_trace_q_search_finds_the_span_token():
78
78
 
79
79
  # One trace, one span whose name carries the unique token. The write path
80
80
  # indexes that name in the span transaction, and the read below waits for
81
- # the projected search collection to publish it.
81
+ # the projected search collection to publish it. Both records are
82
+ # self-contained submissions: each declares its schema version, and the span
83
+ # carries its own tags rather than inheriting the trace's.
82
84
  trace = {
85
+ "schema_version": 1,
83
86
  "trace_id": trace_id,
84
87
  "start_time": _iso(),
85
88
  "end_time": _iso(),
@@ -89,8 +92,11 @@ def test_cross_trace_q_search_finds_the_span_token():
89
92
  "span_count": 1,
90
93
  "spans": [
91
94
  {
95
+ "schema_version": 1,
92
96
  "span_id": span_id,
93
97
  "trace_id": trace_id,
98
+ "project_tags": [tag],
99
+ "tags": {},
94
100
  "name": token,
95
101
  "span_type": "tool",
96
102
  "start_time": _iso(),
@@ -14,6 +14,7 @@ from uuid import uuid4
14
14
 
15
15
  import httpx
16
16
  import pytest
17
+ from cmpnd_ingest.bodies import EncodedTraceBody
17
18
 
18
19
  from cmpnd import retry
19
20
  from cmpnd.configuration import Config
@@ -1179,3 +1180,125 @@ def test_failure_summary_is_quiet_when_nothing_failed():
1179
1180
  exporter._flush_batch([ExportItem(item_type="trace", data=_make_trace(), timestamp=0.0)])
1180
1181
 
1181
1182
  assert "no non-2xx" in exporter.failure_summary()
1183
+
1184
+
1185
+ @pytest.mark.parametrize(
1186
+ "kind,count",
1187
+ [("trace", 1), ("trace", 2), ("trace_start", 1), ("span", 2), ("trace_finalize", 1), ("encoded_trace", 1)],
1188
+ )
1189
+ def test_trace_rejection_logs_server_guidance_and_delivery_loss(kind, count, caplog):
1190
+ body = {
1191
+ "error": "schema_version must be 1",
1192
+ "error_code": "trace_submission_contract",
1193
+ "field": "schema_version",
1194
+ "upgrade": "Upgrade with uv add cmpnd --upgrade",
1195
+ "retryable": False,
1196
+ "unrelated": "private response detail",
1197
+ }
1198
+ capture = ContractCapture([(422, body, {})])
1199
+ exporter = _make_seq_exporter(capture)
1200
+ tid = uuid4()
1201
+ batch = []
1202
+ for _ in range(count):
1203
+ data = (
1204
+ _make_span(trace_id=tid)
1205
+ if kind == "span"
1206
+ else EncodedTraceBody(str(tid), "gzip", gzip.compress(b"{}"))
1207
+ if kind == "encoded_trace"
1208
+ else _make_trace()
1209
+ )
1210
+ batch.append(ExportItem(item_type=kind, data=data, timestamp=0.0))
1211
+ try:
1212
+ with caplog.at_level(logging.ERROR, logger="cmpnd.exporter"):
1213
+ exporter._flush_batch(batch)
1214
+ assert len(capture.requests) == 1
1215
+ assert exporter._error_count == count
1216
+ assert exporter._exported_count == 0
1217
+ assert "422" in caplog.text
1218
+ assert "schema_version must be 1" in caplog.text
1219
+ assert "trace_submission_contract" in caplog.text
1220
+ assert "Upgrade with uv add cmpnd --upgrade" in caplog.text
1221
+ if kind == "encoded_trace":
1222
+ # The exporter never inflates an encoded trace, so it cannot count the
1223
+ # spans the trace embeds and must not report the trace as one record.
1224
+ assert "traces=1, spans=unknown" in caplog.text
1225
+ assert "count=" not in caplog.text
1226
+ else:
1227
+ assert f"count={count}" in caplog.text
1228
+ assert "Records will not be resent" in caplog.text
1229
+ assert "private response detail" not in caplog.text
1230
+ finally:
1231
+ exporter.shutdown()
1232
+
1233
+
1234
+ @pytest.mark.parametrize(
1235
+ "body", [[], {"error": {"secret": "private response detail"}}, {"error": "bad\nforged log\r\x1b[31m" + "x" * 5000}]
1236
+ )
1237
+ def test_trace_rejection_handles_unexpected_or_long_error_bodies(body, caplog):
1238
+ requests = []
1239
+
1240
+ def handle(request):
1241
+ requests.append(request)
1242
+ return httpx.Response(400, json=body)
1243
+
1244
+ exporter = _make_exporter(RequestCapture())
1245
+ exporter._client = httpx.Client(base_url=_make_config().endpoint, transport=httpx.MockTransport(handle))
1246
+ try:
1247
+ with caplog.at_level(logging.ERROR, logger="cmpnd.exporter"):
1248
+ exporter._flush_batch([ExportItem(item_type="trace", data=_make_trace(), timestamp=0.0)])
1249
+ messages = [r.getMessage() for r in caplog.records if r.levelno == logging.ERROR]
1250
+ assert len(requests) == 1
1251
+ assert len(messages) == 1
1252
+ assert "400" in messages[0]
1253
+ assert "count=1" in messages[0]
1254
+ assert len(messages[0]) < 2000
1255
+ assert "private response detail" not in messages[0]
1256
+ assert "\nforged log" not in messages[0]
1257
+ assert "\x1b" not in messages[0]
1258
+ finally:
1259
+ exporter.shutdown()
1260
+
1261
+
1262
+ def test_trace_rejection_handles_non_json_response(caplog):
1263
+ exporter = _make_exporter(RequestCapture())
1264
+ exporter._client = httpx.Client(
1265
+ base_url=_make_config().endpoint,
1266
+ transport=httpx.MockTransport(lambda _: httpx.Response(413, text="<html>private response detail</html>")),
1267
+ )
1268
+ try:
1269
+ with caplog.at_level(logging.ERROR, logger="cmpnd.exporter"):
1270
+ exporter._flush_batch([ExportItem(item_type="trace", data=_make_trace(), timestamp=0.0)])
1271
+ assert "413" in caplog.text
1272
+ assert "count=1" in caplog.text
1273
+ assert "private response detail" not in caplog.text
1274
+ finally:
1275
+ exporter.shutdown()
1276
+
1277
+
1278
+ def test_retry_exhaustion_logs_last_server_message(caplog):
1279
+ capture = ContractCapture([(503, {"error": "database temporarily unavailable", "retryable": True}, {})])
1280
+ exporter = _make_seq_exporter(capture)
1281
+ try:
1282
+ with caplog.at_level(logging.ERROR, logger="cmpnd.exporter"):
1283
+ exporter._flush_batch([ExportItem(item_type="trace", data=_make_trace(), timestamp=0.0)])
1284
+ assert len(capture.requests) == retry.MAX_ATTEMPTS
1285
+ assert "database temporarily unavailable" in caplog.text
1286
+ assert "transient responses over" in caplog.text
1287
+ assert "count=1" in caplog.text
1288
+ finally:
1289
+ exporter.shutdown()
1290
+
1291
+
1292
+ def test_rejected_full_trace_logs_embedded_span_loss(caplog):
1293
+ capture = ContractCapture([(422, {"error": "invalid span", "retryable": False}, {})])
1294
+ exporter = _make_seq_exporter(capture)
1295
+ payload = _make_trace().to_api_dict()
1296
+ payload["spans"] = [_make_span().to_api_dict(), _make_span().to_api_dict()]
1297
+ try:
1298
+ with caplog.at_level(logging.ERROR, logger="cmpnd.exporter"):
1299
+ exporter._send_traces([payload])
1300
+ assert "count=3, traces=1, spans=2" in caplog.text
1301
+ assert exporter._exported_count == 0
1302
+ assert len(capture.requests) == 1
1303
+ finally:
1304
+ exporter.shutdown()