cmpnd 0.18.3__tar.gz → 0.20.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cmpnd-0.18.3 → cmpnd-0.20.0}/PKG-INFO +1 -1
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/__init__.py +5 -0
- cmpnd-0.18.3/cmpnd/cli/api_client.py → cmpnd-0.20.0/cmpnd/_http.py +25 -31
- cmpnd-0.20.0/cmpnd/_timespec.py +45 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/__init__.py +11 -0
- cmpnd-0.20.0/cmpnd/cli/api_client.py +42 -0
- cmpnd-0.20.0/cmpnd/cli/commands/assessments.py +67 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/traces.py +3 -37
- cmpnd-0.20.0/cmpnd/cli/commands/valsets.py +101 -0
- cmpnd-0.20.0/cmpnd/traces.py +136 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/docs/cli.md +47 -0
- cmpnd-0.20.0/docs/reading-traces.md +77 -0
- {cmpnd-0.18.3 → 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_traces_read.py +41 -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.18.3 → cmpnd-0.20.0}/tests/test_cli/test_deployments.py +1 -1
- cmpnd-0.20.0/tests/test_cli/test_valsets.py +122 -0
- cmpnd-0.20.0/tests/test_traces_read.py +163 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/uv.lock +1 -1
- {cmpnd-0.18.3 → cmpnd-0.20.0}/.gitignore +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/CLAUDE.md +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/README.md +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/callback.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/configuration.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/context.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/datasets.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/decorators.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/deployment.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/execution.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/exporter.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/helpers.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/identity.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/imaging.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/mcp_servers.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/models.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/optimization.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/packaging.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/cmpnd/retry.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/docs/custom-proposer.md +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/docs/sdk-instrumentation.md +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/examples/local_ollama.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/README.md +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/ingest/pyproject.toml +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/__init__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/_fake_clock.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/conftest.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/__init__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/conftest.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/__init__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/__main__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/drain.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/ingest.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/sweep.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/__init__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/conftest.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_health.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/__init__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/_provider.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/_result.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/stress/test_trace_search.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_callback.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_signatures.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_configuration.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_context.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_datasets.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_decorators.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_deploy.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_deploy_mcp_tool_references.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_eval.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_eval_start.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_execute.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_exporter.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_identity.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_imaging.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_import_weight.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_integration.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_models.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_packaging.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_poll_until.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_project_payload.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_retry.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_schemas.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_stats_export.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_trace_render.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_truncation.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/tests/test_xxh64.py +0 -0
- {cmpnd-0.18.3 → cmpnd-0.20.0}/todo.md +0 -0
|
@@ -14,6 +14,8 @@ Usage:
|
|
|
14
14
|
|
|
15
15
|
import importlib.metadata
|
|
16
16
|
|
|
17
|
+
from . import traces
|
|
18
|
+
from ._http import ApiError
|
|
17
19
|
from .callback import CmpndCallback, trace_id
|
|
18
20
|
from .configuration import configure, get_config, require_config
|
|
19
21
|
from .context import (
|
|
@@ -67,6 +69,9 @@ __all__ = [
|
|
|
67
69
|
# Cleanup
|
|
68
70
|
"flush_exporter",
|
|
69
71
|
"shutdown_exporter",
|
|
72
|
+
# Programmatic backend reads (cmpnd.traces.list / cmpnd.traces.show)
|
|
73
|
+
"traces",
|
|
74
|
+
"ApiError",
|
|
70
75
|
# Types
|
|
71
76
|
"SpanType",
|
|
72
77
|
"TraceStatus",
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
"""HTTP
|
|
1
|
+
"""Shared HTTP transport for the cmpnd backend API.
|
|
2
|
+
|
|
3
|
+
Two surfaces reach the backend: the CLI (``cmpnd.cli.api_client``) and the
|
|
4
|
+
programmatic library client (``cmpnd.Client``). Both send every request through
|
|
5
|
+
the one transport here, so they cannot drift on base-URL handling, the API-key
|
|
6
|
+
header, the timeout, or the error ladder. The CLI keeps its own credential
|
|
7
|
+
resolution and argparse glue on top; the library client keeps its own config
|
|
8
|
+
resolution; this module is only the wire.
|
|
9
|
+
"""
|
|
2
10
|
|
|
3
11
|
from __future__ import annotations
|
|
4
12
|
|
|
@@ -7,37 +15,23 @@ from typing import Any
|
|
|
7
15
|
|
|
8
16
|
import httpx
|
|
9
17
|
|
|
10
|
-
from . import credentials
|
|
11
18
|
|
|
19
|
+
class ApiError(Exception):
|
|
20
|
+
"""An error from a cmpnd backend request.
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
``status_code`` is the HTTP status when the server answered one, or ``None``
|
|
23
|
+
for a transport-level failure (could not connect, timed out). ``exit_code``
|
|
24
|
+
is the process exit the CLI maps the error to; the library client ignores it.
|
|
25
|
+
"""
|
|
15
26
|
|
|
16
|
-
def __init__(self, message: str, exit_code: int = 1):
|
|
27
|
+
def __init__(self, message: str, exit_code: int = 1, *, status_code: int | None = None):
|
|
17
28
|
super().__init__(message)
|
|
18
29
|
self.exit_code = exit_code
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def resolve_endpoint(args: Any) -> str:
|
|
22
|
-
return getattr(args, "endpoint", None) or os.getenv("CMPND_ENDPOINT", "https://platform.cmpnd.ai")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def resolve_config(args: Any) -> tuple[str, str]:
|
|
26
|
-
"""Resolve API key and endpoint.
|
|
27
|
-
|
|
28
|
-
Order: ``--api-key`` flag → ``CMPND_API_KEY`` env → stored credentials
|
|
29
|
-
(keyed by endpoint). The stored-credentials fallback is what makes
|
|
30
|
-
``cmpnd login`` stick across shell sessions.
|
|
31
|
-
"""
|
|
32
|
-
endpoint = resolve_endpoint(args)
|
|
33
|
-
api_key = getattr(args, "api_key", None) or os.getenv("CMPND_API_KEY") or credentials.load(endpoint)
|
|
34
|
-
if not api_key:
|
|
35
|
-
raise CliError("Not logged in. Run `cmpnd login`, pass --api-key, or set CMPND_API_KEY.")
|
|
36
|
-
return api_key, endpoint
|
|
30
|
+
self.status_code = status_code
|
|
37
31
|
|
|
38
32
|
|
|
39
33
|
class ApiClient:
|
|
40
|
-
"""Thin HTTP client for the cmpnd
|
|
34
|
+
"""Thin HTTP client for the cmpnd backend API."""
|
|
41
35
|
|
|
42
36
|
def __init__(self, api_key: str, endpoint: str):
|
|
43
37
|
self._api_key = api_key
|
|
@@ -89,7 +83,7 @@ class ApiClient:
|
|
|
89
83
|
try:
|
|
90
84
|
timeout = float(raw_timeout)
|
|
91
85
|
except ValueError:
|
|
92
|
-
raise
|
|
86
|
+
raise ApiError(f"Invalid CMPND_HTTP_TIMEOUT={raw_timeout!r}; expected a number of seconds.")
|
|
93
87
|
try:
|
|
94
88
|
with httpx.Client(
|
|
95
89
|
headers={"X-API-Key": self._api_key, "Content-Type": "application/json"},
|
|
@@ -110,14 +104,14 @@ class ApiClient:
|
|
|
110
104
|
else:
|
|
111
105
|
response = client.post(url, json=json)
|
|
112
106
|
except httpx.ConnectError:
|
|
113
|
-
raise
|
|
107
|
+
raise ApiError(f"Could not connect to {self._endpoint}. Check CMPND_ENDPOINT.")
|
|
114
108
|
except httpx.TimeoutException:
|
|
115
|
-
raise
|
|
109
|
+
raise ApiError(f"Request timed out connecting to {self._endpoint}.")
|
|
116
110
|
|
|
117
111
|
if response.status_code == 401:
|
|
118
|
-
raise
|
|
112
|
+
raise ApiError("Invalid or missing API key. Check CMPND_API_KEY or --api-key.", status_code=401)
|
|
119
113
|
if response.status_code == 404:
|
|
120
|
-
raise
|
|
114
|
+
raise ApiError("Resource not found.", status_code=404)
|
|
121
115
|
if response.status_code >= 500:
|
|
122
116
|
# 5xx usually means the server is up but blew up handling the
|
|
123
117
|
# request — a database hiccup, an unhandled exception, etc.
|
|
@@ -129,13 +123,13 @@ class ApiClient:
|
|
|
129
123
|
if body_excerpt:
|
|
130
124
|
msg += f": {body_excerpt}"
|
|
131
125
|
msg += "\n If you operate this server, check its logs for the underlying exception."
|
|
132
|
-
raise
|
|
126
|
+
raise ApiError(msg, exit_code=2, status_code=response.status_code)
|
|
133
127
|
if response.status_code >= 400:
|
|
134
128
|
try:
|
|
135
129
|
detail = response.json()
|
|
136
130
|
except Exception:
|
|
137
131
|
detail = response.text
|
|
138
|
-
raise
|
|
132
|
+
raise ApiError(f"Request failed ({response.status_code}): {detail}", status_code=response.status_code)
|
|
139
133
|
|
|
140
134
|
# A non-JSON route hands its body back verbatim. Checked before the
|
|
141
135
|
# empty-body arm so such a route answers "" rather than None, which is
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Search time-bound sugar, shared by the CLI and the library read client.
|
|
2
|
+
|
|
3
|
+
A search lower/upper bound may be written three ways: an ISO 8601 timestamp, a
|
|
4
|
+
bare date (``YYYY-MM-DD``), or a duration back from now (``30d``, ``2w``,
|
|
5
|
+
``12h``, ``90m``). Both surfaces resolve it the same way here, so ``--since 30d``
|
|
6
|
+
on the command line and ``since="30d"`` in the library mean one thing. The sugar
|
|
7
|
+
lives at the client edge rather than on the wire, so the API stays timestamp-only
|
|
8
|
+
— one grammar for every client, resolved once at the edge that knows what "now"
|
|
9
|
+
means to the user.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from datetime import datetime, timedelta, timezone
|
|
16
|
+
|
|
17
|
+
_RELATIVE_TIME = re.compile(r"^(\d+)([mhdw])$")
|
|
18
|
+
_BARE_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
19
|
+
|
|
20
|
+
_RELATIVE_UNITS = {
|
|
21
|
+
"m": "minutes",
|
|
22
|
+
"h": "hours",
|
|
23
|
+
"d": "days",
|
|
24
|
+
"w": "weeks",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_search_time(value: str, *, end_of_day: bool = False) -> str:
|
|
29
|
+
"""Resolve a search bound to an ISO 8601 timestamp.
|
|
30
|
+
|
|
31
|
+
A duration (``30d``, ``2w``, ``12h``, ``90m``) means that far back from now;
|
|
32
|
+
a bare date (``YYYY-MM-DD``) resolves to that whole day in UTC — start of day
|
|
33
|
+
normally, end of day (``23:59:59Z``) when ``end_of_day`` is set, so an upper
|
|
34
|
+
bound of ``2026-07-15`` includes all of the 15th; anything else passes through
|
|
35
|
+
for the server to parse.
|
|
36
|
+
"""
|
|
37
|
+
text = value.strip()
|
|
38
|
+
if _BARE_DATE.match(text):
|
|
39
|
+
return f"{text}T23:59:59Z" if end_of_day else f"{text}T00:00:00Z"
|
|
40
|
+
m = _RELATIVE_TIME.match(text)
|
|
41
|
+
if not m:
|
|
42
|
+
return value
|
|
43
|
+
amount, unit = int(m.group(1)), m.group(2)
|
|
44
|
+
when = datetime.now(timezone.utc) - timedelta(**{_RELATIVE_UNITS[unit]: amount})
|
|
45
|
+
return when.replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
@@ -13,6 +13,7 @@ import httpx
|
|
|
13
13
|
from . import api_client, deprecation
|
|
14
14
|
from .commands import (
|
|
15
15
|
admin,
|
|
16
|
+
assessments,
|
|
16
17
|
auth,
|
|
17
18
|
datasets,
|
|
18
19
|
deployments,
|
|
@@ -26,6 +27,7 @@ from .commands import (
|
|
|
26
27
|
tool_servers,
|
|
27
28
|
top,
|
|
28
29
|
traces,
|
|
30
|
+
valsets,
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
|
|
@@ -141,6 +143,15 @@ def main() -> None:
|
|
|
141
143
|
datasets.register(subparsers)
|
|
142
144
|
deployments.register(subparsers)
|
|
143
145
|
|
|
146
|
+
# Assessments and valsets CLIs are still in development, hidden behind
|
|
147
|
+
# CMPND_CLI_DEV=1 (mirroring the CMPND_CLI_ADMIN gate above). The env var
|
|
148
|
+
# only hides the commands from customers; the server still gates each action
|
|
149
|
+
# on the assessments:* / valsets:* scope, so this is discoverability, not
|
|
150
|
+
# enforcement.
|
|
151
|
+
if os.environ.get("CMPND_CLI_DEV") == "1":
|
|
152
|
+
assessments.register(subparsers)
|
|
153
|
+
valsets.register(subparsers)
|
|
154
|
+
|
|
144
155
|
# Top-level Docker-style shortcuts (human-readable by default; --json opts in)
|
|
145
156
|
top.register(subparsers)
|
|
146
157
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""CLI credential resolution over the shared HTTP transport.
|
|
2
|
+
|
|
3
|
+
The transport itself (``ApiClient``) and its error (``ApiError``) live in
|
|
4
|
+
``cmpnd._http`` so the CLI and the programmatic library client share one wire.
|
|
5
|
+
This module adds the CLI-only pieces on top: reading the endpoint and API key
|
|
6
|
+
from ``--api-key`` / ``--endpoint`` flags, the environment, and the stored login
|
|
7
|
+
credentials. ``CliError`` is the historical CLI name for the shared error and
|
|
8
|
+
stays as an alias so existing handlers keep raising and catching it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .. import _http
|
|
17
|
+
from . import credentials
|
|
18
|
+
|
|
19
|
+
# The shared transport and its error, re-exported under the names the CLI has
|
|
20
|
+
# always used. `CliError` is `ApiError` — the CLI names it for the process exit
|
|
21
|
+
# it carries; both are the same class.
|
|
22
|
+
ApiClient = _http.ApiClient
|
|
23
|
+
ApiError = _http.ApiError
|
|
24
|
+
CliError = _http.ApiError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_endpoint(args: Any) -> str:
|
|
28
|
+
return getattr(args, "endpoint", None) or os.getenv("CMPND_ENDPOINT", "https://platform.cmpnd.ai")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_config(args: Any) -> tuple[str, str]:
|
|
32
|
+
"""Resolve API key and endpoint.
|
|
33
|
+
|
|
34
|
+
Order: ``--api-key`` flag → ``CMPND_API_KEY`` env → stored credentials
|
|
35
|
+
(keyed by endpoint). The stored-credentials fallback is what makes
|
|
36
|
+
``cmpnd login`` stick across shell sessions.
|
|
37
|
+
"""
|
|
38
|
+
endpoint = resolve_endpoint(args)
|
|
39
|
+
api_key = getattr(args, "api_key", None) or os.getenv("CMPND_API_KEY") or credentials.load(endpoint)
|
|
40
|
+
if not api_key:
|
|
41
|
+
raise CliError("Not logged in. Run `cmpnd login`, pass --api-key, or set CMPND_API_KEY.")
|
|
42
|
+
return api_key, endpoint
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Assessments commands.
|
|
2
|
+
|
|
3
|
+
An assessment is a person's judgement of a trace: an up/down/neutral
|
|
4
|
+
conclusion, a prose body, and per-span annotations. See the assessments API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from argparse import ArgumentParser, _SubParsersAction
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .. import api_client
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
16
|
+
parser = subparsers.add_parser("assessments", help="Record and read judgements of traces")
|
|
17
|
+
sub = parser.add_subparsers(dest="action")
|
|
18
|
+
|
|
19
|
+
create = sub.add_parser("create", help="Record a judgement of a trace")
|
|
20
|
+
create.add_argument("trace_id", help="The trace to assess (its SDK trace id)")
|
|
21
|
+
conclusion = create.add_mutually_exclusive_group(required=True)
|
|
22
|
+
conclusion.add_argument(
|
|
23
|
+
"--up", dest="conclusion", action="store_const", const="up", help="Affirm the trace's output"
|
|
24
|
+
)
|
|
25
|
+
conclusion.add_argument(
|
|
26
|
+
"--down", dest="conclusion", action="store_const", const="down", help="Dispute the trace's output"
|
|
27
|
+
)
|
|
28
|
+
conclusion.add_argument(
|
|
29
|
+
"--neutral", dest="conclusion", action="store_const", const="neutral", help="Neither affirm nor dispute"
|
|
30
|
+
)
|
|
31
|
+
create.add_argument("-m", "--message", dest="body", help="The prose body of the judgement")
|
|
32
|
+
create.add_argument(
|
|
33
|
+
"--annotate",
|
|
34
|
+
dest="annotations",
|
|
35
|
+
action="append",
|
|
36
|
+
nargs=2,
|
|
37
|
+
metavar=("SPAN_ID", "NOTE"),
|
|
38
|
+
help="Attach a note to one span in the trace (repeat for multiple)",
|
|
39
|
+
)
|
|
40
|
+
create.set_defaults(func=handle_create)
|
|
41
|
+
|
|
42
|
+
ls = sub.add_parser("list", help="List a program's assessments")
|
|
43
|
+
ls.add_argument(
|
|
44
|
+
"--program", required=True, help="Program signature hash (16-hex, as `cmpnd signatures list` shows)"
|
|
45
|
+
)
|
|
46
|
+
ls.set_defaults(func=handle_list)
|
|
47
|
+
|
|
48
|
+
show = sub.add_parser("show", help="Show one assessment with its annotations")
|
|
49
|
+
show.add_argument("id", help="Assessment id")
|
|
50
|
+
show.set_defaults(func=handle_show)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def handle_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
54
|
+
body: dict[str, Any] = {"trace_id": args.trace_id, "conclusion": args.conclusion}
|
|
55
|
+
if args.body:
|
|
56
|
+
body["body"] = args.body
|
|
57
|
+
if args.annotations:
|
|
58
|
+
body["annotations"] = [{"span_id": span, "note": note} for span, note in args.annotations]
|
|
59
|
+
return client.post("/api/v1/assessments", json=body)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def handle_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
63
|
+
return client.get("/api/v1/assessments", params={"program": args.program})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def handle_show(client: api_client.ApiClient, args: Any) -> Any:
|
|
67
|
+
return client.get(f"/api/v1/assessments/{args.id}")
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
import re
|
|
7
6
|
import sys
|
|
8
7
|
import time
|
|
9
8
|
from argparse import ArgumentParser, ArgumentTypeError, _SubParsersAction
|
|
10
|
-
from datetime import datetime, timedelta, timezone
|
|
11
9
|
from typing import Any
|
|
12
10
|
|
|
11
|
+
from ... import _timespec
|
|
13
12
|
from .. import api_client, deprecation, pagination, trace_render
|
|
14
13
|
|
|
15
14
|
|
|
@@ -161,39 +160,6 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
161
160
|
follow.set_defaults(func=handle_follow)
|
|
162
161
|
|
|
163
162
|
|
|
164
|
-
_RELATIVE_TIME = re.compile(r"^(\d+)([mhdw])$")
|
|
165
|
-
_BARE_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
166
|
-
|
|
167
|
-
_RELATIVE_UNITS = {
|
|
168
|
-
"m": "minutes",
|
|
169
|
-
"h": "hours",
|
|
170
|
-
"d": "days",
|
|
171
|
-
"w": "weeks",
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def resolve_search_time(value: str, *, end_of_day: bool = False) -> str:
|
|
176
|
-
"""Resolve a search bound to an ISO 8601 timestamp.
|
|
177
|
-
|
|
178
|
-
A duration (``30d``, ``2w``, ``12h``, ``90m``) means that far back from now;
|
|
179
|
-
a bare date (``YYYY-MM-DD``) resolves to that whole day in UTC — start of day
|
|
180
|
-
normally, end of day (``23:59:59Z``) when ``end_of_day`` is set, so an upper
|
|
181
|
-
bound of ``2026-07-15`` includes all of the 15th; anything else passes through
|
|
182
|
-
for the server to parse. The sugar lives here rather than in the API so the
|
|
183
|
-
wire stays timestamp-only — one grammar for every client, resolved once at the
|
|
184
|
-
edge that knows what "now" means to the user.
|
|
185
|
-
"""
|
|
186
|
-
text = value.strip()
|
|
187
|
-
if _BARE_DATE.match(text):
|
|
188
|
-
return f"{text}T23:59:59Z" if end_of_day else f"{text}T00:00:00Z"
|
|
189
|
-
m = _RELATIVE_TIME.match(text)
|
|
190
|
-
if not m:
|
|
191
|
-
return value
|
|
192
|
-
amount, unit = int(m.group(1)), m.group(2)
|
|
193
|
-
when = datetime.now(timezone.utc) - timedelta(**{_RELATIVE_UNITS[unit]: amount})
|
|
194
|
-
return when.replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
195
|
-
|
|
196
|
-
|
|
197
163
|
def handle_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
198
164
|
max_window = bool(getattr(args, "max_window", False))
|
|
199
165
|
if max_window and (getattr(args, "start_time_from", None) or getattr(args, "start_time_to", None)):
|
|
@@ -234,9 +200,9 @@ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
|
234
200
|
# The lower bound resolves to the start of a bare date, the upper bound to
|
|
235
201
|
# the end of it, so `--until 2026-07-15` includes all of the 15th.
|
|
236
202
|
if "start_time_from" in body:
|
|
237
|
-
body["start_time_from"] = resolve_search_time(str(body["start_time_from"]))
|
|
203
|
+
body["start_time_from"] = _timespec.resolve_search_time(str(body["start_time_from"]))
|
|
238
204
|
if "start_time_to" in body:
|
|
239
|
-
body["start_time_to"] = resolve_search_time(str(body["start_time_to"]), end_of_day=True)
|
|
205
|
+
body["start_time_to"] = _timespec.resolve_search_time(str(body["start_time_to"]), end_of_day=True)
|
|
240
206
|
resp = client.post("/api/v1/traces/search", json=body)
|
|
241
207
|
|
|
242
208
|
# A window the caller did not ask for has to be visible: read as an org
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Valsets commands.
|
|
2
|
+
|
|
3
|
+
A valset is a program-owned validation set: a name, the field shape derived
|
|
4
|
+
from the owning program's signature, and its entries. Entries carry the
|
|
5
|
+
inputs and expected outputs of one validation example.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from argparse import ArgumentParser, _SubParsersAction
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .. import api_client
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
18
|
+
parser = subparsers.add_parser("valsets", help="Manage program-owned validation sets")
|
|
19
|
+
sub = parser.add_subparsers(dest="action")
|
|
20
|
+
|
|
21
|
+
create = sub.add_parser("create", help="Create a validation set owned by a program")
|
|
22
|
+
create.add_argument("program_hash", help="Owning program signature hash (16-hex, as `cmpnd signatures list` shows)")
|
|
23
|
+
create.add_argument("--name", required=True, help="Validation-set name (unique within the program)")
|
|
24
|
+
create.set_defaults(func=handle_create)
|
|
25
|
+
|
|
26
|
+
ls = sub.add_parser("list", help="List a program's validation sets")
|
|
27
|
+
ls.add_argument("--program", required=True, help="Program signature hash (16-hex)")
|
|
28
|
+
ls.set_defaults(func=handle_list)
|
|
29
|
+
|
|
30
|
+
show = sub.add_parser("show", help="Show a validation set")
|
|
31
|
+
show.add_argument("id", help="Valset id")
|
|
32
|
+
show.set_defaults(func=handle_show)
|
|
33
|
+
|
|
34
|
+
entries = sub.add_parser("entries", help="Manage a validation set's entries")
|
|
35
|
+
esub = entries.add_subparsers(dest="entries_action")
|
|
36
|
+
|
|
37
|
+
elist = esub.add_parser("list", help="List a valset's active entries")
|
|
38
|
+
elist.add_argument("valset_id", help="Valset id")
|
|
39
|
+
elist.set_defaults(func=handle_entries_list)
|
|
40
|
+
|
|
41
|
+
ecreate = esub.add_parser("create", help="Add an entry from JSON content")
|
|
42
|
+
ecreate.add_argument("valset_id", help="Valset id")
|
|
43
|
+
ecreate.add_argument(
|
|
44
|
+
"--inputs", required=True, metavar="JSON", help="Inputs object; its keys are the valset's input fields"
|
|
45
|
+
)
|
|
46
|
+
ecreate.add_argument(
|
|
47
|
+
"--expected-outputs",
|
|
48
|
+
dest="expected_outputs",
|
|
49
|
+
required=True,
|
|
50
|
+
metavar="JSON",
|
|
51
|
+
help="Expected-outputs object; its keys are the valset's output fields",
|
|
52
|
+
)
|
|
53
|
+
ecreate.add_argument(
|
|
54
|
+
"--from-assessment", dest="from_assessment", help="Source assessment id, recorded as the entry's provenance"
|
|
55
|
+
)
|
|
56
|
+
ecreate.set_defaults(func=handle_entries_create)
|
|
57
|
+
|
|
58
|
+
edelete = esub.add_parser("delete", help="Retire an entry (versioned, not a hard delete)")
|
|
59
|
+
edelete.add_argument("entry_id", help="Entry id")
|
|
60
|
+
edelete.set_defaults(func=handle_entries_delete)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def handle_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
64
|
+
return client.post("/api/v1/valsets", json={"program": args.program_hash, "name": args.name})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def handle_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
68
|
+
return client.get("/api/v1/valsets", params={"program": args.program})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def handle_show(client: api_client.ApiClient, args: Any) -> Any:
|
|
72
|
+
return client.get(f"/api/v1/valsets/{args.id}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def handle_entries_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
76
|
+
return client.get(f"/api/v1/valsets/{args.valset_id}/entries")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def handle_entries_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
80
|
+
inputs = _parse_json_flag("--inputs", args.inputs)
|
|
81
|
+
expected = _parse_json_flag("--expected-outputs", args.expected_outputs)
|
|
82
|
+
body: dict[str, Any] = {"inputs": inputs, "expected_outputs": expected}
|
|
83
|
+
if args.from_assessment:
|
|
84
|
+
body["from_assessment"] = args.from_assessment
|
|
85
|
+
return client.post(f"/api/v1/valsets/{args.valset_id}/entries", json=body)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def handle_entries_delete(client: api_client.ApiClient, args: Any) -> Any:
|
|
89
|
+
client.delete(f"/api/v1/valsets/entries/{args.entry_id}")
|
|
90
|
+
# The route returns 204; echo a confirmation so the CLI does not print null.
|
|
91
|
+
return {"deleted": args.entry_id}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _parse_json_flag(flag: str, value: str) -> dict[str, Any]:
|
|
95
|
+
try:
|
|
96
|
+
parsed = json.loads(value)
|
|
97
|
+
except json.JSONDecodeError as exc:
|
|
98
|
+
raise api_client.CliError(f"{flag} JSON is invalid: {exc}") from exc
|
|
99
|
+
if not isinstance(parsed, dict):
|
|
100
|
+
raise api_client.CliError(f"{flag} JSON must be an object")
|
|
101
|
+
return parsed
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Programmatic trace reads for the cmpnd backend.
|
|
2
|
+
|
|
3
|
+
``import cmpnd`` already emits traces; this module reads them back. ``cmpnd.traces``
|
|
4
|
+
reaches the same two backend reads the CLI does — ``list`` and ``show`` — over the
|
|
5
|
+
same HTTP transport, so the library and the CLI cannot drift. It follows the SDK's
|
|
6
|
+
existing shape: module-level verbs over the credentials ``cmpnd.configure()`` set,
|
|
7
|
+
like ``cmpnd.deploy`` / ``cmpnd.execute`` / ``cmpnd.load_dataset``.
|
|
8
|
+
|
|
9
|
+
>>> import cmpnd
|
|
10
|
+
>>> cmpnd.configure(api_key="ct_xxx")
|
|
11
|
+
>>> page = cmpnd.traces.list(status="error", since="30d", limit=10)
|
|
12
|
+
>>> for row in page["traces"]:
|
|
13
|
+
... detail = cmpnd.traces.show(row["trace_id"])
|
|
14
|
+
|
|
15
|
+
The verbs and filter names match the ``cmpnd traces`` CLI vocabulary — ``list`` and
|
|
16
|
+
``show``, filtered by ``query``, ``program``, ``hash``, ``deployment``, ``since`` /
|
|
17
|
+
``until``, ``all_history`` — so a user moves between the two surfaces without
|
|
18
|
+
relearning names. (The CLI's ``traces search`` spelling is a deprecated alias of
|
|
19
|
+
``traces list``; this surface offers only ``list``.)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from . import _http, _timespec, configuration
|
|
28
|
+
|
|
29
|
+
DEFAULT_ENDPOINT = "https://platform.cmpnd.ai"
|
|
30
|
+
|
|
31
|
+
# The traces route paginates by page number; these verbs offer the friendlier
|
|
32
|
+
# limit/offset pair and map it, matching how the CLI presents pagination.
|
|
33
|
+
_DEFAULT_LIMIT = 50
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def list(
|
|
37
|
+
*,
|
|
38
|
+
status: str | None = None,
|
|
39
|
+
query: str | None = None,
|
|
40
|
+
model: str | None = None,
|
|
41
|
+
program: str | None = None,
|
|
42
|
+
hash: str | None = None,
|
|
43
|
+
deployment: str | None = None,
|
|
44
|
+
metadata: dict[str, str] | None = None,
|
|
45
|
+
tags: list[str] | None = None,
|
|
46
|
+
since: str | None = None,
|
|
47
|
+
until: str | None = None,
|
|
48
|
+
all_history: bool = False,
|
|
49
|
+
limit: int = _DEFAULT_LIMIT,
|
|
50
|
+
offset: int = 0,
|
|
51
|
+
api_key: str | None = None,
|
|
52
|
+
endpoint: str | None = None,
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
"""List and search traces, newest first.
|
|
55
|
+
|
|
56
|
+
Returns the raw response: ``{"traces": [...], "total": N, ...}``. It also
|
|
57
|
+
carries ``default_window_applied`` and ``start_time_from`` when no time filter
|
|
58
|
+
was given, so the caller can tell an org-wide total from a windowed one, and
|
|
59
|
+
``ignored_fields`` naming any request field an older server did not understand.
|
|
60
|
+
|
|
61
|
+
The filters mirror ``cmpnd traces list``: ``query`` searches indexed span text;
|
|
62
|
+
``program`` / ``hash`` name the program by name or signature hash;
|
|
63
|
+
``deployment`` is a deployment id; ``metadata`` is matched by containment (every
|
|
64
|
+
pair must be present); ``tags`` matches any of the given tags. ``since`` /
|
|
65
|
+
``until`` accept an ISO 8601 timestamp, a bare date (``YYYY-MM-DD``), or a
|
|
66
|
+
duration back from now (``30d``, ``2w``, ``12h``, ``90m``), resolved the same
|
|
67
|
+
way the CLI resolves ``--since`` / ``--until``. An upper bound needs a lower
|
|
68
|
+
one, and ``all_history`` (the CLI's ``--all``) cannot combine with either bound
|
|
69
|
+
— the backend enforces both.
|
|
70
|
+
|
|
71
|
+
``api_key`` / ``endpoint`` override the configured credentials for this call.
|
|
72
|
+
"""
|
|
73
|
+
body: dict[str, Any] = _pagination(limit, offset)
|
|
74
|
+
if all_history:
|
|
75
|
+
body["max_window"] = True
|
|
76
|
+
for wire_key, val in (
|
|
77
|
+
("status", status),
|
|
78
|
+
("q", query),
|
|
79
|
+
("model", model),
|
|
80
|
+
("program_name", program),
|
|
81
|
+
("program_signature_hash", hash),
|
|
82
|
+
("deployment_id", deployment),
|
|
83
|
+
("tags", tags),
|
|
84
|
+
):
|
|
85
|
+
if val is not None:
|
|
86
|
+
body[wire_key] = val
|
|
87
|
+
if metadata is not None:
|
|
88
|
+
body["metadata"] = dict(metadata)
|
|
89
|
+
if since is not None:
|
|
90
|
+
body["start_time_from"] = _timespec.resolve_search_time(since)
|
|
91
|
+
if until is not None:
|
|
92
|
+
body["start_time_to"] = _timespec.resolve_search_time(until, end_of_day=True)
|
|
93
|
+
return _client(api_key, endpoint).post("/api/v1/traces/search", json=body)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def show(trace_id: str, *, api_key: str | None = None, endpoint: str | None = None) -> dict[str, Any]:
|
|
97
|
+
"""Fetch one trace and its spans: ``{"trace": {...}, "spans": [...]}``.
|
|
98
|
+
|
|
99
|
+
``api_key`` / ``endpoint`` override the configured credentials for this call.
|
|
100
|
+
"""
|
|
101
|
+
return _client(api_key, endpoint).get(f"/api/v1/traces/{trace_id}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _client(api_key: str | None, endpoint: str | None) -> _http.ApiClient:
|
|
105
|
+
"""Build a transport from the resolved credentials.
|
|
106
|
+
|
|
107
|
+
Resolution order: an explicit ``api_key`` / ``endpoint`` argument, then whatever
|
|
108
|
+
``cmpnd.configure()`` set, then the ``CMPND_API_KEY`` / ``CMPND_ENDPOINT``
|
|
109
|
+
environment. The endpoint falls back to the public platform when nothing names
|
|
110
|
+
one; a missing API key raises, since no read is possible without one.
|
|
111
|
+
"""
|
|
112
|
+
cfg = configuration.get_config()
|
|
113
|
+
resolved_key = api_key or (cfg.api_key if cfg else None) or os.getenv("CMPND_API_KEY")
|
|
114
|
+
if not resolved_key:
|
|
115
|
+
raise _http.ApiError("No API key. Pass api_key=, call cmpnd.configure(api_key=...), or set CMPND_API_KEY.")
|
|
116
|
+
resolved_endpoint = endpoint or (cfg.endpoint if cfg else None) or os.getenv("CMPND_ENDPOINT") or DEFAULT_ENDPOINT
|
|
117
|
+
return _http.ApiClient(resolved_key, resolved_endpoint)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _pagination(limit: int, offset: int) -> dict[str, Any]:
|
|
121
|
+
"""Map the caller's limit/offset onto the route's page/page_size.
|
|
122
|
+
|
|
123
|
+
The route addresses rows by page, so the offset must be a whole multiple of the
|
|
124
|
+
limit — the same rule the CLI's pagination helper enforces.
|
|
125
|
+
"""
|
|
126
|
+
if limit <= 0:
|
|
127
|
+
raise _http.ApiError("limit must be positive")
|
|
128
|
+
if offset < 0:
|
|
129
|
+
raise _http.ApiError("offset must not be negative")
|
|
130
|
+
if offset % limit != 0:
|
|
131
|
+
lower = (offset // limit) * limit
|
|
132
|
+
raise _http.ApiError(
|
|
133
|
+
f"offset must be a multiple of limit (the traces route paginates by page); "
|
|
134
|
+
f"try offset {lower} or {lower + limit}"
|
|
135
|
+
)
|
|
136
|
+
return {"page": offset // limit + 1, "page_size": limit}
|