cmpnd 0.18.2__tar.gz → 0.19.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.2 → cmpnd-0.19.0}/PKG-INFO +1 -1
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/__init__.py +5 -0
- cmpnd-0.18.2/cmpnd/cli/api_client.py → cmpnd-0.19.0/cmpnd/_http.py +25 -31
- cmpnd-0.19.0/cmpnd/_timespec.py +45 -0
- cmpnd-0.19.0/cmpnd/cli/api_client.py +42 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/traces.py +3 -37
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/optimization.py +25 -2
- cmpnd-0.19.0/cmpnd/traces.py +136 -0
- cmpnd-0.19.0/docs/reading-traces.md +77 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/pyproject.toml +1 -1
- cmpnd-0.19.0/tests/e2e/test_traces_read.py +41 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_deployments.py +1 -1
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize_budget.py +36 -0
- cmpnd-0.19.0/tests/test_traces_read.py +163 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/uv.lock +1 -1
- {cmpnd-0.18.2 → cmpnd-0.19.0}/.gitignore +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/CLAUDE.md +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/README.md +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/callback.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/configuration.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/context.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/datasets.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/decorators.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/deployment.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/execution.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/exporter.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/helpers.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/identity.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/imaging.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/mcp_servers.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/models.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/packaging.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/cmpnd/retry.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/docs/cli.md +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/docs/custom-proposer.md +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/docs/sdk-instrumentation.md +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/examples/local_ollama.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/README.md +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/ingest/pyproject.toml +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/_fake_clock.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/conftest.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/conftest.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/__main__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/drain.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/ingest.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/sweep.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/conftest.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_health.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/_provider.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/_result.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/stress/test_trace_search.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_callback.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_signatures.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_configuration.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_context.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_datasets.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_decorators.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_deploy.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_deploy_mcp_tool_references.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_eval.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_eval_start.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_execute.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_exporter.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_identity.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_imaging.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_import_weight.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_integration.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_models.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_packaging.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_poll_until.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_project_payload.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_retry.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_schemas.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_stats_export.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_trace_render.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_truncation.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.0}/tests/test_xxh64.py +0 -0
- {cmpnd-0.18.2 → cmpnd-0.19.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")
|
|
@@ -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
|
|
@@ -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
|
|
@@ -704,6 +704,7 @@ def optimize(
|
|
|
704
704
|
lms: dict[str, Any] | None = None,
|
|
705
705
|
max_metric_calls: int | None = None,
|
|
706
706
|
auto: str | None = None,
|
|
707
|
+
may_author_code: bool | None = None,
|
|
707
708
|
max_steps: int = 10,
|
|
708
709
|
patience: int = 2,
|
|
709
710
|
seed: int = 0,
|
|
@@ -842,6 +843,16 @@ def optimize(
|
|
|
842
843
|
``"heavy"`` — sized from the program's predictor count and the
|
|
843
844
|
valset, using GEPA's own settings. Easier to reach for than a call
|
|
844
845
|
count when you do not yet know what a run costs.
|
|
846
|
+
may_author_code: Whether this run may rewrite the SOURCE of the program's
|
|
847
|
+
``dspy.Flex`` submodules, rather than only their instructions. Left
|
|
848
|
+
unset, the platform's answer is yes — writing a ``dspy.Flex`` is
|
|
849
|
+
already asking for its source to be optimized.
|
|
850
|
+
|
|
851
|
+
Pass ``False`` for "improve my prompts, don't rewrite my code" on a
|
|
852
|
+
program that has both: a cheaper, lower-variance search that still
|
|
853
|
+
optimizes every ordinary predictor and leaves each ``dspy.Flex`` at
|
|
854
|
+
the source you deployed. A program with no ``dspy.Flex`` in it is
|
|
855
|
+
unaffected either way.
|
|
845
856
|
max_steps: How many times to re-optimize the winner, each step a fresh
|
|
846
857
|
``optimize`` call with an advanced seed (default 10). Distinct from
|
|
847
858
|
the budget above, which bounds ONE step's search: total spend is
|
|
@@ -901,7 +912,14 @@ def optimize(
|
|
|
901
912
|
step_config["seed"] = seed
|
|
902
913
|
|
|
903
914
|
initiated = _initiate_step(
|
|
904
|
-
client,
|
|
915
|
+
client,
|
|
916
|
+
cfg.endpoint,
|
|
917
|
+
headers,
|
|
918
|
+
current_deployment_id,
|
|
919
|
+
optimizer,
|
|
920
|
+
data,
|
|
921
|
+
step_config,
|
|
922
|
+
may_author_code,
|
|
905
923
|
)
|
|
906
924
|
optimization_id = initiated["optimization_id"]
|
|
907
925
|
|
|
@@ -1040,6 +1058,7 @@ def _initiate_step(
|
|
|
1040
1058
|
optimizer: str,
|
|
1041
1059
|
data: str,
|
|
1042
1060
|
config: dict[str, Any],
|
|
1061
|
+
may_author_code: bool | None = None,
|
|
1043
1062
|
) -> dict[str, Any]:
|
|
1044
1063
|
"""POST one optimize step to serve; return the 201 body. Raises
|
|
1045
1064
|
OptimizeError carrying the server's error on any non-2xx (e.g. 409/422).
|
|
@@ -1058,7 +1077,11 @@ def _initiate_step(
|
|
|
1058
1077
|
resp = _request(
|
|
1059
1078
|
lambda: client.post(
|
|
1060
1079
|
f"{endpoint}/api/v1/deployments/{deployment_id}/optimize/{optimizer}",
|
|
1061
|
-
json=
|
|
1080
|
+
json=(
|
|
1081
|
+
{"data": data, "config": config}
|
|
1082
|
+
if may_author_code is None
|
|
1083
|
+
else {"data": data, "config": config, "may_author_code": may_author_code}
|
|
1084
|
+
),
|
|
1062
1085
|
headers=headers,
|
|
1063
1086
|
),
|
|
1064
1087
|
what,
|
|
@@ -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}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Reading traces from a program
|
|
2
|
+
|
|
3
|
+
The `cmpnd` SDK emits traces through `configure()` + `auto_instrument()`. It also
|
|
4
|
+
reads them back: `cmpnd.traces` is a programmatic view over the backend's trace
|
|
5
|
+
reads, so a program that builds an eval, a dashboard, or an agent lists and
|
|
6
|
+
fetches its own traces without shelling out to the CLI or hand-rolling httpx.
|
|
7
|
+
It follows the SDK's existing shape — module-level verbs over the credentials
|
|
8
|
+
`cmpnd.configure()` set, like `cmpnd.deploy` / `cmpnd.execute` / `cmpnd.load_dataset`.
|
|
9
|
+
|
|
10
|
+
## Entry point
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import cmpnd
|
|
14
|
+
|
|
15
|
+
cmpnd.configure(api_key="ct_...") # or set CMPND_API_KEY
|
|
16
|
+
|
|
17
|
+
page = cmpnd.traces.list(status="error", since="30d", limit=10)
|
|
18
|
+
for row in page["traces"]:
|
|
19
|
+
detail = cmpnd.traces.show(row["trace_id"])
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Each call resolves its API key and endpoint in this order: an explicit `api_key` /
|
|
23
|
+
`endpoint` argument, then whatever `cmpnd.configure()` set, then the
|
|
24
|
+
`CMPND_API_KEY` / `CMPND_ENDPOINT` environment. The endpoint falls back to the
|
|
25
|
+
public platform when nothing names one; a missing API key raises `cmpnd.ApiError`.
|
|
26
|
+
|
|
27
|
+
## Verbs and filters match the CLI
|
|
28
|
+
|
|
29
|
+
The verb and argument names are the `cmpnd traces` CLI vocabulary, so a user moves
|
|
30
|
+
between the two surfaces without relearning names. (The CLI's `traces search`
|
|
31
|
+
spelling is a deprecated alias of `traces list`; this surface offers only `list`.)
|
|
32
|
+
|
|
33
|
+
## `cmpnd.traces.list(...)`
|
|
34
|
+
|
|
35
|
+
Lists and searches traces, newest first, returning the raw response:
|
|
36
|
+
`{"traces": [...], "total": N, ...}`.
|
|
37
|
+
|
|
38
|
+
| Argument | CLI flag | Meaning |
|
|
39
|
+
| -- | -- | -- |
|
|
40
|
+
| `status` | `--status` | `ok`, `error`, `incomplete`, `in_progress` |
|
|
41
|
+
| `query` | `--query` / `-q` | Full-text search over indexed span text (prompts, completions, tool calls, errors) |
|
|
42
|
+
| `model` | `--model` | Exact model name, e.g. `openai/gpt-4o` |
|
|
43
|
+
| `program` | `--program` | Program name |
|
|
44
|
+
| `hash` | `--hash` | Program signature hash, as a string |
|
|
45
|
+
| `deployment` | `--deployment` | Deployment UUID |
|
|
46
|
+
| `metadata` | `--metadata` | `dict` matched by containment — every pair must be present on the trace |
|
|
47
|
+
| `tags` | `--tag` | `list` — matches any of the given project tags |
|
|
48
|
+
| `since` / `until` | `--since` / `--until` | Time bounds (see below) |
|
|
49
|
+
| `all_history` | `--all` | Widen to the route's longest window |
|
|
50
|
+
| `limit` / `offset` | `--limit` / `--offset` | Page size and start; `offset` must be a whole multiple of `limit` |
|
|
51
|
+
|
|
52
|
+
`since` / `until` accept an ISO 8601 timestamp, a bare date (`YYYY-MM-DD`), or a
|
|
53
|
+
duration back from now (`30d`, `2w`, `12h`, `90m`) — resolved by the same code the
|
|
54
|
+
CLI's `--since` / `--until` use. An upper bound needs a lower one, and
|
|
55
|
+
`all_history` cannot combine with either bound; the backend enforces both.
|
|
56
|
+
|
|
57
|
+
When no time filter is given the response carries `default_window_applied` and the
|
|
58
|
+
`start_time_from` the server chose, so a caller can tell an org-wide total from a
|
|
59
|
+
windowed one. `ignored_fields` names any request field an older server did not
|
|
60
|
+
understand.
|
|
61
|
+
|
|
62
|
+
`api_key` / `endpoint` keyword arguments override the configured credentials for a
|
|
63
|
+
single call, for a script that reads from more than one place.
|
|
64
|
+
|
|
65
|
+
## `cmpnd.traces.show(trace_id)`
|
|
66
|
+
|
|
67
|
+
Fetches one trace and its spans: `{"trace": {...}, "spans": [...]}`. A missing
|
|
68
|
+
trace raises `cmpnd.ApiError`.
|
|
69
|
+
|
|
70
|
+
## One transport, two surfaces
|
|
71
|
+
|
|
72
|
+
`cmpnd.traces` and the `cmpnd` CLI send every request through the same HTTP
|
|
73
|
+
transport (`cmpnd._http.ApiClient`), so the library and the CLI cannot drift on
|
|
74
|
+
base-URL handling, the API-key header, the timeout, or the error ladder. The CLI
|
|
75
|
+
adds its own credential resolution (`--api-key` / `--endpoint` flags, stored
|
|
76
|
+
login) on top; the `cmpnd traces list` / `cmpnd traces show` commands reach the
|
|
77
|
+
same two routes these functions do.
|
|
@@ -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.19.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,41 @@
|
|
|
1
|
+
"""End-to-end tests for the programmatic trace reads against a live server.
|
|
2
|
+
|
|
3
|
+
Drives ``cmpnd.traces`` — the library surface a user reaches through
|
|
4
|
+
``import cmpnd`` — against a real backend: run a program, then list and show the
|
|
5
|
+
trace it produced. This is the level that catches a library↔API contract
|
|
6
|
+
mismatch a mocked transport cannot. Skipped unless CMPND_E2E_API_KEY is set
|
|
7
|
+
(see conftest).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import dspy
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
import cmpnd
|
|
16
|
+
from tests.e2e.conftest import CMPND_E2E_API_KEY, E2E_ENDPOINT, flush_and_wait
|
|
17
|
+
|
|
18
|
+
pytestmark = pytest.mark.e2e
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestTracesReadE2E:
|
|
22
|
+
def test_list_and_show_round_trip(self, server_client, trace_capture):
|
|
23
|
+
dspy.Predict("question -> answer")(question="Who represents District 4?")
|
|
24
|
+
flush_and_wait()
|
|
25
|
+
trace_id = trace_capture.last_trace_id
|
|
26
|
+
assert trace_id is not None, "no trace captured"
|
|
27
|
+
program_hash = server_client.get_trace(trace_id)["trace"]["program_signature_hash"]
|
|
28
|
+
|
|
29
|
+
# list by the program that produced it finds the trace.
|
|
30
|
+
page = cmpnd.traces.list(hash=str(program_hash), limit=50, api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
|
|
31
|
+
assert trace_id in [t["trace_id"] for t in page["traces"]]
|
|
32
|
+
|
|
33
|
+
# show returns the trace and its spans.
|
|
34
|
+
detail = cmpnd.traces.show(trace_id, api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
|
|
35
|
+
assert detail["trace"]["trace_id"] == trace_id
|
|
36
|
+
assert isinstance(detail["spans"], list)
|
|
37
|
+
assert len(detail["spans"]) >= 1
|
|
38
|
+
|
|
39
|
+
def test_show_missing_trace_raises(self):
|
|
40
|
+
with pytest.raises(cmpnd.ApiError):
|
|
41
|
+
cmpnd.traces.show("00000000-0000-0000-0000-000000000000", api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
|
|
@@ -95,7 +95,7 @@ def _transport(response: httpx.Response) -> mock._patch[mock.MagicMock]:
|
|
|
95
95
|
"""
|
|
96
96
|
made = mock.MagicMock()
|
|
97
97
|
made.__enter__.return_value.get.return_value = response
|
|
98
|
-
return mock.patch("cmpnd.
|
|
98
|
+
return mock.patch("cmpnd._http.httpx.Client", return_value=made)
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
class TestOpenAPI:
|