cmpnd 0.85.0__tar.gz → 0.85.2__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.85.0 → cmpnd-0.85.2}/PKG-INFO +1 -1
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_http.py +12 -6
- {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/sdk-instrumentation.md +14 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/exporter_http.py +73 -10
- cmpnd-0.85.2/ingest/cmpnd_ingest/user_agent.py +52 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/pyproject.toml +1 -1
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/seed_review_run.py +19 -3
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_project_attribution.py +0 -73
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_traces_read.py +3 -1
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_trace_search.py +7 -1
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_exporter.py +123 -0
- cmpnd-0.85.2/tests/test_user_agent.py +109 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/uv.lock +1 -1
- {cmpnd-0.85.0 → cmpnd-0.85.2}/.gitignore +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/AGENTS.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/CLAUDE.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/README.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_assessments.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_authored_import.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_deployments.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_exporter_pool.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_runtime_tools.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_traces.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/_valsets.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/authored.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/callback.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/assessments.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/tree_help.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/cli/views.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/client.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/configuration.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/context.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/credentials.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/datasets.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/decorators.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/deployment.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/deployment_metadata.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/deployment_runs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/execution.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/exporter.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/helpers.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/identity.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/imaging.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/lm_configuration.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/mcp_servers.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/models.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/named_programs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/optimization.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/packaging.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/program_naming.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/programs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/references.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/retry.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/cmpnd/traces.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/cli.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/client.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/custom-proposer.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/docs/reading-traces.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/examples/local_ollama.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/README.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/client_floor.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/program_names.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/span_context.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/cmpnd_ingest/trace_facts.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/ingest/pyproject.toml +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/check-pip-install.sh +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/install-test-env.sh +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/release-gate.sh +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/release-real-models.sh +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/selfhost_compat.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/stubs/dspy/utils/dummies.pyi +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_credential_route.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_deployment_signature_helpers.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_fake_clock.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_resolve_refs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/_traced_run.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/conftest.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/continuation_task.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_assessments_cli.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_deploy_pandas.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_authored_metric.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_continuation.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_custom_proposer.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/e2e/test_valsets_cli.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/authored_optimize/judge.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/conftest.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/lm15_prompt_fixture.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/lm15_prompts/four_turn_predict.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/lm15_prompts/rlm_six_turns.json +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/__main__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/drain.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/ingest.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/rlm_size_report.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/sweep.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/load/test_rlm_size_report.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/conftest.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_health.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/_provider.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/_readiness.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/_reliability.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/_result.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/mint_bedrock_token.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_bedrock_converse.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_deploy_programs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_multi_client_stress.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_optimize_client_committee.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_optimize_continuation.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_optimize_loop.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_optimize_stress.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_provider_roles.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_readiness.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_real_provider_twins.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_reliability.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_reliability_classify.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/real_models/test_rlm_wasm.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/span_reference_fixture.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_adapter_reentrancy.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_assessments_api.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_authored.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_authored_binding_credentials.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_authored_bundle_collisions.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_authored_metric_contract.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_authored_named_models.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_authored_optimize.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_callback.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_assessments.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_deployment_runs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_traces_program_and_module.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_tree_help.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cli/test_valsets.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_client.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_client_floor.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_client_program_names.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_configuration.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_context.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_credential_kwargs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_credential_selection.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_credential_values.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_datasets.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_decision_client_spans.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_decorators.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_deploy.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_deploy_mcp_tool_references.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_deploy_program_id.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_deployment_metadata.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_deployment_runs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_deployments_read.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_eval.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_eval_program_id.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_eval_start.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_execute.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_execute_run_identity.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_exporter_program_names.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_gepa_output_normalization.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_identity.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_imaging.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_import_weight.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_integration.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_lm15_native_calls.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_lm15_prompt_fixtures.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_lm15_prompt_references.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_lm_configuration.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_lm_usage_shapes.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_models.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_multi_client_stress_readiness.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_no_signature_hash_on_the_wire.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimization_phase_callback.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimization_program_names.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_packaging.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_poll_until.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_program_id_deprecation.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_program_identity.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_program_names_in_deploy.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_program_naming.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_programs_create.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_project_payload.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_real_provider_twins_readiness.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_release_gate_script.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_release_real_models_script.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_replaced_names.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_retry.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_run_attribution_paths.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_runtime_tools.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_schemas.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_seal_lm_credential.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_signature_inputs.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_span_references.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_span_sent_once.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_span_submission.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_span_submission_wire_contract.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_system_one.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_trace_context_concurrency.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_trace_fact_wire_contract.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_trace_facts.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_trace_render.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_traces_read.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_truncation.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_upload_auth_headers.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_valsets_api.py +0 -0
- {cmpnd-0.85.0 → cmpnd-0.85.2}/tests/test_xxh64.py +0 -0
|
@@ -16,7 +16,7 @@ from typing import Any
|
|
|
16
16
|
from urllib.parse import urlsplit
|
|
17
17
|
|
|
18
18
|
import httpx
|
|
19
|
-
from cmpnd_ingest import client_floor
|
|
19
|
+
from cmpnd_ingest import client_floor, user_agent
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class ApiError(Exception):
|
|
@@ -79,16 +79,22 @@ def server_detail(response: httpx.Response) -> str:
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
def new_client(**kwargs: Any) -> httpx.Client:
|
|
82
|
-
"""An ``httpx.Client`` that
|
|
83
|
-
|
|
82
|
+
"""An ``httpx.Client`` that names this release to the server and warns when
|
|
83
|
+
the server no longer supports it (:mod:`cmpnd_ingest.client_floor`).
|
|
84
84
|
|
|
85
|
-
Every client the SDK builds comes from here, so every
|
|
86
|
-
|
|
85
|
+
Every client the SDK builds comes from here, so every request carries the
|
|
86
|
+
release's ``User-Agent`` (:mod:`cmpnd_ingest.user_agent`) and every answer
|
|
87
|
+
the platform sends gets the check. A caller's own ``headers`` override the
|
|
88
|
+
``User-Agent``, and its own ``event_hooks`` run after the check.
|
|
87
89
|
"""
|
|
88
90
|
hooks = client_floor.event_hooks()
|
|
89
91
|
for event, callers in (kwargs.pop("event_hooks", None) or {}).items():
|
|
90
92
|
hooks.setdefault(event, []).extend(callers)
|
|
91
|
-
|
|
93
|
+
# httpx.Headers matches names without regard to case, so a caller's `user-agent`
|
|
94
|
+
# replaces the default rather than travelling beside it.
|
|
95
|
+
headers = httpx.Headers(user_agent.headers())
|
|
96
|
+
headers.update(kwargs.pop("headers", None) or {})
|
|
97
|
+
return httpx.Client(event_hooks=hooks, headers=headers, **kwargs)
|
|
92
98
|
|
|
93
99
|
|
|
94
100
|
_DEFAULT_PORTS = {"http": 80, "https": 443}
|
|
@@ -331,6 +331,18 @@ Each `_send_*` method routes a failed delivery to one of two counters: a `429` (
|
|
|
331
331
|
|
|
332
332
|
Tolerating is never silent. A send that recovered logs a warning naming what it rode through (`rode through 3 transient responses over 12.4s (503=3) to POST /api/v1/traces`), and an exhausted budget raises `ExportRetriesExhausted` with the same count and window in its message. `BatchExporter.failure_summary()` reports the distribution of non-2xx statuses observed, how many transient answers were ridden through, and a sample error body — that's what the stress lane prints when it fails, so a red lane says *what* the wire answered rather than only how many errors there were.
|
|
333
333
|
|
|
334
|
+
Trace delivery failures log at ERROR on `cmpnd.exporter`. The message names the
|
|
335
|
+
operation and the number of records the exporter will not resend. HTTP failures
|
|
336
|
+
include the status, route and serve's string `error`, `error_code`, `upgrade` and
|
|
337
|
+
`field` values when present. The exporter bounds each value and escapes control
|
|
338
|
+
characters. It omits other body fields and non-JSON responses. Exhausted retries
|
|
339
|
+
include the last server message as well as the attempt count and elapsed time.
|
|
340
|
+
|
|
341
|
+
A non-retryable 422 discards the submission. Upgrade the SDK when serve asks for
|
|
342
|
+
it, then emit fresh telemetry. An upgrade cannot recover discarded records.
|
|
343
|
+
Background failures keep the application running and increment the existing
|
|
344
|
+
error counters. They do not raise an exception in the instrumented program.
|
|
345
|
+
|
|
334
346
|
### The retryable-error contract
|
|
335
347
|
|
|
336
348
|
The server tells a client when a failed write is worth re-sending, in three signals carrying one fact:
|
|
@@ -361,6 +373,8 @@ Two callers implement the waiting:
|
|
|
361
373
|
|
|
362
374
|
Every API answer names the oldest cmpnd release the server supports, in the `X-Cmpnd-Minimum-Client-Version` header. `cmpnd_ingest/client_floor.py` reads it in an httpx response hook. When the installed `cmpnd` is older, it raises an `UpgradeRequiredWarning` once per process that names both versions and the command `pip install -U cmpnd`. The server refuses nothing for a client's version, so a call below the floor still goes through, and the warning appears beside whatever it does. Every client the SDK builds comes from `cmpnd._http.new_client`, which installs the hook, and the exporter installs it in `_client_kwargs`. `tests/test_client_floor.py` fails on an `httpx.Client` built any other way in `cmpnd/`. An install of `cmpnd_ingest` without `cmpnd` has no version to compare and says nothing. `CMPND_NO_UPGRADE_WARNING=1` silences the warning for a user who stays on an older release on purpose, and the warning names that switch. `cmpnd.UpgradeRequiredWarning` is public, so a `warnings` filter silences it in code.
|
|
363
375
|
|
|
376
|
+
Every request names its release in the other direction, so the server can count which releases are in use. `cmpnd_ingest/user_agent.py` builds the `User-Agent` value, such as `cmpnd/0.85.2 python/3.12.13 httpx/0.28.1 dspy/3.4.1`. The first product is `cmpnd` for the SDK and `cmpnd-ingest` for a producer that installs only the transport, such as the optimizer's search process. A library that is not installed is left out. `cmpnd._http.new_client` and the exporter's `_client_kwargs` both send it. A caller that passes its own `User-Agent` to `new_client` keeps it. `tests/test_user_agent.py` checks the value that reaches the wire.
|
|
377
|
+
|
|
364
378
|
### The optimize loop and transient tolerance
|
|
365
379
|
|
|
366
380
|
`optimization._poll_until_terminal` is what a caller blocks on for the length of a run, so one transient answer or one reset connection must not destroy a healthy run. It rides through transient answers on the shared budget, resetting after each successful poll, and never extends past the caller's `timeout` (default 600s per step) — a backoff wait is clipped by the deadline rather than overshooting it.
|
|
@@ -34,7 +34,7 @@ from typing import Any, cast
|
|
|
34
34
|
|
|
35
35
|
import httpx
|
|
36
36
|
|
|
37
|
-
from . import client_floor, program_names, request_log, retry
|
|
37
|
+
from . import client_floor, program_names, request_log, retry, user_agent
|
|
38
38
|
from .bodies import EncodedTraceBody, SpanBody, TraceBody
|
|
39
39
|
from .protocols import (
|
|
40
40
|
EvalRunRecord,
|
|
@@ -59,6 +59,31 @@ logger = logging.getLogger("cmpnd.exporter")
|
|
|
59
59
|
_GZIP_HEADERS = {"Content-Encoding": "gzip"}
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def _export_error_detail(exc: Exception) -> str:
|
|
63
|
+
"""Keep delivery errors useful in background logs without dumping response bodies."""
|
|
64
|
+
|
|
65
|
+
def text(value: str, limit: int = 300) -> str:
|
|
66
|
+
return json.dumps(value[:limit], ensure_ascii=True)[1:-1]
|
|
67
|
+
|
|
68
|
+
cause = exc.__cause__ if isinstance(exc, ExportRetriesExhausted) else exc
|
|
69
|
+
if not isinstance(cause, httpx.HTTPStatusError):
|
|
70
|
+
return text(str(exc), 600)
|
|
71
|
+
response = cause.response
|
|
72
|
+
detail = f"HTTP {response.status_code} for {cause.request.method} {cause.request.url.path}"
|
|
73
|
+
if cause is not exc:
|
|
74
|
+
detail = f"{text(str(exc), 600)}; {detail}"
|
|
75
|
+
try:
|
|
76
|
+
body = response.json()
|
|
77
|
+
except (ValueError, httpx.ResponseNotRead):
|
|
78
|
+
return detail
|
|
79
|
+
if isinstance(body, dict):
|
|
80
|
+
for key in ("error", "error_code", "upgrade", "field"):
|
|
81
|
+
value = body.get(key)
|
|
82
|
+
if isinstance(value, str) and value:
|
|
83
|
+
detail += f"; {key}={text(value)}"
|
|
84
|
+
return detail
|
|
85
|
+
|
|
86
|
+
|
|
62
87
|
def _send_json(client: httpx.Client, method: str, path: str, payload: Any) -> httpx.Response:
|
|
63
88
|
"""POST/PATCH/PUT ``payload`` as gzipped JSON.
|
|
64
89
|
|
|
@@ -1081,11 +1106,13 @@ class BatchExporter:
|
|
|
1081
1106
|
|
|
1082
1107
|
def _client_kwargs(self) -> dict[str, Any]:
|
|
1083
1108
|
"""Shared httpx.Client construction args for the background and sync
|
|
1084
|
-
clients, so both carry the same base URL, auth header,
|
|
1085
|
-
|
|
1109
|
+
clients, so both carry the same base URL, auth header, release
|
|
1110
|
+
``User-Agent`` (``user_agent``), client-floor check (``client_floor``)
|
|
1111
|
+
and, in tests, injected transport."""
|
|
1086
1112
|
kwargs: dict[str, Any] = {
|
|
1087
1113
|
"base_url": self._config.endpoint,
|
|
1088
1114
|
"headers": {
|
|
1115
|
+
**user_agent.headers(),
|
|
1089
1116
|
"X-API-Key": self._config.api_key,
|
|
1090
1117
|
"Content-Type": "application/json",
|
|
1091
1118
|
},
|
|
@@ -1581,15 +1608,30 @@ class BatchExporter:
|
|
|
1581
1608
|
# drop call for opposite responses from the caller.
|
|
1582
1609
|
self._record_drop("trace", "server_shed", count=len(traces_to_send), log=False)
|
|
1583
1610
|
else:
|
|
1584
|
-
logger.error(
|
|
1611
|
+
logger.error(
|
|
1612
|
+
"Failed to export traces batch. Records will not be resent (count=%s, traces=%s, spans=%s): %s",
|
|
1613
|
+
len(traces_to_send) + total_spans,
|
|
1614
|
+
len(traces_to_send),
|
|
1615
|
+
total_spans,
|
|
1616
|
+
_export_error_detail(e),
|
|
1617
|
+
)
|
|
1585
1618
|
with self._stats_lock:
|
|
1586
1619
|
self._error_count += len(traces_to_send)
|
|
1587
1620
|
except Exception as e:
|
|
1588
|
-
logger.error(
|
|
1621
|
+
logger.error(
|
|
1622
|
+
"Failed to export traces batch. Records will not be resent (count=%s, traces=%s, spans=%s): %s",
|
|
1623
|
+
len(traces_to_send) + total_spans,
|
|
1624
|
+
len(traces_to_send),
|
|
1625
|
+
total_spans,
|
|
1626
|
+
_export_error_detail(e),
|
|
1627
|
+
)
|
|
1589
1628
|
with self._stats_lock:
|
|
1590
1629
|
self._error_count += len(traces_to_send)
|
|
1591
1630
|
|
|
1592
1631
|
def _send_encoded_trace(self, trace: EncodedTraceBody) -> None:
|
|
1632
|
+
# The body stays compressed end to end, so the exporter never learns how
|
|
1633
|
+
# many spans it embeds. A failure names the one trace and says the span
|
|
1634
|
+
# count is unknown, rather than understating the loss as one record.
|
|
1593
1635
|
client = self._require_client()
|
|
1594
1636
|
try:
|
|
1595
1637
|
self._send_retrying(
|
|
@@ -1601,11 +1643,19 @@ class BatchExporter:
|
|
|
1601
1643
|
if e.response.status_code == 429:
|
|
1602
1644
|
self._record_drop("trace", "server_shed", count=1)
|
|
1603
1645
|
else:
|
|
1604
|
-
logger.error(
|
|
1646
|
+
logger.error(
|
|
1647
|
+
"Failed to export encoded trace %s. Records will not be resent (traces=1, spans=unknown): %s",
|
|
1648
|
+
trace.trace_id,
|
|
1649
|
+
_export_error_detail(e),
|
|
1650
|
+
)
|
|
1605
1651
|
with self._stats_lock:
|
|
1606
1652
|
self._error_count += 1
|
|
1607
1653
|
except Exception as e:
|
|
1608
|
-
logger.error(
|
|
1654
|
+
logger.error(
|
|
1655
|
+
"Failed to export encoded trace %s. Records will not be resent (traces=1, spans=unknown): %s",
|
|
1656
|
+
trace.trace_id,
|
|
1657
|
+
_export_error_detail(e),
|
|
1658
|
+
)
|
|
1609
1659
|
with self._stats_lock:
|
|
1610
1660
|
self._error_count += 1
|
|
1611
1661
|
|
|
@@ -1619,7 +1669,11 @@ class BatchExporter:
|
|
|
1619
1669
|
self._exported_count += 1
|
|
1620
1670
|
logger.debug(f"Started trace {trace_id}")
|
|
1621
1671
|
except Exception as e:
|
|
1622
|
-
logger.error(
|
|
1672
|
+
logger.error(
|
|
1673
|
+
"Failed to start trace %s. Records will not be resent (count=1): %s",
|
|
1674
|
+
trace_id,
|
|
1675
|
+
_export_error_detail(e),
|
|
1676
|
+
)
|
|
1623
1677
|
with self._stats_lock:
|
|
1624
1678
|
self._error_count += 1
|
|
1625
1679
|
|
|
@@ -1651,7 +1705,12 @@ class BatchExporter:
|
|
|
1651
1705
|
self._exported_count += len(span_list)
|
|
1652
1706
|
logger.debug(f"Exported {len(span_list)} spans for trace {trace_id}")
|
|
1653
1707
|
except Exception as e:
|
|
1654
|
-
logger.error(
|
|
1708
|
+
logger.error(
|
|
1709
|
+
"Failed to export spans for trace %s. Records will not be resent (count=%s): %s",
|
|
1710
|
+
trace_id,
|
|
1711
|
+
len(span_list),
|
|
1712
|
+
_export_error_detail(e),
|
|
1713
|
+
)
|
|
1655
1714
|
with self._stats_lock:
|
|
1656
1715
|
self._error_count += len(span_list)
|
|
1657
1716
|
|
|
@@ -1667,7 +1726,11 @@ class BatchExporter:
|
|
|
1667
1726
|
self._exported_count += 1
|
|
1668
1727
|
logger.debug(f"Finalized trace {trace_id}")
|
|
1669
1728
|
except Exception as e:
|
|
1670
|
-
logger.error(
|
|
1729
|
+
logger.error(
|
|
1730
|
+
"Failed to finalize trace %s. Records will not be resent (count=1): %s",
|
|
1731
|
+
trace_id,
|
|
1732
|
+
_export_error_detail(e),
|
|
1733
|
+
)
|
|
1671
1734
|
with self._stats_lock:
|
|
1672
1735
|
self._error_count += 1
|
|
1673
1736
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""The ``User-Agent`` every client the SDK builds sends, so the server can count releases.
|
|
2
|
+
|
|
3
|
+
The value names the installed distribution first, then the runtime it runs on:
|
|
4
|
+
|
|
5
|
+
cmpnd/0.85.2 python/3.12.13 httpx/0.28.1 dspy/3.4.1
|
|
6
|
+
|
|
7
|
+
The first product is ``cmpnd`` for the SDK and ``cmpnd-ingest`` for a producer that
|
|
8
|
+
installs only the transport, so the two never count as one. A distribution that is not
|
|
9
|
+
installed, such as ``dspy`` for a transport-only producer, is left out. A value that
|
|
10
|
+
cannot be read becomes ``unknown`` rather than failing a request.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import functools
|
|
16
|
+
import importlib.metadata
|
|
17
|
+
import platform
|
|
18
|
+
|
|
19
|
+
# The distributions that provide this package, in the order the first one found names
|
|
20
|
+
# the client. Only one is installed at a time (ingest/README.md).
|
|
21
|
+
_CLIENTS = ("cmpnd", "cmpnd-ingest")
|
|
22
|
+
# Runtime libraries whose version shapes what a client sends.
|
|
23
|
+
_LIBRARIES = ("httpx", "dspy")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _version(distribution: str) -> str | None:
|
|
27
|
+
"""The installed version, or None when it is not installed or cannot be read.
|
|
28
|
+
|
|
29
|
+
Any failure to read metadata counts as absent, because a broken install's
|
|
30
|
+
metadata must not stop a client from being built.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
return importlib.metadata.version(distribution)
|
|
34
|
+
except Exception:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@functools.cache
|
|
39
|
+
def user_agent() -> str:
|
|
40
|
+
"""The ``User-Agent`` header value for this process."""
|
|
41
|
+
client = next(
|
|
42
|
+
(f"{name}/{version}" for name in _CLIENTS if (version := _version(name)) is not None),
|
|
43
|
+
"cmpnd/unknown",
|
|
44
|
+
)
|
|
45
|
+
products = [client, f"python/{platform.python_version()}"]
|
|
46
|
+
products += [f"{name}/{version}" for name in _LIBRARIES if (version := _version(name)) is not None]
|
|
47
|
+
return " ".join(products)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def headers() -> dict[str, str]:
|
|
51
|
+
"""The headers that identify this client, to merge into a client's own."""
|
|
52
|
+
return {"User-Agent": user_agent()}
|
|
@@ -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.85.
|
|
8
|
+
version = "0.85.2"
|
|
9
9
|
description = "DSPy observability and deployment SDK for cmpnd"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.11"
|
|
@@ -46,11 +46,15 @@ import logging
|
|
|
46
46
|
import os
|
|
47
47
|
import pathlib
|
|
48
48
|
import re
|
|
49
|
+
import sys
|
|
49
50
|
import urllib.error
|
|
50
51
|
import urllib.parse
|
|
51
52
|
import urllib.request
|
|
52
53
|
import uuid
|
|
53
54
|
|
|
55
|
+
# The captured fixtures already depend on this repository's serve scripts.
|
|
56
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "serve" / "scripts" / "seed_fixtures"))
|
|
57
|
+
|
|
54
58
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
55
59
|
log = logging.getLogger("seed_review_run")
|
|
56
60
|
|
|
@@ -673,6 +677,14 @@ def _ensure_program(endpoint: str, api_key: str, name: str) -> str | None:
|
|
|
673
677
|
|
|
674
678
|
|
|
675
679
|
def _post(endpoint: str, path: str, payload, api_key: str, method: str = "POST"):
|
|
680
|
+
if path.startswith("/api/v1/traces"):
|
|
681
|
+
from submission import prepare_trace_submission
|
|
682
|
+
|
|
683
|
+
if path == "/api/v1/traces/batch":
|
|
684
|
+
for trace in payload:
|
|
685
|
+
prepare_trace_submission(trace)
|
|
686
|
+
elif path.startswith("/api/v1/traces/") and path.endswith("/spans"):
|
|
687
|
+
prepare_trace_submission(payload, path.split("/")[-2])
|
|
676
688
|
body = json.dumps(payload).encode("utf-8")
|
|
677
689
|
req = urllib.request.Request(
|
|
678
690
|
endpoint + path,
|
|
@@ -680,9 +692,13 @@ def _post(endpoint: str, path: str, payload, api_key: str, method: str = "POST")
|
|
|
680
692
|
method=method,
|
|
681
693
|
headers={"Content-Type": "application/json", "X-API-Key": api_key},
|
|
682
694
|
)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
695
|
+
try:
|
|
696
|
+
with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310 - fixed https endpoint from env
|
|
697
|
+
raw = resp.read()
|
|
698
|
+
return resp.status, (json.loads(raw) if raw else {})
|
|
699
|
+
except urllib.error.HTTPError as exc:
|
|
700
|
+
detail = exc.read().decode("utf-8", "replace")[:400]
|
|
701
|
+
raise RuntimeError(f"{method} {path} returned HTTP {exc.code}: {detail!r}") from exc
|
|
686
702
|
|
|
687
703
|
|
|
688
704
|
def _reserve_optimization(endpoint: str, api_key: str) -> str | None:
|
|
@@ -10,13 +10,11 @@ block so subsequent tests in the same pytest invocation are not affected.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
import datetime
|
|
14
13
|
import uuid
|
|
15
14
|
from collections.abc import Callable
|
|
16
15
|
from typing import Any
|
|
17
16
|
|
|
18
17
|
import dspy
|
|
19
|
-
import httpx
|
|
20
18
|
import pytest
|
|
21
19
|
|
|
22
20
|
import cmpnd
|
|
@@ -121,77 +119,6 @@ def test_project_stamped_on_trace_and_spans(server_client, trace_capture):
|
|
|
121
119
|
_restore_session_config()
|
|
122
120
|
|
|
123
121
|
|
|
124
|
-
def test_streaming_spans_inherit_full_project_tags(server_client):
|
|
125
|
-
"""Spans added via the streaming path (POST /traces/:id/spans) inherit all
|
|
126
|
-
project_tags from the parent trace, not just the first tag.
|
|
127
|
-
|
|
128
|
-
This bypasses the SDK exporter and calls the API directly to simulate
|
|
129
|
-
an old SDK that sends project but not project_tags on spans. Serve must
|
|
130
|
-
preserve both tags from the parent trace.
|
|
131
|
-
"""
|
|
132
|
-
trace_id = str(uuid.uuid4())
|
|
133
|
-
tag_a = f"e2e-stream-a-{uuid.uuid4().hex[:8]}"
|
|
134
|
-
tag_b = f"e2e-stream-b-{uuid.uuid4().hex[:8]}"
|
|
135
|
-
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
136
|
-
headers = {
|
|
137
|
-
"X-API-Key": CMPND_E2E_API_KEY or "",
|
|
138
|
-
"Content-Type": "application/json",
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
# 1. Start trace with two tags
|
|
142
|
-
start_resp = httpx.post(
|
|
143
|
-
f"{E2E_ENDPOINT}/api/v1/traces/{trace_id}/start",
|
|
144
|
-
headers=headers,
|
|
145
|
-
json={
|
|
146
|
-
"module_class": "StreamTest",
|
|
147
|
-
"start_time": now,
|
|
148
|
-
"project": tag_a,
|
|
149
|
-
"project_tags": [tag_a, tag_b],
|
|
150
|
-
},
|
|
151
|
-
timeout=30.0,
|
|
152
|
-
)
|
|
153
|
-
assert start_resp.status_code == 202, f"start_trace failed: {start_resp.status_code} {start_resp.text}"
|
|
154
|
-
|
|
155
|
-
# 2. Add a span WITHOUT project_tags — simulates old SDK behavior
|
|
156
|
-
span_id = str(uuid.uuid4())
|
|
157
|
-
spans_resp = httpx.post(
|
|
158
|
-
f"{E2E_ENDPOINT}/api/v1/traces/{trace_id}/spans",
|
|
159
|
-
headers=headers,
|
|
160
|
-
json={
|
|
161
|
-
"spans": [
|
|
162
|
-
{
|
|
163
|
-
"span_id": span_id,
|
|
164
|
-
"trace_id": trace_id,
|
|
165
|
-
"name": "predict",
|
|
166
|
-
"span_type": "predict",
|
|
167
|
-
"start_time": now,
|
|
168
|
-
"end_time": now,
|
|
169
|
-
# project and project_tags intentionally omitted
|
|
170
|
-
}
|
|
171
|
-
]
|
|
172
|
-
},
|
|
173
|
-
timeout=30.0,
|
|
174
|
-
)
|
|
175
|
-
assert spans_resp.status_code == 200, f"add_spans failed: {spans_resp.status_code} {spans_resp.text}"
|
|
176
|
-
|
|
177
|
-
# 3. Poll for ingestion, then fetch the trace. The trace row and its spans
|
|
178
|
-
# become visible independently and asynchronously. Read the endpoint
|
|
179
|
-
# directly to keep the wait bounded by one retry loop (CMP-843).
|
|
180
|
-
def _spans():
|
|
181
|
-
resp = server_client._client.get(f"/api/v1/traces/{trace_id}")
|
|
182
|
-
return resp.json().get("spans", []) if resp.status_code == 200 else []
|
|
183
|
-
|
|
184
|
-
spans = poll_until(_spans, timeout=60.0, interval=3.0)
|
|
185
|
-
|
|
186
|
-
assert spans, "trace has no spans after streaming add"
|
|
187
|
-
for span in spans:
|
|
188
|
-
span_tags = span.get("project_tags", [])
|
|
189
|
-
assert tag_a in span_tags and tag_b in span_tags, (
|
|
190
|
-
f"span {span.get('span_id')} should inherit both tags from trace, "
|
|
191
|
-
f"got {span_tags!r} — expected {[tag_a, tag_b]!r}"
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
|
|
195
122
|
def test_project_null_when_unset(server_client, trace_capture):
|
|
196
123
|
"""Without configure(project=...), the trace and spans land with NULL project."""
|
|
197
124
|
try:
|
|
@@ -29,7 +29,9 @@ class TestTracesReadE2E:
|
|
|
29
29
|
trace_id = trace_capture.last_trace_id
|
|
30
30
|
assert trace_id is not None, "no trace captured"
|
|
31
31
|
|
|
32
|
-
#
|
|
32
|
+
# Trace headers and spans publish independently. Wait until both are visible
|
|
33
|
+
# before checking the SDK's detail read.
|
|
34
|
+
server_client.get_trace(trace_id)
|
|
33
35
|
detail = cmpnd.traces.show(trace_id, api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
|
|
34
36
|
assert detail["trace"]["trace_id"] == trace_id
|
|
35
37
|
assert isinstance(detail["spans"], list)
|
|
@@ -78,8 +78,11 @@ def test_cross_trace_q_search_finds_the_span_token():
|
|
|
78
78
|
|
|
79
79
|
# One trace, one span whose name carries the unique token. The write path
|
|
80
80
|
# indexes that name in the span transaction, and the read below waits for
|
|
81
|
-
# the projected search collection to publish it.
|
|
81
|
+
# the projected search collection to publish it. Both records are
|
|
82
|
+
# self-contained submissions: each declares its schema version, and the span
|
|
83
|
+
# carries its own tags rather than inheriting the trace's.
|
|
82
84
|
trace = {
|
|
85
|
+
"schema_version": 1,
|
|
83
86
|
"trace_id": trace_id,
|
|
84
87
|
"start_time": _iso(),
|
|
85
88
|
"end_time": _iso(),
|
|
@@ -89,8 +92,11 @@ def test_cross_trace_q_search_finds_the_span_token():
|
|
|
89
92
|
"span_count": 1,
|
|
90
93
|
"spans": [
|
|
91
94
|
{
|
|
95
|
+
"schema_version": 1,
|
|
92
96
|
"span_id": span_id,
|
|
93
97
|
"trace_id": trace_id,
|
|
98
|
+
"project_tags": [tag],
|
|
99
|
+
"tags": {},
|
|
94
100
|
"name": token,
|
|
95
101
|
"span_type": "tool",
|
|
96
102
|
"start_time": _iso(),
|
|
@@ -14,6 +14,7 @@ from uuid import uuid4
|
|
|
14
14
|
|
|
15
15
|
import httpx
|
|
16
16
|
import pytest
|
|
17
|
+
from cmpnd_ingest.bodies import EncodedTraceBody
|
|
17
18
|
|
|
18
19
|
from cmpnd import retry
|
|
19
20
|
from cmpnd.configuration import Config
|
|
@@ -1179,3 +1180,125 @@ def test_failure_summary_is_quiet_when_nothing_failed():
|
|
|
1179
1180
|
exporter._flush_batch([ExportItem(item_type="trace", data=_make_trace(), timestamp=0.0)])
|
|
1180
1181
|
|
|
1181
1182
|
assert "no non-2xx" in exporter.failure_summary()
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
@pytest.mark.parametrize(
|
|
1186
|
+
"kind,count",
|
|
1187
|
+
[("trace", 1), ("trace", 2), ("trace_start", 1), ("span", 2), ("trace_finalize", 1), ("encoded_trace", 1)],
|
|
1188
|
+
)
|
|
1189
|
+
def test_trace_rejection_logs_server_guidance_and_delivery_loss(kind, count, caplog):
|
|
1190
|
+
body = {
|
|
1191
|
+
"error": "schema_version must be 1",
|
|
1192
|
+
"error_code": "trace_submission_contract",
|
|
1193
|
+
"field": "schema_version",
|
|
1194
|
+
"upgrade": "Upgrade with uv add cmpnd --upgrade",
|
|
1195
|
+
"retryable": False,
|
|
1196
|
+
"unrelated": "private response detail",
|
|
1197
|
+
}
|
|
1198
|
+
capture = ContractCapture([(422, body, {})])
|
|
1199
|
+
exporter = _make_seq_exporter(capture)
|
|
1200
|
+
tid = uuid4()
|
|
1201
|
+
batch = []
|
|
1202
|
+
for _ in range(count):
|
|
1203
|
+
data = (
|
|
1204
|
+
_make_span(trace_id=tid)
|
|
1205
|
+
if kind == "span"
|
|
1206
|
+
else EncodedTraceBody(str(tid), "gzip", gzip.compress(b"{}"))
|
|
1207
|
+
if kind == "encoded_trace"
|
|
1208
|
+
else _make_trace()
|
|
1209
|
+
)
|
|
1210
|
+
batch.append(ExportItem(item_type=kind, data=data, timestamp=0.0))
|
|
1211
|
+
try:
|
|
1212
|
+
with caplog.at_level(logging.ERROR, logger="cmpnd.exporter"):
|
|
1213
|
+
exporter._flush_batch(batch)
|
|
1214
|
+
assert len(capture.requests) == 1
|
|
1215
|
+
assert exporter._error_count == count
|
|
1216
|
+
assert exporter._exported_count == 0
|
|
1217
|
+
assert "422" in caplog.text
|
|
1218
|
+
assert "schema_version must be 1" in caplog.text
|
|
1219
|
+
assert "trace_submission_contract" in caplog.text
|
|
1220
|
+
assert "Upgrade with uv add cmpnd --upgrade" in caplog.text
|
|
1221
|
+
if kind == "encoded_trace":
|
|
1222
|
+
# The exporter never inflates an encoded trace, so it cannot count the
|
|
1223
|
+
# spans the trace embeds and must not report the trace as one record.
|
|
1224
|
+
assert "traces=1, spans=unknown" in caplog.text
|
|
1225
|
+
assert "count=" not in caplog.text
|
|
1226
|
+
else:
|
|
1227
|
+
assert f"count={count}" in caplog.text
|
|
1228
|
+
assert "Records will not be resent" in caplog.text
|
|
1229
|
+
assert "private response detail" not in caplog.text
|
|
1230
|
+
finally:
|
|
1231
|
+
exporter.shutdown()
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
@pytest.mark.parametrize(
|
|
1235
|
+
"body", [[], {"error": {"secret": "private response detail"}}, {"error": "bad\nforged log\r\x1b[31m" + "x" * 5000}]
|
|
1236
|
+
)
|
|
1237
|
+
def test_trace_rejection_handles_unexpected_or_long_error_bodies(body, caplog):
|
|
1238
|
+
requests = []
|
|
1239
|
+
|
|
1240
|
+
def handle(request):
|
|
1241
|
+
requests.append(request)
|
|
1242
|
+
return httpx.Response(400, json=body)
|
|
1243
|
+
|
|
1244
|
+
exporter = _make_exporter(RequestCapture())
|
|
1245
|
+
exporter._client = httpx.Client(base_url=_make_config().endpoint, transport=httpx.MockTransport(handle))
|
|
1246
|
+
try:
|
|
1247
|
+
with caplog.at_level(logging.ERROR, logger="cmpnd.exporter"):
|
|
1248
|
+
exporter._flush_batch([ExportItem(item_type="trace", data=_make_trace(), timestamp=0.0)])
|
|
1249
|
+
messages = [r.getMessage() for r in caplog.records if r.levelno == logging.ERROR]
|
|
1250
|
+
assert len(requests) == 1
|
|
1251
|
+
assert len(messages) == 1
|
|
1252
|
+
assert "400" in messages[0]
|
|
1253
|
+
assert "count=1" in messages[0]
|
|
1254
|
+
assert len(messages[0]) < 2000
|
|
1255
|
+
assert "private response detail" not in messages[0]
|
|
1256
|
+
assert "\nforged log" not in messages[0]
|
|
1257
|
+
assert "\x1b" not in messages[0]
|
|
1258
|
+
finally:
|
|
1259
|
+
exporter.shutdown()
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
def test_trace_rejection_handles_non_json_response(caplog):
|
|
1263
|
+
exporter = _make_exporter(RequestCapture())
|
|
1264
|
+
exporter._client = httpx.Client(
|
|
1265
|
+
base_url=_make_config().endpoint,
|
|
1266
|
+
transport=httpx.MockTransport(lambda _: httpx.Response(413, text="<html>private response detail</html>")),
|
|
1267
|
+
)
|
|
1268
|
+
try:
|
|
1269
|
+
with caplog.at_level(logging.ERROR, logger="cmpnd.exporter"):
|
|
1270
|
+
exporter._flush_batch([ExportItem(item_type="trace", data=_make_trace(), timestamp=0.0)])
|
|
1271
|
+
assert "413" in caplog.text
|
|
1272
|
+
assert "count=1" in caplog.text
|
|
1273
|
+
assert "private response detail" not in caplog.text
|
|
1274
|
+
finally:
|
|
1275
|
+
exporter.shutdown()
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
def test_retry_exhaustion_logs_last_server_message(caplog):
|
|
1279
|
+
capture = ContractCapture([(503, {"error": "database temporarily unavailable", "retryable": True}, {})])
|
|
1280
|
+
exporter = _make_seq_exporter(capture)
|
|
1281
|
+
try:
|
|
1282
|
+
with caplog.at_level(logging.ERROR, logger="cmpnd.exporter"):
|
|
1283
|
+
exporter._flush_batch([ExportItem(item_type="trace", data=_make_trace(), timestamp=0.0)])
|
|
1284
|
+
assert len(capture.requests) == retry.MAX_ATTEMPTS
|
|
1285
|
+
assert "database temporarily unavailable" in caplog.text
|
|
1286
|
+
assert "transient responses over" in caplog.text
|
|
1287
|
+
assert "count=1" in caplog.text
|
|
1288
|
+
finally:
|
|
1289
|
+
exporter.shutdown()
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
def test_rejected_full_trace_logs_embedded_span_loss(caplog):
|
|
1293
|
+
capture = ContractCapture([(422, {"error": "invalid span", "retryable": False}, {})])
|
|
1294
|
+
exporter = _make_seq_exporter(capture)
|
|
1295
|
+
payload = _make_trace().to_api_dict()
|
|
1296
|
+
payload["spans"] = [_make_span().to_api_dict(), _make_span().to_api_dict()]
|
|
1297
|
+
try:
|
|
1298
|
+
with caplog.at_level(logging.ERROR, logger="cmpnd.exporter"):
|
|
1299
|
+
exporter._send_traces([payload])
|
|
1300
|
+
assert "count=3, traces=1, spans=2" in caplog.text
|
|
1301
|
+
assert exporter._exported_count == 0
|
|
1302
|
+
assert len(capture.requests) == 1
|
|
1303
|
+
finally:
|
|
1304
|
+
exporter.shutdown()
|