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.
- {cmpnd-0.77.0 → cmpnd-0.79.0}/PKG-INFO +1 -1
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_gepa_patch.py +45 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/callback.py +38 -5
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/admin.py +31 -2
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/context.py +24 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/optimizers/gepa.py +4 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/cli.md +10 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/sdk-instrumentation.md +36 -1
- {cmpnd-0.77.0 → cmpnd-0.79.0}/pyproject.toml +1 -1
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_admin.py +91 -5
- cmpnd-0.79.0/tests/test_optimization_phase_callback.py +196 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_run_attribution_paths.py +101 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/uv.lock +1 -1
- {cmpnd-0.77.0 → cmpnd-0.79.0}/.gitignore +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/AGENTS.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/CLAUDE.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/README.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_assessments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_authored_import.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_deployments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_exporter_pool.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_http.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_runtime_tools.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_traces.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/_valsets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/authored.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/assessments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/tree_help.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/cli/views.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/client.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/configuration.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/credentials.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/datasets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/decorators.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/deployment.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/deployment_metadata.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/deployment_runs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/execution.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/exporter.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/helpers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/identity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/imaging.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/lm_configuration.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/mcp_servers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/models.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/named_programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/optimization.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/packaging.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/program_naming.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/references.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/retry.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/cmpnd/traces.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/client.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/custom-proposer.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/docs/reading-traces.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/examples/local_ollama.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/README.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/program_names.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/ingest/pyproject.toml +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/check-pip-install.sh +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/install-test-env.sh +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/release-gate.sh +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/release-real-models.sh +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/selfhost_compat.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/stubs/dspy/utils/dummies.pyi +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_credential_route.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_deployment_signature_helpers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_fake_clock.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_resolve_refs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/_traced_run.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/conftest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/continuation_task.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_assessments_cli.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_deploy_pandas.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_authored_metric.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_continuation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_custom_proposer.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_traces_read.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/e2e/test_valsets_cli.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/authored_optimize/judge.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/conftest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/__main__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/drain.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/ingest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/rlm_size_report.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/sweep.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/load/test_rlm_size_report.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/conftest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_health.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/_provider.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/_readiness.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/_reliability.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/_result.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/mint_bedrock_token.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_bedrock_converse.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_deploy_programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_multi_client_stress.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_optimize_client_committee.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_optimize_continuation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_optimize_loop.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_optimize_stress.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_provider_roles.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_readiness.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_real_provider_twins.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_reliability.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_reliability_classify.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_rlm_wasm.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/real_models/test_trace_search.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/span_reference_fixture.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_adapter_reentrancy.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_assessments_api.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_authored.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_authored_binding_credentials.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_authored_bundle_collisions.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_authored_metric_contract.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_authored_named_models.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_authored_optimize.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_callback.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_assessments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_deployment_runs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_traces_program_and_module.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_tree_help.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cli/test_valsets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_client.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_client_floor.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_client_program_names.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_configuration.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_context.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_credential_kwargs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_credential_selection.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_credential_values.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_datasets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_decision_client_spans.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_decorators.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_deploy.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_deploy_mcp_tool_references.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_deploy_program_id.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_deployment_metadata.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_deployment_runs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_deployments_read.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_eval.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_eval_program_id.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_eval_start.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_execute.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_execute_run_identity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_exporter.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_exporter_program_names.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_gepa_output_normalization.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_identity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_imaging.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_import_weight.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_integration.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_lm15_native_calls.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_lm_configuration.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_lm_usage_shapes.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_models.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_multi_client_stress_readiness.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_no_signature_hash_on_the_wire.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimization_program_names.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_packaging.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_poll_until.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_program_id_deprecation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_program_identity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_program_names_in_deploy.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_program_naming.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_programs_create.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_project_payload.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_real_provider_twins_readiness.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_release_gate_script.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_release_real_models_script.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_replaced_names.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_retry.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_runtime_tools.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_schemas.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_seal_lm_credential.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_signature_inputs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_span_references.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_span_sent_once.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_system_one.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_trace_context_concurrency.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_trace_fact_wire_contract.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_trace_facts.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_trace_render.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_traces_read.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_truncation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_upload_auth_headers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_valsets_api.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.79.0}/tests/test_xxh64.py +0 -0
|
@@ -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.
|
|
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
|
|
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"] =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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"]
|