cmpnd 0.77.0__tar.gz → 0.79.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 (405) hide show
  1. {cmpnd-0.77.0 → cmpnd-0.79.0}/PKG-INFO +1 -1
  2. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_gepa_patch.py +45 -0
  3. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/callback.py +38 -5
  4. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/admin.py +31 -2
  5. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/context.py +24 -0
  6. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/optimizers/gepa.py +4 -0
  7. {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/cli.md +10 -0
  8. {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/sdk-instrumentation.md +36 -1
  9. {cmpnd-0.77.0 → cmpnd-0.79.0}/pyproject.toml +1 -1
  10. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_admin.py +91 -5
  11. cmpnd-0.79.0/tests/test_optimization_phase_callback.py +196 -0
  12. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_run_attribution_paths.py +101 -0
  13. {cmpnd-0.77.0 → cmpnd-0.79.0}/uv.lock +1 -1
  14. {cmpnd-0.77.0 → cmpnd-0.79.0}/.gitignore +0 -0
  15. {cmpnd-0.77.0 → cmpnd-0.79.0}/AGENTS.md +0 -0
  16. {cmpnd-0.77.0 → cmpnd-0.79.0}/CLAUDE.md +0 -0
  17. {cmpnd-0.77.0 → cmpnd-0.79.0}/README.md +0 -0
  18. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/__init__.py +0 -0
  19. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_assessments.py +0 -0
  20. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_authored_import.py +0 -0
  21. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_deployments.py +0 -0
  22. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_exporter_pool.py +0 -0
  23. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_http.py +0 -0
  24. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_program_patch.py +0 -0
  25. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_rlm_patch.py +0 -0
  26. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_runtime_tools.py +0 -0
  27. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_timespec.py +0 -0
  28. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_traces.py +0 -0
  29. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_valsets.py +0 -0
  30. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/authored.py +0 -0
  31. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/__init__.py +0 -0
  32. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/api_client.py +0 -0
  33. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/__init__.py +0 -0
  34. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/assessments.py +0 -0
  35. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/auth.py +0 -0
  36. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/datasets.py +0 -0
  37. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/deployments.py +0 -0
  38. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/evals.py +0 -0
  39. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/login.py +0 -0
  40. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/optimizations.py +0 -0
  41. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/org.py +0 -0
  42. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/programs.py +0 -0
  43. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  44. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  45. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/top.py +0 -0
  46. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/traces.py +0 -0
  47. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/valsets.py +0 -0
  48. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/credentials.py +0 -0
  49. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/deprecation.py +0 -0
  50. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/pagination.py +0 -0
  51. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/shell.py +0 -0
  52. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/trace_render.py +0 -0
  53. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/tree_help.py +0 -0
  54. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/verbs.py +0 -0
  55. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/views.py +0 -0
  56. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/client.py +0 -0
  57. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/configuration.py +0 -0
  58. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/credentials.py +0 -0
  59. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/dataset_sync.py +0 -0
  60. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/datasets.py +0 -0
  61. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/decorators.py +0 -0
  62. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/deployment.py +0 -0
  63. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/deployment_metadata.py +0 -0
  64. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/deployment_runs.py +0 -0
  65. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/eval_handler.py +0 -0
  66. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/execution.py +0 -0
  67. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/exporter.py +0 -0
  68. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/exporter_http.py +0 -0
  69. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/helpers.py +0 -0
  70. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/identity.py +0 -0
  71. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/imaging.py +0 -0
  72. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/__init__.py +0 -0
  73. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/decode.py +0 -0
  74. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/encode.py +0 -0
  75. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/hash.py +0 -0
  76. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/program.py +0 -0
  77. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/reflection.py +0 -0
  78. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/types.py +0 -0
  79. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/xxh64.py +0 -0
  80. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/lm_configuration.py +0 -0
  81. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/mcp_servers.py +0 -0
  82. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/models.py +0 -0
  83. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/named_programs.py +0 -0
  84. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/optimization.py +0 -0
  85. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/optimizers/__init__.py +0 -0
  86. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  87. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/optimizers/resume.py +0 -0
  88. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/optimizers/tracker.py +0 -0
  89. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/packaging.py +0 -0
  90. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/program_naming.py +0 -0
  91. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/programs.py +0 -0
  92. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/references.py +0 -0
  93. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/retry.py +0 -0
  94. {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/traces.py +0 -0
  95. {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/2026-04-02-code-review.md +0 -0
  96. {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/client.md +0 -0
  97. {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/custom-proposer.md +0 -0
  98. {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  99. {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  100. {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/reading-traces.md +0 -0
  101. {cmpnd-0.77.0 → cmpnd-0.79.0}/examples/generalizing_proposer.py +0 -0
  102. {cmpnd-0.77.0 → cmpnd-0.79.0}/examples/local_ollama.py +0 -0
  103. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/LINEAGE.md +0 -0
  104. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/README.md +0 -0
  105. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  106. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  107. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
  108. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  109. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/program_names.py +0 -0
  110. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  111. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/py.typed +0 -0
  112. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  113. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/retry.py +0 -0
  114. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  115. {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/pyproject.toml +0 -0
  116. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/audit-sdist.sh +0 -0
  117. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/capture_review_run.py +0 -0
  118. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/check-pip-install.sh +0 -0
  119. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/check_release_artifacts.py +0 -0
  120. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/fixtures/review_run.json.gz +0 -0
  121. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/gepa_resume_bug.py +0 -0
  122. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/install-test-env.sh +0 -0
  123. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/release-gate.sh +0 -0
  124. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/release-real-models.sh +0 -0
  125. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/require_version_bump.py +0 -0
  126. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/seed_review_run.py +0 -0
  127. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/selfhost_compat.py +0 -0
  128. {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/test-in-linux.sh +0 -0
  129. {cmpnd-0.77.0 → cmpnd-0.79.0}/stubs/dspy/__init__.pyi +0 -0
  130. {cmpnd-0.77.0 → cmpnd-0.79.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  131. {cmpnd-0.77.0 → cmpnd-0.79.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  132. {cmpnd-0.77.0 → cmpnd-0.79.0}/stubs/dspy/utils/__init__.pyi +0 -0
  133. {cmpnd-0.77.0 → cmpnd-0.79.0}/stubs/dspy/utils/callback.pyi +0 -0
  134. {cmpnd-0.77.0 → cmpnd-0.79.0}/stubs/dspy/utils/dummies.pyi +0 -0
  135. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/__init__.py +0 -0
  136. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_credential_route.py +0 -0
  137. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_deployment_signature_helpers.py +0 -0
  138. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_exporter_helpers.py +0 -0
  139. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_fake_clock.py +0 -0
  140. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_identity_helpers.py +0 -0
  141. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_resolve_refs.py +0 -0
  142. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_traced_run.py +0 -0
  143. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/conftest.py +0 -0
  144. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/__init__.py +0 -0
  145. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/committee_task.py +0 -0
  146. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/conftest.py +0 -0
  147. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/continuation_task.py +0 -0
  148. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  149. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  150. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/fake_lm.py +0 -0
  151. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/helpers.py +0 -0
  152. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/target_capabilities.py +0 -0
  153. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_assessments_cli.py +0 -0
  154. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_capture_prompts.py +0 -0
  155. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_composite_module.py +0 -0
  156. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  157. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_deploy_execute.py +0 -0
  158. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_deploy_pandas.py +0 -0
  159. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_deploy_programs.py +0 -0
  160. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_edge_cases.py +0 -0
  161. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_example_hash.py +0 -0
  162. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_gepa_optimization.py +0 -0
  163. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_interpreter_spans.py +0 -0
  164. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_lm_accuracy.py +0 -0
  165. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_module_tracing.py +0 -0
  166. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimization_lineage.py +0 -0
  167. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  168. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_authored_metric.py +0 -0
  169. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_continuation.py +0 -0
  170. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_custom_proposer.py +0 -0
  171. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_failures.py +0 -0
  172. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_loop.py +0 -0
  173. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  174. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  175. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_parallel_export.py +0 -0
  176. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_project_attribution.py +0 -0
  177. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_react_tool_spans.py +0 -0
  178. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_save_load.py +0 -0
  179. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_trace_structure.py +0 -0
  180. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_traces_read.py +0 -0
  181. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_user_attribution.py +0 -0
  182. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_valsets_cli.py +0 -0
  183. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  184. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  185. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  186. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  187. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  188. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  189. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  190. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  191. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  192. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/10_react.json +0 -0
  193. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/11_refine.json +0 -0
  194. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/12_retrieve.json +0 -0
  195. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/13_knn.json +0 -0
  196. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/14_parallel.json +0 -0
  197. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/README.md +0 -0
  198. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/ingest_conformance.py +0 -0
  199. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/__init__.py +0 -0
  200. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/authored_optimize/judge.py +0 -0
  201. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  202. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/conftest.py +0 -0
  203. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  204. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/hosted_optimize_probe.py +0 -0
  205. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/optimization_run_probe.py +0 -0
  206. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  207. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/test_logs_renders_traces.py +0 -0
  208. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/test_optimization_run.py +0 -0
  209. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/test_optimizing_progress.py +0 -0
  210. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/test_unhappy_path.py +0 -0
  211. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/__init__.py +0 -0
  212. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/__main__.py +0 -0
  213. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/drain.py +0 -0
  214. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/ingest.py +0 -0
  215. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/latency_transport.py +0 -0
  216. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/rlm_shape.py +0 -0
  217. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/rlm_size_report.py +0 -0
  218. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/sweep.py +0 -0
  219. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/test_rlm_shape.py +0 -0
  220. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/test_rlm_size_report.py +0 -0
  221. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/__init__.py +0 -0
  222. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/conftest.py +0 -0
  223. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/seed_parity.py +0 -0
  224. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_auth.py +0 -0
  225. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_datasets.py +0 -0
  226. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_deployments.py +0 -0
  227. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_evals.py +0 -0
  228. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_health.py +0 -0
  229. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_modules.py +0 -0
  230. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_optimizations.py +0 -0
  231. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_programs.py +0 -0
  232. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_signatures.py +0 -0
  233. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_trace_streaming.py +0 -0
  234. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_traces.py +0 -0
  235. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/__init__.py +0 -0
  236. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/_provider.py +0 -0
  237. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/_readiness.py +0 -0
  238. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/_reliability.py +0 -0
  239. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/_result.py +0 -0
  240. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/mint_bedrock_token.py +0 -0
  241. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_bedrock_converse.py +0 -0
  242. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_deploy_programs.py +0 -0
  243. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_multi_client_stress.py +0 -0
  244. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_optimize_client_committee.py +0 -0
  245. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_optimize_continuation.py +0 -0
  246. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_optimize_loop.py +0 -0
  247. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_optimize_loop_wasm.py +0 -0
  248. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_optimize_stress.py +0 -0
  249. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_provider_roles.py +0 -0
  250. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_readiness.py +0 -0
  251. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_real_provider_twins.py +0 -0
  252. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_reliability.py +0 -0
  253. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_reliability_classify.py +0 -0
  254. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_rlm_wasm.py +0 -0
  255. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_trace_search.py +0 -0
  256. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/span_reference_fixture.py +0 -0
  257. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_adapter_reentrancy.py +0 -0
  258. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_assessments_api.py +0 -0
  259. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_authored.py +0 -0
  260. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_authored_binding_credentials.py +0 -0
  261. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_authored_bundle_collisions.py +0 -0
  262. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_authored_metric_contract.py +0 -0
  263. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_authored_named_models.py +0 -0
  264. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_authored_optimize.py +0 -0
  265. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_callback.py +0 -0
  266. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_candidate_lineage.py +0 -0
  267. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/__init__.py +0 -0
  268. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/conftest.py +0 -0
  269. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_api_client.py +0 -0
  270. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_assessments.py +0 -0
  271. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_auth.py +0 -0
  272. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_credentials.py +0 -0
  273. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_datasets.py +0 -0
  274. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_deployment_runs.py +0 -0
  275. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_deployments.py +0 -0
  276. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_evals.py +0 -0
  277. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_login.py +0 -0
  278. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_main.py +0 -0
  279. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_optimizations.py +0 -0
  280. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_org.py +0 -0
  281. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_pagination.py +0 -0
  282. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_parity.py +0 -0
  283. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_programs.py +0 -0
  284. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_provider_credentials.py +0 -0
  285. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_schema_sample.py +0 -0
  286. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_shell.py +0 -0
  287. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_tool_servers.py +0 -0
  288. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_top.py +0 -0
  289. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_traces.py +0 -0
  290. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_traces_program_and_module.py +0 -0
  291. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  292. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_tree_help.py +0 -0
  293. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_valsets.py +0 -0
  294. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_client.py +0 -0
  295. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_client_floor.py +0 -0
  296. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_client_program_names.py +0 -0
  297. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_clustering/__init__.py +0 -0
  298. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_clustering/conftest.py +0 -0
  299. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_clustering/test_edge_cases.py +0 -0
  300. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  301. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_clustering/test_type_conversion.py +0 -0
  302. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cmp551_grouping_identity.py +0 -0
  303. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_configuration.py +0 -0
  304. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_context.py +0 -0
  305. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_context_propagation.py +0 -0
  306. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_credential_kwargs.py +0 -0
  307. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_credential_selection.py +0 -0
  308. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_credential_values.py +0 -0
  309. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_dataset_polling.py +0 -0
  310. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_dataset_sync.py +0 -0
  311. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_datasets.py +0 -0
  312. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_decision_client_spans.py +0 -0
  313. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_decorators.py +0 -0
  314. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_deploy.py +0 -0
  315. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  316. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_deploy_program_id.py +0 -0
  317. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_deployment_metadata.py +0 -0
  318. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_deployment_runs.py +0 -0
  319. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_deployments_read.py +0 -0
  320. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_eval.py +0 -0
  321. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_eval_example_attribution.py +0 -0
  322. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_eval_program_id.py +0 -0
  323. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_eval_start.py +0 -0
  324. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_eval_trace_grouping.py +0 -0
  325. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_exception_paths.py +0 -0
  326. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_execute.py +0 -0
  327. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_execute_run_identity.py +0 -0
  328. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_exporter.py +0 -0
  329. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_exporter_program_names.py +0 -0
  330. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_exporter_queue.py +0 -0
  331. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_exporter_real.py +0 -0
  332. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_fake_lm.py +0 -0
  333. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_gepa_callback.py +0 -0
  334. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_gepa_integration.py +0 -0
  335. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_gepa_output_normalization.py +0 -0
  336. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_gepa_resume.py +0 -0
  337. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_identity.py +0 -0
  338. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_imaging.py +0 -0
  339. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_import_weight.py +0 -0
  340. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_ingest_conformance.py +0 -0
  341. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_ingest_second_producer.py +0 -0
  342. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_integration.py +0 -0
  343. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_interpreter_callback.py +0 -0
  344. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_ir_encoder_properties.py +0 -0
  345. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_ir_golden_vectors.py +0 -0
  346. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_ir_roundtrip.py +0 -0
  347. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_lm15_native_calls.py +0 -0
  348. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_lm_configuration.py +0 -0
  349. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_lm_usage_shapes.py +0 -0
  350. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_mcp_tools.py +0 -0
  351. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_models.py +0 -0
  352. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_multi_client_stress_readiness.py +0 -0
  353. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_no_signature_hash_on_the_wire.py +0 -0
  354. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimization_program_names.py +0 -0
  355. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimization_progress.py +0 -0
  356. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimization_start.py +0 -0
  357. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize.py +0 -0
  358. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize_budget.py +0 -0
  359. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize_lift.py +0 -0
  360. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize_lms.py +0 -0
  361. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize_proposer.py +0 -0
  362. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize_reflection_lm.py +0 -0
  363. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize_terminal_failure.py +0 -0
  364. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_packaging.py +0 -0
  365. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_packaging_callable_source.py +0 -0
  366. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_packaging_mcp_tools.py +0 -0
  367. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_packaging_program_source.py +0 -0
  368. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_poll_until.py +0 -0
  369. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_program_id_deprecation.py +0 -0
  370. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_program_identity.py +0 -0
  371. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_program_lineage_persist.py +0 -0
  372. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_program_names_in_deploy.py +0 -0
  373. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_program_naming.py +0 -0
  374. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_programs_create.py +0 -0
  375. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_project_payload.py +0 -0
  376. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_real_provider_twins_readiness.py +0 -0
  377. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_reflector_properties.py +0 -0
  378. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_reflector_smoke.py +0 -0
  379. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_release_gate_script.py +0 -0
  380. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_release_real_models_script.py +0 -0
  381. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_replaced_names.py +0 -0
  382. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_repro_reactv2_bugs.py +0 -0
  383. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_require_version_bump.py +0 -0
  384. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_retry.py +0 -0
  385. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_reversible_failures.py +0 -0
  386. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_rlm_patch.py +0 -0
  387. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_runtime_tools.py +0 -0
  388. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_schemas.py +0 -0
  389. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_seal_lm_credential.py +0 -0
  390. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_seed_review_run.py +0 -0
  391. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_signature_inputs.py +0 -0
  392. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_span_references.py +0 -0
  393. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_span_sent_once.py +0 -0
  394. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_system_one.py +0 -0
  395. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_trace_context_concurrency.py +0 -0
  396. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_trace_fact_wire_contract.py +0 -0
  397. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_trace_facts.py +0 -0
  398. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_trace_render.py +0 -0
  399. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_traces_read.py +0 -0
  400. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_tracked_gepa.py +0 -0
  401. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_tracker_enrich.py +0 -0
  402. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_truncation.py +0 -0
  403. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_upload_auth_headers.py +0 -0
  404. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_valsets_api.py +0 -0
  405. {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.77.0
3
+ Version: 0.79.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>
@@ -9,6 +9,10 @@ module-style (``from . import identity``); basedpyright can't resolve
9
9
  """
10
10
 
11
11
  import contextvars
12
+ import functools
13
+ from collections.abc import Callable
14
+
15
+ from . import context
12
16
 
13
17
  _gepa_patched = False
14
18
 
@@ -39,6 +43,43 @@ def _gepa_supports_callbacks() -> bool:
39
43
  return False
40
44
 
41
45
 
46
+ # Marks a metric wrapper _judge_metric_calls installed, so a compile nested in another
47
+ # (TrackedGEPA under auto_instrument) leaves the outer wrapper in place. An attribute
48
+ # on our own wrapper, read with getattr, because the user's metric may be any
49
+ # callable: a dataclass instance is unhashable, so no set or dict can hold it.
50
+ _JUDGED_METRIC_MARK = "_cmpnd_judges_metric_calls"
51
+
52
+
53
+ def _judge_metric_calls(gepa) -> Callable[[], None]:
54
+ """Run each of ``gepa``'s metric calls as the metric phase; return the undo.
55
+
56
+ dspy.GEPA reads its metric from ``metric_fn`` once its compile starts, so
57
+ swapping in a wrapper for the compile's span reaches every metric call, those
58
+ DSPy Evaluate makes and those GEPA's feedback makes. A trace a grading module
59
+ roots inside a call is then tagged ``optimization_phase: metric``, as a hosted
60
+ run's judge traces are, while the rollout the metric grades ran before it and
61
+ carries no phase. The wrapper keeps the metric's signature (``functools.wraps``)
62
+ for DSPy's checks of it, and the undo restores the optimizer's own metric.
63
+ """
64
+ metric = getattr(gepa, "metric_fn", None)
65
+ if not callable(metric) or getattr(metric, _JUDGED_METRIC_MARK, False) is True:
66
+ return lambda: None
67
+
68
+ @functools.wraps(metric)
69
+ def judged_metric(*args, **kwargs):
70
+ with context.optimization_phase("metric"):
71
+ return metric(*args, **kwargs)
72
+
73
+ setattr(judged_metric, _JUDGED_METRIC_MARK, True)
74
+ gepa.metric_fn = judged_metric
75
+
76
+ def restore() -> None:
77
+ if getattr(gepa, "metric_fn", None) is judged_metric:
78
+ gepa.metric_fn = metric
79
+
80
+ return restore
81
+
82
+
42
83
  def _run_tracked_compile(
43
84
  gepa,
44
85
  student,
@@ -137,8 +178,10 @@ def _run_tracked_compile(
137
178
  trk.generated_program_name = program_naming.generated_program_name(student)
138
179
 
139
180
  restore_attribution = callback.start_trace_attribution()
181
+ restore_metric = None
140
182
 
141
183
  try:
184
+ restore_metric = _judge_metric_calls(gepa)
142
185
  result = run_original()
143
186
 
144
187
  if not has_callbacks:
@@ -164,6 +207,8 @@ def _run_tracked_compile(
164
207
  raise
165
208
 
166
209
  finally:
210
+ if restore_metric is not None:
211
+ restore_metric()
167
212
  restore_attribution()
168
213
 
169
214
 
@@ -517,6 +517,11 @@ def _finish_reason(response: Any) -> str | None:
517
517
  return "stop"
518
518
 
519
519
 
520
+ # The `optimization.role` an LM call in each GEPA phase is priced under. A metric scores a
521
+ # rollout, so its calls are rollout spend; reflection proposes the next candidate.
522
+ _OPTIMIZATION_PHASE_ROLES = {"metric": "rollout", "reflection": "reflection"}
523
+
524
+
520
525
  class _CallbackBase:
521
526
  """Shared state and span-tracking primitives for the CmpndCallback mixins."""
522
527
 
@@ -532,6 +537,7 @@ class _CallbackBase:
532
537
  max_output_field_size: int | None = None,
533
538
  stream_trace_starts: bool | None = None,
534
539
  own_config: configuration.Config | None = None,
540
+ optimization_phase: str | None = None,
535
541
  ) -> None:
536
542
  # A client passes its own config here; the auto_instrument callback leaves
537
543
  # it None and reads the process-global config live, so a reconfigure()
@@ -541,6 +547,19 @@ class _CallbackBase:
541
547
  # later cmpnd.configure(). That fixedness is what keeps a client
542
548
  # orthogonal to the global config.
543
549
  self._own_config = own_config
550
+ # The optimization phase this callback traces for a run it does not own: an
551
+ # executor that scores a hosted rollout with the author's metric ("metric") or runs
552
+ # the author's GEPA proposer ("reflection"). Such a process knows the program the
553
+ # run optimizes and not the run's id, which the ingest key it pushes with decides.
554
+ # With a phase set, an LM call outside any trace becomes a standalone GEPA trace as
555
+ # a local optimization's judge and reflection calls do, and a trace a module opens
556
+ # is tagged with the phase. None is every other caller.
557
+ if optimization_phase not in (None, *_OPTIMIZATION_PHASE_ROLES):
558
+ raise ValueError(
559
+ f"optimization_phase must be one of {sorted(_OPTIMIZATION_PHASE_ROLES)} or None, "
560
+ f"got {optimization_phase!r}"
561
+ )
562
+ self._optimization_phase = optimization_phase
544
563
  # Set once this callback has warned that a declared name replaced the program
545
564
  # id its config read from the environment, so the warning appears once per client.
546
565
  self._warned_name_over_id = False
@@ -1348,6 +1367,12 @@ class _ModuleMixin(_CallbackBase):
1348
1367
  # Apply default tags
1349
1368
  if config and config.default_tags:
1350
1369
  trace.tags.update(config.default_tags)
1370
+ # A trace opened while this callback traces a phase, or while a local run
1371
+ # calls its metric, is the metric's or the proposer's, never a rollout, even
1372
+ # when its module is the program's own class.
1373
+ phase = self._optimization_phase or context.get_current_optimization_phase()
1374
+ if phase is not None:
1375
+ trace.tags["optimization_phase"] = phase
1351
1376
 
1352
1377
  return trace
1353
1378
 
@@ -1401,7 +1426,9 @@ class _LMMixin(_CallbackBase):
1401
1426
  span.model_name = getattr(instance, "model", None)
1402
1427
  span.model_provider = helpers.infer_provider(instance)
1403
1428
 
1404
- if self._active_optimization_run_id is not None or context.get_current_optimization_run_id() is not None:
1429
+ if self._optimization_phase is not None:
1430
+ span.attributes["optimization.role"] = _OPTIMIZATION_PHASE_ROLES[self._optimization_phase]
1431
+ elif self._active_optimization_run_id is not None or context.get_current_optimization_run_id() is not None:
1405
1432
  span.attributes["optimization.role"] = "rollout"
1406
1433
 
1407
1434
  # The usage fields read one named value each out of the raw kwargs. The
@@ -1603,7 +1630,9 @@ class _LMMixin(_CallbackBase):
1603
1630
  # must NOT also be added to _orphaned_lm_usage / untraced_lm_usage — that
1604
1631
  # would double-count them in the optimization total.
1605
1632
  current_trace = context.get_current_trace()
1606
- if current_trace is None and self._active_optimization_run_id is not None:
1633
+ if current_trace is None and (
1634
+ self._active_optimization_run_id is not None or self._optimization_phase is not None
1635
+ ):
1607
1636
  self._emit_optimization_lm_trace(span)
1608
1637
  return # handled as a standalone GEPA-process trace
1609
1638
 
@@ -1644,14 +1673,18 @@ class _LMMixin(_CallbackBase):
1644
1673
  carries the distinction. Each becomes a standalone one-span
1645
1674
  trace so the prompt, response, tokens and cost are visible alongside the
1646
1675
  student traces instead of being reduced to an aggregate token count.
1676
+
1677
+ A callback built with an ``optimization_phase`` takes its phase from that
1678
+ instead, and may have no run id: it traces the metric or the proposer of a
1679
+ run whose id the ingest key the trace is pushed with decides.
1647
1680
  """
1648
1681
  run_id = self._active_optimization_run_id
1649
- if not run_id:
1682
+ if not run_id and self._optimization_phase is None:
1650
1683
  return
1651
1684
 
1652
- phase = "metric" if self._active_eval_run is not None else "reflection"
1685
+ phase = self._optimization_phase or ("metric" if self._active_eval_run is not None else "reflection")
1653
1686
  span.attributes["gepa.phase"] = phase
1654
- span.attributes["optimization.role"] = "reflection" if phase == "reflection" else "rollout"
1687
+ span.attributes["optimization.role"] = _OPTIMIZATION_PHASE_ROLES[phase]
1655
1688
 
1656
1689
  trace = models.TraceBuilder(
1657
1690
  trace_id=span.trace_id or uuid4(),
@@ -182,8 +182,14 @@ def _register_user(sub: _SubParsersAction[ArgumentParser]) -> None:
182
182
  create.add_argument("--admin", dest="admin", action="store_true", help="Grant platform-admin")
183
183
  create.set_defaults(func=handle_user_create)
184
184
 
185
- approve = usub.add_parser("approve", help="Approve a pending signup")
185
+ approve = usub.add_parser("approve", help="Approve a pending membership")
186
186
  approve.add_argument("id", help="User id")
187
+ approve.add_argument(
188
+ "--org-id",
189
+ dest="org_id",
190
+ default="",
191
+ help="Org whose membership to approve (default: the user's active org)",
192
+ )
187
193
  approve.set_defaults(func=handle_user_approve)
188
194
 
189
195
  promote = usub.add_parser("promote", help="Grant platform-admin")
@@ -457,7 +463,30 @@ def handle_user_create(client: api_client.ApiClient, args: Any) -> Any:
457
463
 
458
464
 
459
465
  def handle_user_approve(client: api_client.ApiClient, args: Any) -> Any:
460
- return client.patch(f"/api/v1/admin/users/{args.id}", json={"is_approved": True})
466
+ user_path = f"/api/v1/admin/users/{args.id}"
467
+ org_id = args.org_id or client.get(user_path)["org_id"]
468
+ try:
469
+ return client.post(f"{user_path}/approve", json={"org_id": org_id})
470
+ except api_client.CliError as err:
471
+ if not _route_unmounted(err):
472
+ raise
473
+ # A server older than the approve route approves only the active org,
474
+ # through PATCH.
475
+ if args.org_id:
476
+ raise api_client.CliError(
477
+ "This server cannot approve a membership in a named org. Upgrade it, or omit --org-id "
478
+ "to approve the membership in the user's active org."
479
+ ) from err
480
+ return client.patch(user_path, json={"is_approved": True})
481
+
482
+
483
+ def _route_unmounted(err: api_client.CliError) -> bool:
484
+ """Whether a 404 came from a server that has no such route.
485
+
486
+ Go's ServeMux answers an unmounted route with plain text. A mounted route's
487
+ own 404 is JSON, such as an approval for a membership that does not exist.
488
+ """
489
+ return err.status_code == 404 and "404 page not found" in str(err)
461
490
 
462
491
 
463
492
  def handle_user_promote(client: api_client.ApiClient, args: Any) -> Any:
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Generator
6
+ from contextlib import contextmanager
5
7
  from contextvars import ContextVar
6
8
  from datetime import datetime, timezone
7
9
  from typing import TYPE_CHECKING, Any
@@ -27,6 +29,12 @@ _current_eval_run: ContextVar[models.EvalRunBuilder | None] = ContextVar("cmpnd_
27
29
  # evals). Prefixed text id (`op_…`), minted via cmpnd.identity.
28
30
  _current_optimization_run_id: ContextVar[str | None] = ContextVar("cmpnd_current_optimization_run_id", default=None)
29
31
 
32
+ # The optimization phase the code running now serves, when that code is an
33
+ # optimizer's metric rather than the program it optimizes. A local GEPA run sets it
34
+ # around each metric call, so a trace the metric's grading module roots is tagged as
35
+ # the metric's, as a hosted run's kernel tags it through its callback's own phase.
36
+ _current_optimization_phase: ContextVar[str | None] = ContextVar("cmpnd_current_optimization_phase", default=None)
37
+
30
38
 
31
39
  def get_current_trace() -> models.TraceBuilder | None:
32
40
  """Get the current trace."""
@@ -79,6 +87,21 @@ def set_current_optimization_run_id(optimization_run_id: str | None) -> None:
79
87
  _current_optimization_run_id.set(optimization_run_id)
80
88
 
81
89
 
90
+ def get_current_optimization_phase() -> str | None:
91
+ """The optimization phase the running code serves, or None outside a metric call."""
92
+ return _current_optimization_phase.get()
93
+
94
+
95
+ @contextmanager
96
+ def optimization_phase(phase: str) -> Generator[None]:
97
+ """Run the block as the optimization phase ``phase``, restoring the outer phase after."""
98
+ token = _current_optimization_phase.set(phase)
99
+ try:
100
+ yield
101
+ finally:
102
+ _current_optimization_phase.reset(token)
103
+
104
+
82
105
  # The program each optimization run this process started optimizes, by run id. A trace
83
106
  # linked to a run that names no program of its own takes its run's, so everything a run
84
107
  # emits is reachable through that program: GEPA's reflection and judge calls, a judge
@@ -148,4 +171,5 @@ def clear_context() -> None:
148
171
  _span_stack.set(())
149
172
  _current_eval_run.set(None)
150
173
  _current_optimization_run_id.set(None)
174
+ _current_optimization_phase.set(None)
151
175
  _optimization_program_ids.clear()
@@ -205,9 +205,11 @@ class TrackedGEPA:
205
205
  existing_callbacks = self._gepa.gepa_kwargs.get("callbacks") or []
206
206
  self._gepa.gepa_kwargs["callbacks"] = existing_callbacks + [gcb]
207
207
  restore_attribution = None
208
+ restore_metric = None
208
209
 
209
210
  try:
210
211
  restore_attribution = gcb.start_trace_attribution()
212
+ restore_metric = _gepa_patch._judge_metric_calls(self._gepa)
211
213
  # Run optimization (trainset/valset are keyword-only in GEPA)
212
214
  compile_token = _gepa_patch._tracked_compile_optimizer.set(self._gepa)
213
215
  try:
@@ -246,6 +248,8 @@ class TrackedGEPA:
246
248
  raise
247
249
 
248
250
  finally:
251
+ if restore_metric is not None:
252
+ restore_metric()
249
253
  if restore_attribution is not None:
250
254
  restore_attribution()
251
255
  # Remove our callback from gepa_kwargs
@@ -494,6 +494,8 @@ stores one key per org per endpoint in `~/.config/cmpnd/credentials.json`, or
494
494
  `$CMPND_CONFIG_DIR/credentials.json` when that variable is set. The file has
495
495
  mode 0600. An active pointer names the org whose key commands use.
496
496
  `cmpnd login` stores a new key beside the others and makes its org active.
497
+ When you belong to more than one org, the approval page asks which org the
498
+ key is for and starts on your active org in the web app.
497
499
 
498
500
  - `cmpnd login --org <slug-or-id>` mints a key for that org instead of the
499
501
  active one. The approval page names the org, and serve refuses an org where
@@ -891,6 +893,14 @@ EMAIL` withdraws the open owner invites sent to that address, and the links
891
893
  stop working. `cmpnd admin org show` lists pending addresses under
892
894
  `seed.pending_owner_emails`.
893
895
 
896
+ `cmpnd admin user approve USER_ID --org-id ORG_ID` approves the user's pending
897
+ membership in that org, which need not be their active org. Without
898
+ `--org-id` it approves the membership in their active org, and a server
899
+ older than the approve route approves it the way older releases did. Serve applies the
900
+ operator queue's rules. A member waiting to join an org that has its own owner
901
+ belongs to that owner's queue, so serve refuses it with 400. `cmpnd admin user
902
+ reject USER_ID --org-id ORG_ID` declines a pending membership.
903
+
894
904
  ### Plans, limits and signups
895
905
 
896
906
  Every org has a plan, `free`, `pro` or `enterprise`, and each plan sets
@@ -180,6 +180,7 @@ Everything lives in `python-sdk/cmpnd/context.py`:
180
180
  - `_span_stack` — an immutable tuple of `SpanBuilder`s. `push_span` and `pop_span` set a new tuple rather than mutating the old one, because a context copy shares the objects its parent context holds: a mutable stack would be one list shared by every sibling copy, so concurrent runs (GEPA's `DspyAdapter.batch_evaluate`, which runs each candidate in a `copy_context()`, or `asyncio.gather` over `acall`) would read each other's spans as their parent. `get_current_span` returns the innermost. `tests/test_trace_context_concurrency.py` pins this.
181
181
  - `_current_eval_run` (`context.py:24`) — set while an `EvalHandler` is active.
182
182
  - `_current_optimization_run_id` (`context.py:29`) — set while a patched `GEPA.compile` is running, so evals triggered inside it can be linked back.
183
+ - `_current_optimization_phase` — set to `"metric"` for the span of each metric call a tracked GEPA compile makes, so a trace the metric's grading module roots is tagged as the metric's. `context.optimization_phase(phase)` sets it for a block.
183
184
 
184
185
  `create_child_span` at `context.py:83` is the standard constructor: it reads the current trace + span, assigns `trace_id` and `parent_span_id`, and returns an unpushed `SpanBuilder`. The callback uses it for every child span; so does the `start_span` context manager below. Pass `root=True` to have it ignore the stack and leave `parent_span_id` unset, for a span that starts a trace beneath one that does not parent it.
185
186
 
@@ -202,7 +203,41 @@ callback never fires. This prevents an ordinary evaluation after a failure from
202
203
  being queued under the failed run or being claimed as the next optimization's
203
204
  seed evidence.
204
205
 
205
- For tests that need a clean slate, `clear_context()` at `context.py:122` resets all four vars — see the `cmpnd-testing` skill and [`../AGENTS.md`](../AGENTS.md) § "Running Tests & Checks".
206
+ ### A callback that traces one phase of a run it does not own
207
+
208
+ `CmpndCallback(optimization_phase=...)` takes `"metric"` or `"reflection"`. A worker
209
+ kernel builds one around a hosted run's metric and around its custom GEPA proposer.
210
+ That process knows the program the run optimizes, from its config, and not the run's
211
+ id, which serve stamps from the ingest key the traces are pushed with. Such a callback
212
+ makes those calls look like a local optimization's:
213
+
214
+ - An LM call outside any trace becomes the standalone one-span `GEPA` trace that
215
+ `_emit_optimization_lm_trace` writes for a local run. Its span carries `gepa.phase`
216
+ and `optimization.role`, and its trace carries the `optimization_phase` tag. The
217
+ callback takes the phase from its constructor, not from an active eval run, and
218
+ leaves `optimization_run_id` unset.
219
+ - A trace that a dspy module opens is tagged `optimization_phase`, so a program's trace
220
+ list tells a judge module from a rollout of the same class.
221
+ - Every LM span gets `optimization.role`: `rollout` for a metric, `reflection` for a
222
+ proposer.
223
+
224
+ A local run owns its run, so its callback declares no phase. It tags its judge traces
225
+ through context instead. While a tracked GEPA compile runs, through `auto_instrument`,
226
+ `TrackedGEPA` or a client's `optimize_program`, `_gepa_patch._judge_metric_calls`
227
+ swaps the optimizer's `metric_fn` for a wrapper that runs each call under
228
+ `context.optimization_phase("metric")`, and restores the metric when the compile ends.
229
+ `_create_trace` reads that phase when its callback has none. So a judge module the
230
+ metric grades through roots a trace tagged `optimization_phase: metric`, as a hosted
231
+ judge's module trace is. The rollout the metric grades ran before the call and carries
232
+ no tag. The context phase changes only that tag: a direct judge or reflection call is
233
+ still the trace `_emit_optimization_lm_trace` builds, and module spans get no
234
+ `optimization.role`.
235
+
236
+ `tests/test_optimization_phase_callback.py` drives both paths, and the context phase,
237
+ through real DSPy dispatch. `tests/test_run_attribution_paths.py` runs a real GEPA
238
+ search whose metric grades through a module.
239
+
240
+ For tests that need a clean slate, `clear_context()` in `context.py` resets these vars — see the `cmpnd-testing` skill and [`../AGENTS.md`](../AGENTS.md) § "Running Tests & Checks".
206
241
 
207
242
  ### Across a real fork
208
243
 
@@ -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.77.0"
8
+ version = "0.79.0"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"
@@ -2,8 +2,15 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  from argparse import Namespace
7
+ from collections.abc import Callable
8
+ from unittest import mock
6
9
 
10
+ import httpx
11
+ import pytest
12
+
13
+ from cmpnd.cli import api_client
7
14
  from cmpnd.cli.commands import admin
8
15
 
9
16
 
@@ -183,11 +190,15 @@ class TestAdminUser:
183
190
  },
184
191
  )
185
192
 
186
- def test_approve_patches_is_approved(self, mock_client):
187
- mock_client.patch.return_value = {"id": "u_1", "is_approved": True}
188
- result = admin.handle_user_approve(mock_client, Namespace(id="u_1"))
189
- mock_client.patch.assert_called_once_with("/api/v1/admin/users/u_1", json={"is_approved": True})
190
- assert result["is_approved"] is True
193
+ def test_approve_parser_takes_an_optional_org(self):
194
+ from argparse import ArgumentParser
195
+
196
+ parser = ArgumentParser()
197
+ admin.register(parser.add_subparsers())
198
+ args = parser.parse_args(["admin", "user", "approve", "u_1", "--org-id", "o_2"])
199
+ assert args.org_id == "o_2"
200
+ assert args.func is admin.handle_user_approve
201
+ assert parser.parse_args(["admin", "user", "approve", "u_1"]).org_id == ""
191
202
 
192
203
  def test_promote_patches_is_admin_true(self, mock_client):
193
204
  mock_client.patch.return_value = {"id": "u_1", "is_admin": True}
@@ -413,3 +424,78 @@ class TestAdminSignup:
413
424
  with pytest.raises(SystemExit):
414
425
  args.func(mock_client, args)
415
426
  mock_client.patch.assert_not_called()
427
+
428
+
429
+ def _serve(answer: Callable[[httpx.Request], httpx.Response]):
430
+ """A real ApiClient over a captured transport: `sent` collects each request."""
431
+ sent: list[httpx.Request] = []
432
+
433
+ def handle(request: httpx.Request) -> httpx.Response:
434
+ sent.append(request)
435
+ return answer(request)
436
+
437
+ real = httpx.Client
438
+ patch = mock.patch("httpx.Client", lambda **kwargs: real(**kwargs, transport=httpx.MockTransport(handle)))
439
+ return api_client.ApiClient("k", "http://serve.test"), sent, patch
440
+
441
+
442
+ def _wire(sent: list[httpx.Request]) -> list[tuple[str, str, object]]:
443
+ return [(r.method, r.url.path, json.loads(r.content) if r.content else None) for r in sent]
444
+
445
+
446
+ # Go's ServeMux answers an unmounted route with this plain-text 404.
447
+ _UNMOUNTED = httpx.Response(404, text="404 page not found\n")
448
+
449
+
450
+ class TestAdminUserApproveWire:
451
+ def test_approve_posts_the_named_org(self):
452
+ client, sent, patch = _serve(lambda _r: httpx.Response(200, json={"newly_approved": True}))
453
+ with patch:
454
+ result = admin.handle_user_approve(client, _parse("user", "approve", "u_1", "--org-id", "o_2"))
455
+ assert _wire(sent) == [("POST", "/api/v1/admin/users/u_1/approve", {"org_id": "o_2"})]
456
+ assert result["newly_approved"] is True
457
+
458
+ def test_approve_without_org_approves_the_active_org(self):
459
+ def answer(request: httpx.Request) -> httpx.Response:
460
+ if request.method == "GET":
461
+ return httpx.Response(200, json={"id": "u_1", "org_id": "o_1"})
462
+ return httpx.Response(200, json={"newly_approved": True})
463
+
464
+ client, sent, patch = _serve(answer)
465
+ with patch:
466
+ admin.handle_user_approve(client, _parse("user", "approve", "u_1"))
467
+ assert _wire(sent) == [
468
+ ("GET", "/api/v1/admin/users/u_1", None),
469
+ ("POST", "/api/v1/admin/users/u_1/approve", {"org_id": "o_1"}),
470
+ ]
471
+
472
+ def test_approve_without_org_falls_back_to_patch_on_a_server_without_the_route(self):
473
+ def answer(request: httpx.Request) -> httpx.Response:
474
+ if request.method == "GET":
475
+ return httpx.Response(200, json={"id": "u_1", "org_id": "o_1"})
476
+ if request.method == "POST":
477
+ return _UNMOUNTED
478
+ return httpx.Response(200, json={"id": "u_1", "is_approved": True})
479
+
480
+ client, sent, patch = _serve(answer)
481
+ with patch:
482
+ result = admin.handle_user_approve(client, _parse("user", "approve", "u_1"))
483
+ assert _wire(sent)[-1] == ("PATCH", "/api/v1/admin/users/u_1", {"is_approved": True})
484
+ assert result["is_approved"] is True
485
+
486
+ def test_approve_with_org_on_a_server_without_the_route_says_so(self):
487
+ client, sent, patch = _serve(lambda _r: _UNMOUNTED)
488
+ with patch, pytest.raises(api_client.CliError, match="--org-id"):
489
+ admin.handle_user_approve(client, _parse("user", "approve", "u_1", "--org-id", "o_2"))
490
+ assert [r.method for r in sent] == ["POST"]
491
+
492
+ def test_a_missing_membership_is_not_mistaken_for_an_old_server(self):
493
+ def answer(request: httpx.Request) -> httpx.Response:
494
+ if request.method == "GET":
495
+ return httpx.Response(200, json={"id": "u_1", "org_id": "o_1"})
496
+ return httpx.Response(404, json={"error": "no membership for that user in that org"})
497
+
498
+ client, sent, patch = _serve(answer)
499
+ with patch, pytest.raises(api_client.CliError, match="no membership"):
500
+ admin.handle_user_approve(client, _parse("user", "approve", "u_1"))
501
+ assert [r.method for r in sent] == ["GET", "POST"]