cmpnd 0.19.0__tar.gz → 0.20.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cmpnd-0.19.0 → cmpnd-0.20.0}/PKG-INFO +1 -1
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/__init__.py +11 -0
- cmpnd-0.20.0/cmpnd/cli/commands/assessments.py +67 -0
- cmpnd-0.20.0/cmpnd/cli/commands/valsets.py +101 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/cli.md +47 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/pyproject.toml +1 -1
- cmpnd-0.20.0/tests/e2e/test_assessments_cli.py +68 -0
- cmpnd-0.20.0/tests/e2e/test_valsets_cli.py +102 -0
- cmpnd-0.20.0/tests/test_cli/test_assessments.py +84 -0
- cmpnd-0.20.0/tests/test_cli/test_valsets.py +122 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/uv.lock +1 -1
- {cmpnd-0.19.0 → cmpnd-0.20.0}/.gitignore +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/CLAUDE.md +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/README.md +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/_http.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/callback.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/configuration.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/context.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/datasets.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/decorators.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/deployment.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/execution.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/exporter.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/helpers.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/identity.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/imaging.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/mcp_servers.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/models.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/optimization.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/packaging.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/retry.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/cmpnd/traces.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/custom-proposer.md +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/reading-traces.md +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/docs/sdk-instrumentation.md +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/examples/local_ollama.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/README.md +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/ingest/pyproject.toml +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/_fake_clock.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/conftest.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_traces_read.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/conftest.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/__main__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/drain.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/ingest.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/sweep.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/conftest.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_health.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/_provider.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/_result.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/stress/test_trace_search.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_callback.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_signatures.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_configuration.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_context.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_datasets.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_decorators.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_deploy.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_deploy_mcp_tool_references.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_eval.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_eval_start.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_execute.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_exporter.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_identity.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_imaging.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_import_weight.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_integration.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_models.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_packaging.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_poll_until.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_project_payload.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_retry.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_schemas.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_stats_export.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_trace_render.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_traces_read.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_truncation.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/tests/test_xxh64.py +0 -0
- {cmpnd-0.19.0 → cmpnd-0.20.0}/todo.md +0 -0
|
@@ -13,6 +13,7 @@ import httpx
|
|
|
13
13
|
from . import api_client, deprecation
|
|
14
14
|
from .commands import (
|
|
15
15
|
admin,
|
|
16
|
+
assessments,
|
|
16
17
|
auth,
|
|
17
18
|
datasets,
|
|
18
19
|
deployments,
|
|
@@ -26,6 +27,7 @@ from .commands import (
|
|
|
26
27
|
tool_servers,
|
|
27
28
|
top,
|
|
28
29
|
traces,
|
|
30
|
+
valsets,
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
|
|
@@ -141,6 +143,15 @@ def main() -> None:
|
|
|
141
143
|
datasets.register(subparsers)
|
|
142
144
|
deployments.register(subparsers)
|
|
143
145
|
|
|
146
|
+
# Assessments and valsets CLIs are still in development, hidden behind
|
|
147
|
+
# CMPND_CLI_DEV=1 (mirroring the CMPND_CLI_ADMIN gate above). The env var
|
|
148
|
+
# only hides the commands from customers; the server still gates each action
|
|
149
|
+
# on the assessments:* / valsets:* scope, so this is discoverability, not
|
|
150
|
+
# enforcement.
|
|
151
|
+
if os.environ.get("CMPND_CLI_DEV") == "1":
|
|
152
|
+
assessments.register(subparsers)
|
|
153
|
+
valsets.register(subparsers)
|
|
154
|
+
|
|
144
155
|
# Top-level Docker-style shortcuts (human-readable by default; --json opts in)
|
|
145
156
|
top.register(subparsers)
|
|
146
157
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Assessments commands.
|
|
2
|
+
|
|
3
|
+
An assessment is a person's judgement of a trace: an up/down/neutral
|
|
4
|
+
conclusion, a prose body, and per-span annotations. See the assessments API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from argparse import ArgumentParser, _SubParsersAction
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .. import api_client
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
16
|
+
parser = subparsers.add_parser("assessments", help="Record and read judgements of traces")
|
|
17
|
+
sub = parser.add_subparsers(dest="action")
|
|
18
|
+
|
|
19
|
+
create = sub.add_parser("create", help="Record a judgement of a trace")
|
|
20
|
+
create.add_argument("trace_id", help="The trace to assess (its SDK trace id)")
|
|
21
|
+
conclusion = create.add_mutually_exclusive_group(required=True)
|
|
22
|
+
conclusion.add_argument(
|
|
23
|
+
"--up", dest="conclusion", action="store_const", const="up", help="Affirm the trace's output"
|
|
24
|
+
)
|
|
25
|
+
conclusion.add_argument(
|
|
26
|
+
"--down", dest="conclusion", action="store_const", const="down", help="Dispute the trace's output"
|
|
27
|
+
)
|
|
28
|
+
conclusion.add_argument(
|
|
29
|
+
"--neutral", dest="conclusion", action="store_const", const="neutral", help="Neither affirm nor dispute"
|
|
30
|
+
)
|
|
31
|
+
create.add_argument("-m", "--message", dest="body", help="The prose body of the judgement")
|
|
32
|
+
create.add_argument(
|
|
33
|
+
"--annotate",
|
|
34
|
+
dest="annotations",
|
|
35
|
+
action="append",
|
|
36
|
+
nargs=2,
|
|
37
|
+
metavar=("SPAN_ID", "NOTE"),
|
|
38
|
+
help="Attach a note to one span in the trace (repeat for multiple)",
|
|
39
|
+
)
|
|
40
|
+
create.set_defaults(func=handle_create)
|
|
41
|
+
|
|
42
|
+
ls = sub.add_parser("list", help="List a program's assessments")
|
|
43
|
+
ls.add_argument(
|
|
44
|
+
"--program", required=True, help="Program signature hash (16-hex, as `cmpnd signatures list` shows)"
|
|
45
|
+
)
|
|
46
|
+
ls.set_defaults(func=handle_list)
|
|
47
|
+
|
|
48
|
+
show = sub.add_parser("show", help="Show one assessment with its annotations")
|
|
49
|
+
show.add_argument("id", help="Assessment id")
|
|
50
|
+
show.set_defaults(func=handle_show)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def handle_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
54
|
+
body: dict[str, Any] = {"trace_id": args.trace_id, "conclusion": args.conclusion}
|
|
55
|
+
if args.body:
|
|
56
|
+
body["body"] = args.body
|
|
57
|
+
if args.annotations:
|
|
58
|
+
body["annotations"] = [{"span_id": span, "note": note} for span, note in args.annotations]
|
|
59
|
+
return client.post("/api/v1/assessments", json=body)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def handle_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
63
|
+
return client.get("/api/v1/assessments", params={"program": args.program})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def handle_show(client: api_client.ApiClient, args: Any) -> Any:
|
|
67
|
+
return client.get(f"/api/v1/assessments/{args.id}")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Valsets commands.
|
|
2
|
+
|
|
3
|
+
A valset is a program-owned validation set: a name, the field shape derived
|
|
4
|
+
from the owning program's signature, and its entries. Entries carry the
|
|
5
|
+
inputs and expected outputs of one validation example.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from argparse import ArgumentParser, _SubParsersAction
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .. import api_client
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
18
|
+
parser = subparsers.add_parser("valsets", help="Manage program-owned validation sets")
|
|
19
|
+
sub = parser.add_subparsers(dest="action")
|
|
20
|
+
|
|
21
|
+
create = sub.add_parser("create", help="Create a validation set owned by a program")
|
|
22
|
+
create.add_argument("program_hash", help="Owning program signature hash (16-hex, as `cmpnd signatures list` shows)")
|
|
23
|
+
create.add_argument("--name", required=True, help="Validation-set name (unique within the program)")
|
|
24
|
+
create.set_defaults(func=handle_create)
|
|
25
|
+
|
|
26
|
+
ls = sub.add_parser("list", help="List a program's validation sets")
|
|
27
|
+
ls.add_argument("--program", required=True, help="Program signature hash (16-hex)")
|
|
28
|
+
ls.set_defaults(func=handle_list)
|
|
29
|
+
|
|
30
|
+
show = sub.add_parser("show", help="Show a validation set")
|
|
31
|
+
show.add_argument("id", help="Valset id")
|
|
32
|
+
show.set_defaults(func=handle_show)
|
|
33
|
+
|
|
34
|
+
entries = sub.add_parser("entries", help="Manage a validation set's entries")
|
|
35
|
+
esub = entries.add_subparsers(dest="entries_action")
|
|
36
|
+
|
|
37
|
+
elist = esub.add_parser("list", help="List a valset's active entries")
|
|
38
|
+
elist.add_argument("valset_id", help="Valset id")
|
|
39
|
+
elist.set_defaults(func=handle_entries_list)
|
|
40
|
+
|
|
41
|
+
ecreate = esub.add_parser("create", help="Add an entry from JSON content")
|
|
42
|
+
ecreate.add_argument("valset_id", help="Valset id")
|
|
43
|
+
ecreate.add_argument(
|
|
44
|
+
"--inputs", required=True, metavar="JSON", help="Inputs object; its keys are the valset's input fields"
|
|
45
|
+
)
|
|
46
|
+
ecreate.add_argument(
|
|
47
|
+
"--expected-outputs",
|
|
48
|
+
dest="expected_outputs",
|
|
49
|
+
required=True,
|
|
50
|
+
metavar="JSON",
|
|
51
|
+
help="Expected-outputs object; its keys are the valset's output fields",
|
|
52
|
+
)
|
|
53
|
+
ecreate.add_argument(
|
|
54
|
+
"--from-assessment", dest="from_assessment", help="Source assessment id, recorded as the entry's provenance"
|
|
55
|
+
)
|
|
56
|
+
ecreate.set_defaults(func=handle_entries_create)
|
|
57
|
+
|
|
58
|
+
edelete = esub.add_parser("delete", help="Retire an entry (versioned, not a hard delete)")
|
|
59
|
+
edelete.add_argument("entry_id", help="Entry id")
|
|
60
|
+
edelete.set_defaults(func=handle_entries_delete)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def handle_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
64
|
+
return client.post("/api/v1/valsets", json={"program": args.program_hash, "name": args.name})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def handle_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
68
|
+
return client.get("/api/v1/valsets", params={"program": args.program})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def handle_show(client: api_client.ApiClient, args: Any) -> Any:
|
|
72
|
+
return client.get(f"/api/v1/valsets/{args.id}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def handle_entries_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
76
|
+
return client.get(f"/api/v1/valsets/{args.valset_id}/entries")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def handle_entries_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
80
|
+
inputs = _parse_json_flag("--inputs", args.inputs)
|
|
81
|
+
expected = _parse_json_flag("--expected-outputs", args.expected_outputs)
|
|
82
|
+
body: dict[str, Any] = {"inputs": inputs, "expected_outputs": expected}
|
|
83
|
+
if args.from_assessment:
|
|
84
|
+
body["from_assessment"] = args.from_assessment
|
|
85
|
+
return client.post(f"/api/v1/valsets/{args.valset_id}/entries", json=body)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def handle_entries_delete(client: api_client.ApiClient, args: Any) -> Any:
|
|
89
|
+
client.delete(f"/api/v1/valsets/entries/{args.entry_id}")
|
|
90
|
+
# The route returns 204; echo a confirmation so the CLI does not print null.
|
|
91
|
+
return {"deleted": args.entry_id}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _parse_json_flag(flag: str, value: str) -> dict[str, Any]:
|
|
95
|
+
try:
|
|
96
|
+
parsed = json.loads(value)
|
|
97
|
+
except json.JSONDecodeError as exc:
|
|
98
|
+
raise api_client.CliError(f"{flag} JSON is invalid: {exc}") from exc
|
|
99
|
+
if not isinstance(parsed, dict):
|
|
100
|
+
raise api_client.CliError(f"{flag} JSON must be an object")
|
|
101
|
+
return parsed
|
|
@@ -79,6 +79,27 @@ Every list command paginates with `--limit`/`--offset`. The older
|
|
|
79
79
|
warn. On the page-number commands `--offset` must be a whole multiple of
|
|
80
80
|
`--limit`; the error names the nearest valid offsets.
|
|
81
81
|
|
|
82
|
+
## Assessments
|
|
83
|
+
|
|
84
|
+
`cmpnd assessments` records and reads a person's judgement of a trace: an
|
|
85
|
+
up/down/neutral conclusion, an optional prose body, and per-span annotations.
|
|
86
|
+
|
|
87
|
+
> **In development — behind `CMPND_CLI_DEV=1`.** The group is registered only
|
|
88
|
+
> when that env var is set (mirroring `CMPND_CLI_ADMIN`), so it stays hidden from
|
|
89
|
+
> `cmpnd --help` until the flow is finished. The env var only hides the commands;
|
|
90
|
+
> the server still gates each action on the `assessments:*` scope.
|
|
91
|
+
|
|
92
|
+
- `cmpnd assessments create <trace-id> --up|--down|--neutral [-m "prose"] [--annotate <span-id> "note" …]`
|
|
93
|
+
— record one judgement. Exactly one conclusion flag is required (`--up`
|
|
94
|
+
affirms the trace's output, `--down` disputes it, `--neutral` does neither);
|
|
95
|
+
they are mutually exclusive. `-m`/`--message` sets the prose body. `--annotate`
|
|
96
|
+
takes a span id and a note, and repeats to attach a note to more than one span
|
|
97
|
+
in the trace.
|
|
98
|
+
- `cmpnd assessments list --program <16-hex-hash>` — the assessments recorded
|
|
99
|
+
against a program, addressed by its program signature hash in the 16-char hex
|
|
100
|
+
form `cmpnd signatures list` prints.
|
|
101
|
+
- `cmpnd assessments show <assessment-id>` — one assessment with its annotations.
|
|
102
|
+
|
|
82
103
|
## Other query groups
|
|
83
104
|
|
|
84
105
|
`cmpnd programs`, `cmpnd signatures`, `cmpnd evals`, `cmpnd datasets`,
|
|
@@ -99,6 +120,32 @@ found. Each group's `--help` names the column its ids come from.
|
|
|
99
120
|
`cmpnd deployments openapi <id>` prints the OpenAPI document a deployment
|
|
100
121
|
serves, which is what an agent needs before it can call one.
|
|
101
122
|
|
|
123
|
+
## Validation sets
|
|
124
|
+
|
|
125
|
+
`cmpnd valsets` manages a program-owned validation set: a named set of
|
|
126
|
+
validation examples whose field shape comes from the owning program's signature.
|
|
127
|
+
The program hash is the 16-hex signature hash throughout (what `cmpnd signatures
|
|
128
|
+
list` shows). Entries live in a nested `entries` subgroup, the same shape as
|
|
129
|
+
`cmpnd datasets examples`.
|
|
130
|
+
|
|
131
|
+
> **In development — behind `CMPND_CLI_DEV=1`.** Registered only when that env
|
|
132
|
+
> var is set (mirroring `CMPND_CLI_ADMIN`), so it stays hidden from
|
|
133
|
+
> `cmpnd --help` until the flow is finished. The env var only hides the commands;
|
|
134
|
+
> the server still gates each action on the `valsets:*` scope.
|
|
135
|
+
|
|
136
|
+
- `cmpnd valsets create <program-hash> --name <name>` — create a validation set
|
|
137
|
+
owned by a program; the name is unique within that program.
|
|
138
|
+
- `cmpnd valsets list --program <16-hex-hash>` — list a program's validation sets.
|
|
139
|
+
- `cmpnd valsets show <valset-id>` — show one validation set.
|
|
140
|
+
- `cmpnd valsets entries list <valset-id>` — list a valset's active entries.
|
|
141
|
+
- `cmpnd valsets entries create <valset-id> --inputs '{…}' --expected-outputs
|
|
142
|
+
'{…}' [--from-assessment <id>]` — add an entry. `--inputs` and
|
|
143
|
+
`--expected-outputs` are JSON objects keyed by the valset's declared input and
|
|
144
|
+
output fields. `--from-assessment` records a source assessment id as the
|
|
145
|
+
entry's provenance.
|
|
146
|
+
- `cmpnd valsets entries delete <entry-id>` — retire an entry (versioned, not a
|
|
147
|
+
hard delete).
|
|
148
|
+
|
|
102
149
|
## Credentials
|
|
103
150
|
|
|
104
151
|
`cmpnd credentials` covers two kinds of key, because both answer the same
|
|
@@ -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.20.0"
|
|
9
9
|
description = "DSPy observability and deployment SDK for cmpnd"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""End-to-end tests for the `cmpnd assessments` CLI against a live server.
|
|
2
|
+
|
|
3
|
+
The CLI is an API consumer, so these drive the real command handlers through a
|
|
4
|
+
real ApiClient against a live backend — the level that catches a contract
|
|
5
|
+
mismatch a mocked client cannot (e.g. the program-hash wire format). Skipped
|
|
6
|
+
unless CMPND_E2E_API_KEY is set (see conftest).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
|
|
13
|
+
import dspy
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from cmpnd.cli import api_client
|
|
17
|
+
from cmpnd.cli.commands import assessments as assessments_cmd
|
|
18
|
+
from tests.e2e.conftest import CMPND_E2E_API_KEY, E2E_ENDPOINT, flush_and_wait
|
|
19
|
+
|
|
20
|
+
pytestmark = pytest.mark.e2e
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ns(**kw: object) -> argparse.Namespace:
|
|
24
|
+
return argparse.Namespace(**kw)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestAssessmentsCliE2E:
|
|
28
|
+
def test_create_list_show_against_live_api(self, server_client, trace_capture):
|
|
29
|
+
# Produce a real trace to assess, then read back its program hash + a span.
|
|
30
|
+
dspy.Predict("question -> answer")(question="Who represents District 4?")
|
|
31
|
+
flush_and_wait()
|
|
32
|
+
trace_id = trace_capture.last_trace_id
|
|
33
|
+
assert trace_id is not None, "no trace captured"
|
|
34
|
+
data = server_client.get_trace(trace_id)
|
|
35
|
+
# The trace API returns the hash as a signed int64 (decimal); the
|
|
36
|
+
# assessments and modules APIs speak the 16-hex display form. Normalize
|
|
37
|
+
# so the create/list calls and the assertion compare like for like.
|
|
38
|
+
program_hash = format(int(data["trace"]["program_signature_hash"]) & 0xFFFFFFFFFFFFFFFF, "016x")
|
|
39
|
+
assert data["spans"], "trace has no spans to annotate"
|
|
40
|
+
span_id = data["spans"][0]["span_id"]
|
|
41
|
+
|
|
42
|
+
client = api_client.ApiClient(CMPND_E2E_API_KEY, E2E_ENDPOINT)
|
|
43
|
+
|
|
44
|
+
# create — a down judgement with one span annotation.
|
|
45
|
+
created = assessments_cmd.handle_create(
|
|
46
|
+
client,
|
|
47
|
+
_ns(
|
|
48
|
+
trace_id=trace_id,
|
|
49
|
+
conclusion="down",
|
|
50
|
+
body="the answer names the office, not the representative",
|
|
51
|
+
annotations=[[span_id, "this predict returned the wrong field"]],
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
assert created["id"].startswith("as_")
|
|
55
|
+
assert created["conclusion"] == "down"
|
|
56
|
+
# The program hash the server copied off the trace round-trips as the same
|
|
57
|
+
# 16-hex string the trace read returned — the CLI passes it through verbatim.
|
|
58
|
+
assert created["program_signature_hash"] == program_hash
|
|
59
|
+
assert created["annotations"][0]["span_id"] == span_id
|
|
60
|
+
|
|
61
|
+
# list by program (the 16-hex form) finds the assessment we just created.
|
|
62
|
+
listed = assessments_cmd.handle_list(client, _ns(program=program_hash))
|
|
63
|
+
assert created["id"] in [a["id"] for a in listed["assessments"]]
|
|
64
|
+
|
|
65
|
+
# show round-trips the body and the annotation note.
|
|
66
|
+
shown = assessments_cmd.handle_show(client, _ns(id=created["id"]))
|
|
67
|
+
assert shown["id"] == created["id"]
|
|
68
|
+
assert shown["annotations"][0]["note"] == "this predict returned the wrong field"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""End-to-end tests for the `cmpnd valsets` CLI against a live server.
|
|
2
|
+
|
|
3
|
+
Drives the real command handlers through a real ApiClient against a live
|
|
4
|
+
backend — the level that catches a CLI↔API contract mismatch a mocked client
|
|
5
|
+
cannot. Skipped unless CMPND_E2E_API_KEY is set (see conftest).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
import dspy
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from cmpnd.cli import api_client
|
|
17
|
+
from cmpnd.cli.commands import assessments as assessments_cmd
|
|
18
|
+
from cmpnd.cli.commands import valsets as valsets_cmd
|
|
19
|
+
from tests.e2e.conftest import CMPND_E2E_API_KEY, E2E_ENDPOINT, flush_and_wait
|
|
20
|
+
|
|
21
|
+
pytestmark = pytest.mark.e2e
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ns(**kw: object) -> argparse.Namespace:
|
|
25
|
+
return argparse.Namespace(**kw)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _program_and_trace(server_client, trace_capture) -> tuple[str, str, str]:
|
|
29
|
+
"""Run a program, return (program_hash, trace_id, first span id)."""
|
|
30
|
+
dspy.Predict("question -> answer")(question="Who represents District 4?")
|
|
31
|
+
flush_and_wait()
|
|
32
|
+
trace_id = trace_capture.last_trace_id
|
|
33
|
+
assert trace_id is not None, "no trace captured"
|
|
34
|
+
data = server_client.get_trace(trace_id)
|
|
35
|
+
# The trace API returns the hash as a signed int64 (decimal); the valsets
|
|
36
|
+
# and modules APIs speak the 16-hex display form, so normalize before the
|
|
37
|
+
# create/list calls that carry it.
|
|
38
|
+
program_hash = format(int(data["trace"]["program_signature_hash"]) & 0xFFFFFFFFFFFFFFFF, "016x")
|
|
39
|
+
return program_hash, trace_id, data["spans"][0]["span_id"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestValsetsCliE2E:
|
|
43
|
+
def test_valset_and_entry_lifecycle(self, server_client, trace_capture):
|
|
44
|
+
program_hash, _trace_id, _span = _program_and_trace(server_client, trace_capture)
|
|
45
|
+
client = api_client.ApiClient(CMPND_E2E_API_KEY, E2E_ENDPOINT)
|
|
46
|
+
|
|
47
|
+
# create derives the field shape from the owning program.
|
|
48
|
+
vs = valsets_cmd.handle_create(client, _ns(program_hash=program_hash, name=f"e2e gold {uuid.uuid4().hex[:8]}"))
|
|
49
|
+
assert vs["id"].startswith("vs_")
|
|
50
|
+
assert vs["program_signature_hash"] == program_hash
|
|
51
|
+
assert vs["input_fields"] == ["question"]
|
|
52
|
+
|
|
53
|
+
# list by program finds it; show returns the header.
|
|
54
|
+
listed = valsets_cmd.handle_list(client, _ns(program=program_hash))
|
|
55
|
+
assert vs["id"] in [v["id"] for v in listed["valsets"]]
|
|
56
|
+
assert valsets_cmd.handle_show(client, _ns(id=vs["id"]))["id"] == vs["id"]
|
|
57
|
+
|
|
58
|
+
# entries create — a manual entry matching the field shape.
|
|
59
|
+
entry = valsets_cmd.handle_entries_create(
|
|
60
|
+
client,
|
|
61
|
+
_ns(
|
|
62
|
+
valset_id=vs["id"],
|
|
63
|
+
inputs='{"question": "Who represents District 4?"}',
|
|
64
|
+
expected_outputs='{"answer": "Rep. Alice Nguyen"}',
|
|
65
|
+
from_assessment=None,
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
assert entry["id"].startswith("ve_")
|
|
69
|
+
assert entry["origin"] == "manual"
|
|
70
|
+
|
|
71
|
+
# entries list shows it; entries delete retires it (versioned).
|
|
72
|
+
assert entry["id"] in [
|
|
73
|
+
e["id"] for e in valsets_cmd.handle_entries_list(client, _ns(valset_id=vs["id"]))["entries"]
|
|
74
|
+
]
|
|
75
|
+
assert valsets_cmd.handle_entries_delete(client, _ns(entry_id=entry["id"])) == {"deleted": entry["id"]}
|
|
76
|
+
assert entry["id"] not in [
|
|
77
|
+
e["id"] for e in valsets_cmd.handle_entries_list(client, _ns(valset_id=vs["id"]))["entries"]
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
def test_entry_from_assessment(self, server_client, trace_capture):
|
|
81
|
+
program_hash, trace_id, span_id = _program_and_trace(server_client, trace_capture)
|
|
82
|
+
client = api_client.ApiClient(CMPND_E2E_API_KEY, E2E_ENDPOINT)
|
|
83
|
+
|
|
84
|
+
# An assessment of the trace, then an entry created from it.
|
|
85
|
+
assessment = assessments_cmd.handle_create(
|
|
86
|
+
client,
|
|
87
|
+
_ns(trace_id=trace_id, conclusion="down", body="corrected", annotations=[[span_id, "wrong field"]]),
|
|
88
|
+
)
|
|
89
|
+
vs = valsets_cmd.handle_create(
|
|
90
|
+
client, _ns(program_hash=program_hash, name=f"e2e from-assessment {uuid.uuid4().hex[:8]}")
|
|
91
|
+
)
|
|
92
|
+
entry = valsets_cmd.handle_entries_create(
|
|
93
|
+
client,
|
|
94
|
+
_ns(
|
|
95
|
+
valset_id=vs["id"],
|
|
96
|
+
inputs='{"question": "Who represents District 4?"}',
|
|
97
|
+
expected_outputs='{"answer": "Rep. Alice Nguyen"}',
|
|
98
|
+
from_assessment=assessment["id"],
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
assert entry["origin"] == "assessment"
|
|
102
|
+
assert entry["source_assessment_id"] == assessment["id"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Tests for cmpnd.cli.commands.assessments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from cmpnd.cli.commands import assessments as assessments_cmd
|
|
8
|
+
from cmpnd.cli.commands.assessments import handle_create, handle_list, handle_show
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestAssessmentsCreate:
|
|
12
|
+
def test_minimal_up_judgement(self, mock_client):
|
|
13
|
+
mock_client.post.return_value = {"id": "as_1"}
|
|
14
|
+
args = argparse.Namespace(trace_id="t-1", conclusion="up", body=None, annotations=None)
|
|
15
|
+
handle_create(mock_client, args)
|
|
16
|
+
mock_client.post.assert_called_once_with("/api/v1/assessments", json={"trace_id": "t-1", "conclusion": "up"})
|
|
17
|
+
|
|
18
|
+
def test_body_and_repeatable_annotations_assemble_the_array(self, mock_client):
|
|
19
|
+
mock_client.post.return_value = {"id": "as_1"}
|
|
20
|
+
args = argparse.Namespace(
|
|
21
|
+
trace_id="t-1",
|
|
22
|
+
conclusion="down",
|
|
23
|
+
body="the office is wrong",
|
|
24
|
+
annotations=[["span-a", "dropped the district"], ["span-b", "mislabeled it"]],
|
|
25
|
+
)
|
|
26
|
+
handle_create(mock_client, args)
|
|
27
|
+
mock_client.post.assert_called_once_with(
|
|
28
|
+
"/api/v1/assessments",
|
|
29
|
+
json={
|
|
30
|
+
"trace_id": "t-1",
|
|
31
|
+
"conclusion": "down",
|
|
32
|
+
"body": "the office is wrong",
|
|
33
|
+
"annotations": [
|
|
34
|
+
{"span_id": "span-a", "note": "dropped the district"},
|
|
35
|
+
{"span_id": "span-b", "note": "mislabeled it"},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestAssessmentsList:
|
|
42
|
+
def test_passes_program_through(self, mock_client):
|
|
43
|
+
mock_client.get.return_value = {"assessments": [], "total": 0}
|
|
44
|
+
args = argparse.Namespace(program="08729442d1b3a3bf")
|
|
45
|
+
handle_list(mock_client, args)
|
|
46
|
+
mock_client.get.assert_called_once_with("/api/v1/assessments", params={"program": "08729442d1b3a3bf"})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestAssessmentsShow:
|
|
50
|
+
def test_show_by_id(self, mock_client):
|
|
51
|
+
mock_client.get.return_value = {"id": "as_9"}
|
|
52
|
+
args = argparse.Namespace(id="as_9")
|
|
53
|
+
handle_show(mock_client, args)
|
|
54
|
+
mock_client.get.assert_called_once_with("/api/v1/assessments/as_9")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestAssessmentsRegistration:
|
|
58
|
+
def test_subcommands_register(self):
|
|
59
|
+
import argparse as ap
|
|
60
|
+
|
|
61
|
+
parser = ap.ArgumentParser()
|
|
62
|
+
sub = parser.add_subparsers(dest="resource")
|
|
63
|
+
assessments_cmd.register(sub)
|
|
64
|
+
group = sub.choices["assessments"]
|
|
65
|
+
actions = group._subparsers._group_actions[0].choices # type: ignore[union-attr]
|
|
66
|
+
assert set(actions) == {"create", "list", "show"}
|
|
67
|
+
|
|
68
|
+
def test_conclusion_is_required(self):
|
|
69
|
+
import argparse as ap
|
|
70
|
+
|
|
71
|
+
parser = ap.ArgumentParser()
|
|
72
|
+
sub = parser.add_subparsers(dest="resource")
|
|
73
|
+
assessments_cmd.register(sub)
|
|
74
|
+
# A create with no conclusion flag is rejected by the mutually-exclusive group.
|
|
75
|
+
import contextlib
|
|
76
|
+
import io
|
|
77
|
+
|
|
78
|
+
with contextlib.redirect_stderr(io.StringIO()):
|
|
79
|
+
try:
|
|
80
|
+
parser.parse_args(["assessments", "create", "t-1"])
|
|
81
|
+
except SystemExit as exc:
|
|
82
|
+
assert exc.code == 2
|
|
83
|
+
else:
|
|
84
|
+
raise AssertionError("expected create without a conclusion to exit 2")
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Tests for cmpnd.cli.commands.valsets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from cmpnd.cli.api_client import CliError
|
|
10
|
+
from cmpnd.cli.commands import valsets as valsets_cmd
|
|
11
|
+
from cmpnd.cli.commands.valsets import (
|
|
12
|
+
handle_create,
|
|
13
|
+
handle_entries_create,
|
|
14
|
+
handle_entries_delete,
|
|
15
|
+
handle_entries_list,
|
|
16
|
+
handle_list,
|
|
17
|
+
handle_show,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestValsetsCreate:
|
|
22
|
+
def test_sends_program_and_name(self, mock_client):
|
|
23
|
+
mock_client.post.return_value = {"id": "vs_1"}
|
|
24
|
+
args = argparse.Namespace(program_hash="08729442d1b3a3bf", name="gold")
|
|
25
|
+
handle_create(mock_client, args)
|
|
26
|
+
mock_client.post.assert_called_once_with(
|
|
27
|
+
"/api/v1/valsets", json={"program": "08729442d1b3a3bf", "name": "gold"}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestValsetsList:
|
|
32
|
+
def test_passes_program_through(self, mock_client):
|
|
33
|
+
mock_client.get.return_value = {"valsets": [], "total": 0}
|
|
34
|
+
handle_list(mock_client, argparse.Namespace(program="08729442d1b3a3bf"))
|
|
35
|
+
mock_client.get.assert_called_once_with("/api/v1/valsets", params={"program": "08729442d1b3a3bf"})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestValsetsShow:
|
|
39
|
+
def test_show_by_id(self, mock_client):
|
|
40
|
+
mock_client.get.return_value = {"id": "vs_9"}
|
|
41
|
+
handle_show(mock_client, argparse.Namespace(id="vs_9"))
|
|
42
|
+
mock_client.get.assert_called_once_with("/api/v1/valsets/vs_9")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestValsetsEntries:
|
|
46
|
+
def test_entries_list(self, mock_client):
|
|
47
|
+
mock_client.get.return_value = {"entries": [], "total": 0}
|
|
48
|
+
handle_entries_list(mock_client, argparse.Namespace(valset_id="vs_1"))
|
|
49
|
+
mock_client.get.assert_called_once_with("/api/v1/valsets/vs_1/entries")
|
|
50
|
+
|
|
51
|
+
def test_entries_create_parses_json_and_omits_absent_source(self, mock_client):
|
|
52
|
+
mock_client.post.return_value = {"id": "ve_1"}
|
|
53
|
+
args = argparse.Namespace(
|
|
54
|
+
valset_id="vs_1",
|
|
55
|
+
inputs='{"question": "who?"}',
|
|
56
|
+
expected_outputs='{"answer": "them"}',
|
|
57
|
+
from_assessment=None,
|
|
58
|
+
)
|
|
59
|
+
handle_entries_create(mock_client, args)
|
|
60
|
+
mock_client.post.assert_called_once_with(
|
|
61
|
+
"/api/v1/valsets/vs_1/entries",
|
|
62
|
+
json={"inputs": {"question": "who?"}, "expected_outputs": {"answer": "them"}},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def test_entries_create_includes_source_assessment(self, mock_client):
|
|
66
|
+
mock_client.post.return_value = {"id": "ve_1"}
|
|
67
|
+
args = argparse.Namespace(
|
|
68
|
+
valset_id="vs_1",
|
|
69
|
+
inputs='{"question": "who?"}',
|
|
70
|
+
expected_outputs='{"answer": "them"}',
|
|
71
|
+
from_assessment="as_9",
|
|
72
|
+
)
|
|
73
|
+
handle_entries_create(mock_client, args)
|
|
74
|
+
mock_client.post.assert_called_once_with(
|
|
75
|
+
"/api/v1/valsets/vs_1/entries",
|
|
76
|
+
json={
|
|
77
|
+
"inputs": {"question": "who?"},
|
|
78
|
+
"expected_outputs": {"answer": "them"},
|
|
79
|
+
"from_assessment": "as_9",
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def test_entries_create_rejects_bad_json(self, mock_client):
|
|
84
|
+
args = argparse.Namespace(valset_id="vs_1", inputs="{not json", expected_outputs="{}", from_assessment=None)
|
|
85
|
+
with pytest.raises(CliError, match="--inputs JSON is invalid"):
|
|
86
|
+
handle_entries_create(mock_client, args)
|
|
87
|
+
mock_client.post.assert_not_called()
|
|
88
|
+
|
|
89
|
+
@pytest.mark.parametrize("value", ["[]", "42", "null", '"text"'])
|
|
90
|
+
def test_entries_create_rejects_non_object_json(self, mock_client, value):
|
|
91
|
+
args = argparse.Namespace(
|
|
92
|
+
valset_id="vs_1", inputs=value, expected_outputs='{"answer": "them"}', from_assessment=None
|
|
93
|
+
)
|
|
94
|
+
with pytest.raises(CliError, match="--inputs JSON must be an object"):
|
|
95
|
+
handle_entries_create(mock_client, args)
|
|
96
|
+
mock_client.post.assert_not_called()
|
|
97
|
+
|
|
98
|
+
def test_entries_create_rejects_non_object_expected_outputs(self, mock_client):
|
|
99
|
+
args = argparse.Namespace(
|
|
100
|
+
valset_id="vs_1", inputs='{"question": "who?"}', expected_outputs="[]", from_assessment=None
|
|
101
|
+
)
|
|
102
|
+
with pytest.raises(CliError, match="--expected-outputs JSON must be an object"):
|
|
103
|
+
handle_entries_create(mock_client, args)
|
|
104
|
+
mock_client.post.assert_not_called()
|
|
105
|
+
|
|
106
|
+
def test_entries_delete_confirms(self, mock_client):
|
|
107
|
+
mock_client.delete.return_value = None
|
|
108
|
+
result = handle_entries_delete(mock_client, argparse.Namespace(entry_id="ve_1"))
|
|
109
|
+
mock_client.delete.assert_called_once_with("/api/v1/valsets/entries/ve_1")
|
|
110
|
+
assert result == {"deleted": "ve_1"}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestValsetsRegistration:
|
|
114
|
+
def test_subcommands_register(self):
|
|
115
|
+
parser = argparse.ArgumentParser()
|
|
116
|
+
sub = parser.add_subparsers(dest="resource")
|
|
117
|
+
valsets_cmd.register(sub)
|
|
118
|
+
group = sub.choices["valsets"]
|
|
119
|
+
actions = group._subparsers._group_actions[0].choices # type: ignore[union-attr]
|
|
120
|
+
assert set(actions) == {"create", "list", "show", "entries"}
|
|
121
|
+
entries = actions["entries"]._subparsers._group_actions[0].choices # type: ignore[union-attr]
|
|
122
|
+
assert set(entries) == {"list", "create", "delete"}
|
|
File without changes
|