cmpnd 0.33.0__tar.gz → 0.33.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.33.0 → cmpnd-0.33.2}/PKG-INFO +1 -1
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/execution.py +68 -4
- {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/sdk-instrumentation.md +20 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/README.md +18 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/exporter_http.py +28 -1
- {cmpnd-0.33.0 → cmpnd-0.33.2}/pyproject.toml +1 -1
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/conftest.py +44 -15
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_capture_prompts.py +25 -4
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_edge_cases.py +18 -6
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_parallel_export.py +33 -10
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_project_attribution.py +61 -22
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_save_load.py +76 -16
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_trace_structure.py +83 -23
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_real_provider_twins.py +57 -12
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_execute.py +150 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_ingest_second_producer.py +53 -0
- cmpnd-0.33.2/tests/test_real_provider_twins_readiness.py +68 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/uv.lock +1 -1
- {cmpnd-0.33.0 → cmpnd-0.33.2}/.gitignore +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/AGENTS.md +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/CLAUDE.md +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/README.md +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_assessments.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_authored_import.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_deployments.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_exporter_pool.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_http.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_runtime_tools.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_traces.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_valsets.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/authored.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/callback.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/assessments.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/tree_help.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/client.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/configuration.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/context.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/datasets.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/decorators.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/deployment.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/deployment_metadata.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/exporter.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/helpers.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/identity.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/imaging.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/lm_configuration.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/mcp_servers.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/models.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/optimization.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/packaging.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/references.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/retry.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/traces.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/cli.md +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/client.md +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/custom-proposer.md +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/reading-traces.md +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/examples/local_ollama.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/trace_facts.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/pyproject.toml +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/release-stress.sh +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/stubs/dspy/utils/dummies.pyi +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/_deployment_signature_helpers.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/_fake_clock.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/_resolve_refs.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/_traced_run.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/conftest.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_assessments_cli.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_traces_read.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_valsets_cli.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/authored_optimize/judge.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/conftest.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/__main__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/drain.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/ingest.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/rlm_size_report.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/sweep.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/test_rlm_size_report.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/conftest.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_health.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/span_reference_fixture.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/_provider.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/_readiness.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/_result.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_readiness.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_trace_search.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_adapter_reentrancy.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_assessments_api.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_authored.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_authored_bundle_collisions.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_authored_metric_contract.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_authored_named_models.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_authored_optimize.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_callback.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_assessments.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_signatures.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_tree_help.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_valsets.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_client.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_configuration.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_context.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_datasets.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_decorators.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_deploy.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_deploy_mcp_tool_references.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_deployment_metadata.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_deployments_read.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_eval.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_eval_start.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_execute_run_identity.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_exporter.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_gepa_output_normalization.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_identity.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_imaging.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_import_weight.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_integration.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_lm_configuration.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_lm_usage_shapes.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_models.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_packaging.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_poll_until.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_project_payload.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_release_stress_script.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_retry.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_run_attribution_paths.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_runtime_tools.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_schemas.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_span_references.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_span_sent_once.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_trace_fact_wire_contract.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_trace_facts.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_trace_render.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_traces_read.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_truncation.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_valsets_api.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_xxh64.py +0 -0
- {cmpnd-0.33.0 → cmpnd-0.33.2}/todo.md +0 -0
|
@@ -8,6 +8,8 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import inspect
|
|
10
10
|
import keyword
|
|
11
|
+
import math
|
|
12
|
+
import time
|
|
11
13
|
from typing import TYPE_CHECKING, Any
|
|
12
14
|
|
|
13
15
|
import httpx
|
|
@@ -20,6 +22,7 @@ from httpx import HTTPStatusError
|
|
|
20
22
|
|
|
21
23
|
from . import _http, configuration
|
|
22
24
|
from .deployment_metadata import DeploymentArtifactMetadata, DeploymentSignatureResponse, ProgramFieldType
|
|
25
|
+
from .retry import retry_after_seconds
|
|
23
26
|
|
|
24
27
|
if TYPE_CHECKING:
|
|
25
28
|
from dspy.primitives.prediction import Prediction
|
|
@@ -89,6 +92,64 @@ def _retryable_provider_failure(
|
|
|
89
92
|
)
|
|
90
93
|
|
|
91
94
|
|
|
95
|
+
_CAPACITY_MAX_ATTEMPTS = 3
|
|
96
|
+
_CAPACITY_MAX_WAIT_SECONDS = 15.0
|
|
97
|
+
_capacity_now = time.monotonic
|
|
98
|
+
_capacity_sleep = time.sleep
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _pre_admission_capacity(resp: httpx.Response) -> bool:
|
|
102
|
+
"""Only a worker's explicit pre-admission refusal proves no program ran."""
|
|
103
|
+
if resp.status_code != 503:
|
|
104
|
+
return False
|
|
105
|
+
try:
|
|
106
|
+
body = resp.json()
|
|
107
|
+
except ValueError:
|
|
108
|
+
return False
|
|
109
|
+
return (
|
|
110
|
+
isinstance(body, dict)
|
|
111
|
+
and body.get("retryable") is True
|
|
112
|
+
and isinstance(body.get("error"), dict)
|
|
113
|
+
and body["error"].get("type") == "Capacity"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _post_with_capacity_retry(
|
|
118
|
+
client: httpx.Client, url: str, body: dict[str, Any], api_key: str, timeout: Any
|
|
119
|
+
) -> httpx.Response:
|
|
120
|
+
# httpx's numeric timeout applies to each network phase, not the entire call.
|
|
121
|
+
# Also bound the waits and each later request by elapsed time so retries do
|
|
122
|
+
# not silently grant the call a fresh timeout.
|
|
123
|
+
deadline = (
|
|
124
|
+
_capacity_now() + timeout
|
|
125
|
+
if isinstance(timeout, (int, float)) and not isinstance(timeout, bool) and timeout > 0
|
|
126
|
+
else None
|
|
127
|
+
)
|
|
128
|
+
last_response: httpx.Response | None = None
|
|
129
|
+
for attempt in range(_CAPACITY_MAX_ATTEMPTS):
|
|
130
|
+
remaining = deadline - _capacity_now() if deadline is not None else None
|
|
131
|
+
if attempt and remaining is not None and remaining <= 0:
|
|
132
|
+
assert last_response is not None
|
|
133
|
+
return last_response
|
|
134
|
+
if remaining is None:
|
|
135
|
+
resp = client.post(url, json=body, headers={"X-API-Key": api_key})
|
|
136
|
+
else:
|
|
137
|
+
resp = client.post(url, json=body, headers={"X-API-Key": api_key}, timeout=remaining)
|
|
138
|
+
last_response = resp
|
|
139
|
+
if not _pre_admission_capacity(resp) or attempt == _CAPACITY_MAX_ATTEMPTS - 1:
|
|
140
|
+
return resp
|
|
141
|
+
|
|
142
|
+
asked = retry_after_seconds(resp)
|
|
143
|
+
if asked is not None and (not math.isfinite(asked) or asked > _CAPACITY_MAX_WAIT_SECONDS):
|
|
144
|
+
return resp
|
|
145
|
+
delay = max(0.5 * 2**attempt, asked or 0.0, 0.0)
|
|
146
|
+
if delay > _CAPACITY_MAX_WAIT_SECONDS or (deadline is not None and delay >= deadline - _capacity_now()):
|
|
147
|
+
return resp
|
|
148
|
+
_capacity_sleep(delay)
|
|
149
|
+
assert last_response is not None
|
|
150
|
+
return last_response
|
|
151
|
+
|
|
152
|
+
|
|
92
153
|
class DeployedProgram:
|
|
93
154
|
"""A deployed DSPy program that can be called like a local module.
|
|
94
155
|
|
|
@@ -258,7 +319,8 @@ def execute(deployment_id, timeout=30, metadata=None, tags=None, **inputs):
|
|
|
258
319
|
Args:
|
|
259
320
|
deployment_id: The deployment ID returned by deploy(), or a
|
|
260
321
|
DeployedProgram instance.
|
|
261
|
-
timeout: HTTP request timeout in seconds (default 30).
|
|
322
|
+
timeout: HTTP request timeout in seconds (default 30). Capacity retries
|
|
323
|
+
share this budget when it is numeric.
|
|
262
324
|
metadata: Optional ``dict[str, str]`` of decision-time correlation
|
|
263
325
|
data merged onto the server-side root trace (e.g.
|
|
264
326
|
``{"discord.message_id": "123"}``). Forwarded to the execute
|
|
@@ -298,10 +360,12 @@ def execute(deployment_id, timeout=30, metadata=None, tags=None, **inputs):
|
|
|
298
360
|
body["tags"] = _validate_str_map("tags", tags)
|
|
299
361
|
|
|
300
362
|
with httpx.Client(timeout=timeout) as client:
|
|
301
|
-
resp =
|
|
363
|
+
resp = _post_with_capacity_retry(
|
|
364
|
+
client,
|
|
302
365
|
f"{config.endpoint}/api/v1/deployments/{dep_id}/execute",
|
|
303
|
-
|
|
304
|
-
|
|
366
|
+
body,
|
|
367
|
+
config.api_key,
|
|
368
|
+
timeout,
|
|
305
369
|
)
|
|
306
370
|
|
|
307
371
|
if resp.status_code == 409:
|
|
@@ -241,6 +241,11 @@ All tunables live on `Config` in `python-sdk/cmpnd/config.py:12`:
|
|
|
241
241
|
|
|
242
242
|
Override any of these by passing kwargs to `cmpnd.configure(...)`.
|
|
243
243
|
|
|
244
|
+
The HTTP transport also reads connect, read, write and connection pool timeouts from
|
|
245
|
+
environment variables when the exporter is constructed. Each defaults to 30 seconds. See
|
|
246
|
+
[`cmpnd-ingest` HTTP request timeouts](../ingest/README.md#http-request-timeouts) for
|
|
247
|
+
the variable names and their scope.
|
|
248
|
+
|
|
244
249
|
### Shape of the loop
|
|
245
250
|
|
|
246
251
|
- `start()` at `exporter_http.py:131` creates the worker thread and the `ThreadPoolExecutor`, then registers `shutdown()` via `threading._register_atexit` (or `atexit.register` as a fallback). The registration ordering is deliberate — there's a comment at `exporter_http.py:139` explaining why it has to go after the executor init.
|
|
@@ -358,6 +363,21 @@ private prediction metadata, so a program can use `output` or
|
|
|
358
363
|
`deployment_run_id` as an output field. The accessor also accepts an execute
|
|
359
364
|
exception. Trace delivery can finish after the execute response arrives.
|
|
360
365
|
|
|
366
|
+
When the worker refuses an execute before admitting it, serve returns HTTP 503
|
|
367
|
+
with `error.type: "Capacity"`, `retryable: true`, and a `Retry-After` hint.
|
|
368
|
+
`cmpnd.execute()` retries only that exact response, up to three total attempts.
|
|
369
|
+
It waits at least as long as the hint, but never waits more than 15 seconds for
|
|
370
|
+
one retry. If the hint exceeds that bound, the SDK returns the Capacity error
|
|
371
|
+
without retrying early. Other failures, including provider errors and uncertain
|
|
372
|
+
transport failures, return immediately because the program may have run.
|
|
373
|
+
|
|
374
|
+
A numeric `timeout` still controls each HTTP request. The SDK also charges
|
|
375
|
+
Capacity retry waits and later requests against the same elapsed budget. The
|
|
376
|
+
default is 30 seconds, so callers running programs that may spend longer in
|
|
377
|
+
admission or execution must pass a larger timeout. Serve assigns a fresh run id
|
|
378
|
+
to each attempt. When serve responds, `cmpnd.deployment_run_id()` returns only
|
|
379
|
+
the final attempt's run id, whether execute succeeds or raises.
|
|
380
|
+
|
|
361
381
|
```python
|
|
362
382
|
try:
|
|
363
383
|
result = deployed(question="What changed?")
|
|
@@ -97,6 +97,24 @@ queue is `max_queue_size` deep and sheds past it rather than growing. Only the t
|
|
|
97
97
|
run-record entry points block, because a run must exist before anything referring to it
|
|
98
98
|
ships.
|
|
99
99
|
|
|
100
|
+
## HTTP request timeouts
|
|
101
|
+
|
|
102
|
+
`BatchExporter` reads these environment variables when it is constructed. Each accepts a finite,
|
|
103
|
+
positive number of seconds and defaults to 30 seconds.
|
|
104
|
+
|
|
105
|
+
| Variable | What it bounds |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `CMPND_INGEST_CONNECT_TIMEOUT_SECONDS` | Establishing a connection to serve |
|
|
108
|
+
| `CMPND_INGEST_READ_TIMEOUT_SECONDS` | Waiting for the next response chunk |
|
|
109
|
+
| `CMPND_INGEST_WRITE_TIMEOUT_SECONDS` | Waiting to send the next request chunk |
|
|
110
|
+
| `CMPND_INGEST_POOL_TIMEOUT_SECONDS` | Waiting for an available connection |
|
|
111
|
+
|
|
112
|
+
The same settings apply to blocking run writes and queued exports in both the SDK and
|
|
113
|
+
the hosted search process. HTTPX applies each timeout to a phase of network activity;
|
|
114
|
+
none sets a total request deadline. The body size caps do not bound upload time or
|
|
115
|
+
serve's processing time. Raise the write timeout for slow uploads or the read timeout
|
|
116
|
+
for slow responses. Export retry budgets remain separate.
|
|
117
|
+
|
|
100
118
|
## Proving conformance from another producer
|
|
101
119
|
|
|
102
120
|
Set `CMPND_INGEST_REQUEST_LOG=<path>` and the transport appends one
|
|
@@ -20,6 +20,7 @@ import base64
|
|
|
20
20
|
import gzip
|
|
21
21
|
import json
|
|
22
22
|
import logging
|
|
23
|
+
import math
|
|
23
24
|
import os
|
|
24
25
|
import queue
|
|
25
26
|
import threading
|
|
@@ -112,6 +113,31 @@ class ExportRetriesExhausted(RuntimeError):
|
|
|
112
113
|
# spending a round trip — and megabytes of gzip — on a 400 it could have predicted.
|
|
113
114
|
CHECKPOINT_STATE_MAX_BYTES = 8 * 1024 * 1024
|
|
114
115
|
|
|
116
|
+
_DEFAULT_HTTP_TIMEOUT_SECONDS = 30.0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _http_timeout_seconds(phase: str) -> float:
|
|
120
|
+
name = f"CMPND_INGEST_{phase}_TIMEOUT_SECONDS"
|
|
121
|
+
value = os.getenv(name)
|
|
122
|
+
if value is None:
|
|
123
|
+
return _DEFAULT_HTTP_TIMEOUT_SECONDS
|
|
124
|
+
try:
|
|
125
|
+
seconds = float(value)
|
|
126
|
+
except ValueError as exc:
|
|
127
|
+
raise ValueError(f"{name} must be a finite, positive number of seconds") from exc
|
|
128
|
+
if not math.isfinite(seconds) or seconds <= 0:
|
|
129
|
+
raise ValueError(f"{name} must be a finite, positive number of seconds")
|
|
130
|
+
return seconds
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _http_timeout() -> httpx.Timeout:
|
|
134
|
+
return httpx.Timeout(
|
|
135
|
+
connect=_http_timeout_seconds("CONNECT"),
|
|
136
|
+
read=_http_timeout_seconds("READ"),
|
|
137
|
+
write=_http_timeout_seconds("WRITE"),
|
|
138
|
+
pool=_http_timeout_seconds("POOL"),
|
|
139
|
+
)
|
|
140
|
+
|
|
115
141
|
|
|
116
142
|
class CheckpointStateTooLarge(ValueError):
|
|
117
143
|
"""A checkpoint's state is past the contract's ceiling, so it was never sent.
|
|
@@ -273,6 +299,7 @@ class BatchExporter:
|
|
|
273
299
|
def __init__(self, config: ExporterConfig, transport: httpx.BaseTransport | None = None) -> None:
|
|
274
300
|
self._config = config
|
|
275
301
|
self._transport = transport
|
|
302
|
+
self._http_timeout = _http_timeout()
|
|
276
303
|
# Two lanes, one bound each — see _SHEDDABLE_ITEM_TYPES for why the
|
|
277
304
|
# split exists, and _next_item for why the drainer reads them in this
|
|
278
305
|
# order. Both are bounded: an unbounded lifecycle lane would trade a
|
|
@@ -963,7 +990,7 @@ class BatchExporter:
|
|
|
963
990
|
"X-API-Key": self._config.api_key,
|
|
964
991
|
"Content-Type": "application/json",
|
|
965
992
|
},
|
|
966
|
-
"timeout":
|
|
993
|
+
"timeout": self._http_timeout,
|
|
967
994
|
}
|
|
968
995
|
if self._transport:
|
|
969
996
|
kwargs["transport"] = self._transport
|
|
@@ -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.33.
|
|
8
|
+
version = "0.33.2"
|
|
9
9
|
description = "DSPy observability and deployment SDK for cmpnd"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -17,6 +17,8 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
import os
|
|
19
19
|
import time
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from typing import Any
|
|
20
22
|
|
|
21
23
|
import httpx
|
|
22
24
|
import pytest
|
|
@@ -118,12 +120,12 @@ def flush_and_wait(seconds: float = 3.0) -> None:
|
|
|
118
120
|
|
|
119
121
|
|
|
120
122
|
# ---------------------------------------------------------------------------
|
|
121
|
-
# ServerClient — thin wrapper around the
|
|
123
|
+
# ServerClient — thin wrapper around the serve API
|
|
122
124
|
# ---------------------------------------------------------------------------
|
|
123
125
|
|
|
124
126
|
|
|
125
127
|
class ServerClient:
|
|
126
|
-
"""HTTP client for querying the
|
|
128
|
+
"""HTTP client for querying the serve API during tests."""
|
|
127
129
|
|
|
128
130
|
def __init__(self, endpoint: str, api_key: str) -> None:
|
|
129
131
|
self._client = httpx.Client(
|
|
@@ -140,16 +142,44 @@ class ServerClient:
|
|
|
140
142
|
|
|
141
143
|
# -- Traces -------------------------------------------------------------
|
|
142
144
|
|
|
143
|
-
def get_trace(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
145
|
+
def get_trace(
|
|
146
|
+
self,
|
|
147
|
+
trace_id: str,
|
|
148
|
+
*,
|
|
149
|
+
ready: Callable[[dict[str, Any]], bool] | None = None,
|
|
150
|
+
timeout: float = 60.0,
|
|
151
|
+
interval: float = 2.0,
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
"""Wait for a selected trace and the fields its caller will assert."""
|
|
154
|
+
deadline = time.monotonic() + timeout
|
|
155
|
+
last_detail: dict[str, Any] | None = None
|
|
156
|
+
last_status: int | None = None
|
|
157
|
+
while (remaining := deadline - time.monotonic()) > 0:
|
|
158
|
+
try:
|
|
159
|
+
response = self._client.get(f"/api/v1/traces/{trace_id}", timeout=min(10.0, remaining))
|
|
160
|
+
except httpx.TimeoutException:
|
|
161
|
+
response = None
|
|
162
|
+
if response is not None:
|
|
163
|
+
last_status = response.status_code
|
|
164
|
+
if response.status_code == 200:
|
|
165
|
+
detail = response.json()
|
|
166
|
+
assert isinstance(detail, dict), f"trace {trace_id} returned a non-object detail"
|
|
167
|
+
last_detail = detail
|
|
168
|
+
if (ready is None or ready(detail)) and time.monotonic() <= deadline:
|
|
169
|
+
return detail
|
|
170
|
+
elif response.status_code != 404:
|
|
171
|
+
response.raise_for_status()
|
|
172
|
+
remaining = deadline - time.monotonic()
|
|
173
|
+
if remaining > 0:
|
|
174
|
+
time.sleep(min(interval, remaining))
|
|
175
|
+
if last_detail is None:
|
|
176
|
+
raise AssertionError(f"trace {trace_id} did not appear within {timeout}s; last HTTP status={last_status}")
|
|
177
|
+
trace = last_detail.get("trace") or last_detail
|
|
178
|
+
spans = last_detail.get("spans")
|
|
179
|
+
raise AssertionError(
|
|
180
|
+
f"trace {trace_id} did not reach the asserted state within {timeout}s; "
|
|
181
|
+
f"last trace status={trace.get('status')!r}, spans={len(spans) if isinstance(spans, list) else None}"
|
|
182
|
+
)
|
|
153
183
|
|
|
154
184
|
def search_traces(self, **filters) -> dict:
|
|
155
185
|
resp = self._client.post("/api/v1/traces/search", json=filters)
|
|
@@ -218,9 +248,8 @@ class ServerClient:
|
|
|
218
248
|
def _pin_fake_lm() -> None:
|
|
219
249
|
"""Set dspy's active LM to the deterministic in-process fake (CMP-255).
|
|
220
250
|
|
|
221
|
-
The dummy api_key/api_base
|
|
222
|
-
|
|
223
|
-
the fake ignores them. Only touches ``dspy.settings.lm`` — the litellm
|
|
251
|
+
The dummy api_key/api_base satisfy DSPy's LM configuration; the fake
|
|
252
|
+
ignores them. Only touches ``dspy.settings.lm`` — the litellm
|
|
224
253
|
provider map (and any test-specific responder) is left alone.
|
|
225
254
|
"""
|
|
226
255
|
import dspy
|
|
@@ -12,6 +12,7 @@ from cmpnd.callback import CmpndCallback
|
|
|
12
12
|
from tests.e2e.conftest import (
|
|
13
13
|
CMPND_E2E_API_KEY,
|
|
14
14
|
E2E_ENDPOINT,
|
|
15
|
+
ServerClient,
|
|
15
16
|
flush_and_wait,
|
|
16
17
|
)
|
|
17
18
|
|
|
@@ -24,7 +25,7 @@ def _is_empty(value) -> bool:
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class TestCapturePromptsDisabled:
|
|
27
|
-
def test_lm_call_inputs_empty_when_disabled(self, server_client, trace_capture):
|
|
28
|
+
def test_lm_call_inputs_empty_when_disabled(self, server_client: ServerClient, trace_capture):
|
|
28
29
|
"""With capture_prompts=False: LM_CALL inputs should be empty."""
|
|
29
30
|
from cmpnd.exporter import ensure_exporter_started, shutdown_exporter
|
|
30
31
|
|
|
@@ -55,7 +56,16 @@ class TestCapturePromptsDisabled:
|
|
|
55
56
|
|
|
56
57
|
trace_id = trace_capture.last_trace_id
|
|
57
58
|
assert trace_id is not None
|
|
58
|
-
data = server_client.get_trace(
|
|
59
|
+
data = server_client.get_trace(
|
|
60
|
+
trace_id,
|
|
61
|
+
ready=lambda detail: (
|
|
62
|
+
detail["trace"]["status"] == "ok"
|
|
63
|
+
and any(
|
|
64
|
+
span.get("span_type") == "lm_call" and not _is_empty(span.get("outputs"))
|
|
65
|
+
for span in detail["spans"]
|
|
66
|
+
)
|
|
67
|
+
),
|
|
68
|
+
)
|
|
59
69
|
spans = data["spans"]
|
|
60
70
|
|
|
61
71
|
lm_spans = [s for s in spans if s.get("span_type") == "lm_call"]
|
|
@@ -74,7 +84,7 @@ class TestCapturePromptsDisabled:
|
|
|
74
84
|
|
|
75
85
|
|
|
76
86
|
class TestCapturePromptsEnabled:
|
|
77
|
-
def test_lm_call_inputs_present_when_enabled(self, server_client, trace_capture):
|
|
87
|
+
def test_lm_call_inputs_present_when_enabled(self, server_client: ServerClient, trace_capture):
|
|
78
88
|
"""With capture_prompts=True: LM_CALL inputs should contain the formatted prompt."""
|
|
79
89
|
# Reconfigure with capture_prompts=True
|
|
80
90
|
cmpnd.configure(
|
|
@@ -99,7 +109,18 @@ class TestCapturePromptsEnabled:
|
|
|
99
109
|
|
|
100
110
|
trace_id = trace_capture.last_trace_id
|
|
101
111
|
assert trace_id is not None
|
|
102
|
-
data = server_client.get_trace(
|
|
112
|
+
data = server_client.get_trace(
|
|
113
|
+
trace_id,
|
|
114
|
+
ready=lambda detail: (
|
|
115
|
+
detail["trace"]["status"] == "ok"
|
|
116
|
+
and any(
|
|
117
|
+
span.get("span_type") == "lm_call"
|
|
118
|
+
and not _is_empty(span.get("inputs"))
|
|
119
|
+
and not _is_empty(span.get("outputs"))
|
|
120
|
+
for span in detail["spans"]
|
|
121
|
+
)
|
|
122
|
+
),
|
|
123
|
+
)
|
|
103
124
|
spans = data["spans"]
|
|
104
125
|
|
|
105
126
|
lm_spans = [s for s in spans if s.get("span_type") == "lm_call"]
|
|
@@ -9,13 +9,13 @@ from __future__ import annotations
|
|
|
9
9
|
import dspy
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
12
|
-
from tests.e2e.conftest import flush_and_wait
|
|
12
|
+
from tests.e2e.conftest import ServerClient, flush_and_wait
|
|
13
13
|
|
|
14
14
|
pytestmark = pytest.mark.e2e
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class TestMultipleSequentialCalls:
|
|
18
|
-
def test_sequential_calls_distinct_traces(self, server_client, trace_capture):
|
|
18
|
+
def test_sequential_calls_distinct_traces(self, server_client: ServerClient, trace_capture):
|
|
19
19
|
predict = dspy.Predict("question -> answer")
|
|
20
20
|
|
|
21
21
|
# First call
|
|
@@ -33,15 +33,15 @@ class TestMultipleSequentialCalls:
|
|
|
33
33
|
flush_and_wait()
|
|
34
34
|
|
|
35
35
|
# Both traces should be on the server
|
|
36
|
-
first_data = server_client.get_trace(first_trace_id)
|
|
36
|
+
first_data = server_client.get_trace(first_trace_id, ready=lambda data: data["trace"]["status"] == "ok")
|
|
37
37
|
assert first_data["trace"]["status"] == "ok"
|
|
38
38
|
|
|
39
|
-
second_data = server_client.get_trace(second_trace_id)
|
|
39
|
+
second_data = server_client.get_trace(second_trace_id, ready=lambda data: data["trace"]["status"] == "ok")
|
|
40
40
|
assert second_data["trace"]["status"] == "ok"
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
class TestLargeInputTruncation:
|
|
44
|
-
def test_large_input(self, server_client, trace_capture):
|
|
44
|
+
def test_large_input(self, server_client: ServerClient, trace_capture):
|
|
45
45
|
predict = dspy.Predict("text -> summary")
|
|
46
46
|
large_text = "The quick brown fox. " * 2000
|
|
47
47
|
predict(text=large_text)
|
|
@@ -51,7 +51,19 @@ class TestLargeInputTruncation:
|
|
|
51
51
|
trace_id = trace_capture.last_trace_id
|
|
52
52
|
assert trace_id is not None
|
|
53
53
|
|
|
54
|
-
data
|
|
54
|
+
def ready(data: dict) -> bool:
|
|
55
|
+
predict_spans = [
|
|
56
|
+
span
|
|
57
|
+
for span in data["spans"]
|
|
58
|
+
if span.get("span_type") == "predict" or "predict" in span.get("name", "").lower()
|
|
59
|
+
]
|
|
60
|
+
return (
|
|
61
|
+
data["trace"]["status"] == "ok"
|
|
62
|
+
and bool(predict_spans)
|
|
63
|
+
and "text" in (predict_spans[0].get("inputs") or {})
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
data = server_client.get_trace(trace_id, ready=ready)
|
|
55
67
|
trace = data["trace"]
|
|
56
68
|
spans = data["spans"]
|
|
57
69
|
|
|
@@ -7,11 +7,14 @@ across multiple endpoint types in a single flush cycle.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
10
13
|
import dspy
|
|
11
14
|
import pytest
|
|
12
15
|
|
|
13
16
|
from cmpnd.exporter import get_exporter
|
|
14
|
-
from tests.e2e.conftest import flush_and_wait
|
|
17
|
+
from tests.e2e.conftest import ServerClient, flush_and_wait
|
|
15
18
|
|
|
16
19
|
pytestmark = pytest.mark.e2e
|
|
17
20
|
|
|
@@ -21,12 +24,12 @@ pytestmark = pytest.mark.e2e
|
|
|
21
24
|
# ---------------------------------------------------------------------------
|
|
22
25
|
|
|
23
26
|
|
|
24
|
-
def _fetch_trace(server_client, trace_capture):
|
|
27
|
+
def _fetch_trace(server_client: ServerClient, trace_capture, ready: Callable[[dict[str, Any]], bool]):
|
|
25
28
|
"""Flush, then fetch the last captured trace and its spans."""
|
|
26
29
|
flush_and_wait()
|
|
27
30
|
trace_id = trace_capture.last_trace_id
|
|
28
31
|
assert trace_id is not None, "No trace was captured"
|
|
29
|
-
data = server_client.get_trace(trace_id)
|
|
32
|
+
data = server_client.get_trace(trace_id, ready=ready)
|
|
30
33
|
return data["trace"], data["spans"]
|
|
31
34
|
|
|
32
35
|
|
|
@@ -38,12 +41,21 @@ def _fetch_trace(server_client, trace_capture):
|
|
|
38
41
|
class TestParallelExportTraceIntegrity:
|
|
39
42
|
"""Verify traces and spans arrive intact after parallel dispatch."""
|
|
40
43
|
|
|
41
|
-
def test_trace_with_spans_arrives_intact(self, server_client, trace_capture):
|
|
44
|
+
def test_trace_with_spans_arrives_intact(self, server_client: ServerClient, trace_capture):
|
|
42
45
|
"""A standard DSPy call should produce a complete trace with all spans."""
|
|
43
46
|
predict = dspy.Predict("question -> answer")
|
|
44
47
|
predict(question="What is the speed of light?")
|
|
45
48
|
|
|
46
|
-
trace, spans = _fetch_trace(
|
|
49
|
+
trace, spans = _fetch_trace(
|
|
50
|
+
server_client,
|
|
51
|
+
trace_capture,
|
|
52
|
+
lambda data: (
|
|
53
|
+
data["trace"]["program_name"] is not None
|
|
54
|
+
and data["trace"]["status"] in ("ok", "error")
|
|
55
|
+
and len(data["spans"]) >= 1
|
|
56
|
+
and all(span["span_type"] is not None and span["name"] is not None for span in data["spans"])
|
|
57
|
+
),
|
|
58
|
+
)
|
|
47
59
|
|
|
48
60
|
assert trace["program_name"] is not None
|
|
49
61
|
assert trace["status"] in ("ok", "error")
|
|
@@ -54,18 +66,26 @@ class TestParallelExportTraceIntegrity:
|
|
|
54
66
|
assert span["span_type"] is not None
|
|
55
67
|
assert span["name"] is not None
|
|
56
68
|
|
|
57
|
-
def test_token_counts_populated(self, server_client, trace_capture):
|
|
69
|
+
def test_token_counts_populated(self, server_client: ServerClient, trace_capture):
|
|
58
70
|
"""Token counts should survive parallel serialization."""
|
|
59
71
|
predict = dspy.Predict("topic -> explanation")
|
|
60
72
|
predict(topic="Gravity")
|
|
61
73
|
|
|
62
|
-
trace, spans = _fetch_trace(
|
|
74
|
+
trace, spans = _fetch_trace(
|
|
75
|
+
server_client,
|
|
76
|
+
trace_capture,
|
|
77
|
+
lambda data: (
|
|
78
|
+
(data["trace"].get("total_tokens") or 0) > 0
|
|
79
|
+
and (data["trace"].get("prompt_tokens") or 0) > 0
|
|
80
|
+
and (data["trace"].get("completion_tokens") or 0) > 0
|
|
81
|
+
),
|
|
82
|
+
)
|
|
63
83
|
|
|
64
84
|
assert trace["total_tokens"] > 0
|
|
65
85
|
assert trace["prompt_tokens"] > 0
|
|
66
86
|
assert trace["completion_tokens"] > 0
|
|
67
87
|
|
|
68
|
-
def test_no_data_loss_after_parallel_flush(self, server_client, trace_capture):
|
|
88
|
+
def test_no_data_loss_after_parallel_flush(self, server_client: ServerClient, trace_capture):
|
|
69
89
|
"""Exporter stats should show zero drops/errors for a clean call."""
|
|
70
90
|
predict = dspy.Predict("animal -> habitat")
|
|
71
91
|
predict(animal="Penguin")
|
|
@@ -85,7 +105,7 @@ class TestParallelExportTraceIntegrity:
|
|
|
85
105
|
class TestParallelExportMixedWorkload:
|
|
86
106
|
"""Verify multiple export types dispatched in parallel all land on server."""
|
|
87
107
|
|
|
88
|
-
def test_multiple_traces_in_quick_succession(self, server_client, trace_capture):
|
|
108
|
+
def test_multiple_traces_in_quick_succession(self, server_client: ServerClient, trace_capture):
|
|
89
109
|
"""Multiple DSPy calls before flush should all arrive via parallel dispatch."""
|
|
90
110
|
predict = dspy.Predict("fruit -> color")
|
|
91
111
|
|
|
@@ -99,6 +119,9 @@ class TestParallelExportMixedWorkload:
|
|
|
99
119
|
|
|
100
120
|
# Verify all traces are retrievable from the server
|
|
101
121
|
for trace_id in trace_capture.trace_ids[-3:]:
|
|
102
|
-
data = server_client.get_trace(
|
|
122
|
+
data = server_client.get_trace(
|
|
123
|
+
trace_id,
|
|
124
|
+
ready=lambda detail: detail["trace"]["status"] in ("ok", "error") and len(detail["spans"]) >= 1,
|
|
125
|
+
)
|
|
103
126
|
assert data["trace"]["status"] in ("ok", "error")
|
|
104
127
|
assert len(data["spans"]) >= 1
|