cmpnd 0.8.2__tar.gz → 0.8.3__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.8.2 → cmpnd-0.8.3}/PKG-INFO +1 -1
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/exporter_http.py +172 -111
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/optimization.py +211 -35
- cmpnd-0.8.3/cmpnd/retry.py +186 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/docs/sdk-instrumentation.md +63 -7
- {cmpnd-0.8.2 → cmpnd-0.8.3}/pyproject.toml +1 -1
- cmpnd-0.8.3/tests/_fake_clock.py +31 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/test_multi_client_stress.py +14 -8
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/test_reliability_classify.py +44 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_eval_start.py +0 -2
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_exporter.py +139 -100
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_optimization_start.py +77 -2
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_optimize.py +354 -3
- cmpnd-0.8.3/tests/test_retry.py +113 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_schemas.py +60 -3
- {cmpnd-0.8.2 → cmpnd-0.8.3}/uv.lock +1 -1
- {cmpnd-0.8.2 → cmpnd-0.8.3}/.gitignore +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/CLAUDE.md +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/README.md +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/__init__.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/callback.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/configuration.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/context.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/datasets.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/decorators.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/deployment.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/execution.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/exporter.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/helpers.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/identity.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/imaging.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/models.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/cmpnd/packaging.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/examples/local_ollama.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/__init__.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/integration/__init__.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/integration/conftest.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/__init__.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/conftest.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/test_health.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/__init__.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/_provider.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_callback.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_configuration.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_context.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_datasets.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_decorators.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_deploy.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_eval.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_execute.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_identity.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_imaging.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_import_weight.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_integration.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_models.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_packaging.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_project_payload.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_stats_export.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_trace_render.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_truncation.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/tests/test_xxh64.py +0 -0
- {cmpnd-0.8.2 → cmpnd-0.8.3}/todo.md +0 -0
|
@@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Any, cast
|
|
|
23
23
|
|
|
24
24
|
import httpx
|
|
25
25
|
|
|
26
|
-
from . import configuration
|
|
26
|
+
from . import configuration, retry
|
|
27
27
|
|
|
28
28
|
if TYPE_CHECKING:
|
|
29
29
|
from . import models
|
|
@@ -55,73 +55,73 @@ def _send_json(client: httpx.Client, method: str, path: str, payload: Any) -> ht
|
|
|
55
55
|
return client.request(method, path, content=body, headers=_GZIP_HEADERS)
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
_RETRYABLE_STATUS = frozenset({502, 503, 504})
|
|
58
|
+
# Backpressure, not unavailability: the server wants the record shed, so the
|
|
59
|
+
# caller counts it as *dropped* and it is never retried — not even carrying a
|
|
60
|
+
# retryable flag. Named because that exemption is the one thing the contract's
|
|
61
|
+
# body-flag read must not be able to override.
|
|
62
|
+
_SHED_STATUS = 429
|
|
64
63
|
|
|
65
|
-
# Backpressure, not unavailability: the server's buffer is full and it wants the
|
|
66
|
-
# record dropped, so the caller counts it as *dropped* and we never retry it —
|
|
67
|
-
# not even if the body carried a retryable flag. Named because that exemption is
|
|
68
|
-
# the one thing the body-flag read below must not be able to override.
|
|
69
|
-
_DROP_STATUS = 429
|
|
70
64
|
|
|
65
|
+
class ExportRetriesExhausted(RuntimeError):
|
|
66
|
+
"""A data POST rode through transient answers until its budget ran out.
|
|
71
67
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
The `"retryable": true` error-body field is the server's stable machine
|
|
76
|
-
signal for "this failed transiently and did not commit — send it again". It
|
|
77
|
-
is what keeps this side off a hard-coded status list: the server can start
|
|
78
|
-
signalling a new transient condition without an SDK release. A body that
|
|
79
|
-
isn't JSON, isn't an object, or lacks the field reads as not-retryable.
|
|
68
|
+
Distinct from the underlying status error so the exhaustion — not whichever
|
|
69
|
+
blip happened to be last — is what a caller sees and classifies, and so the
|
|
70
|
+
count and the window it covered can be named in the message.
|
|
80
71
|
"""
|
|
81
|
-
try:
|
|
82
|
-
body = response.json()
|
|
83
|
-
except Exception:
|
|
84
|
-
return False
|
|
85
|
-
return isinstance(body, dict) and body.get("retryable") is True
|
|
86
72
|
|
|
87
73
|
|
|
88
74
|
def _is_retryable_export_error(exc: BaseException) -> bool:
|
|
89
75
|
"""Whether a failed data send is a transient delivery blip worth retrying.
|
|
90
76
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
77
|
+
A failed *response* is classified by the platform's retryable-error contract
|
|
78
|
+
(``cmpnd.retry``): the body's ``retryable: true`` first, then the
|
|
79
|
+
gateway/overload statuses. So 4xx (client error, a retry won't help) and a
|
|
80
|
+
plain 500 (the platform's to answer for, mirroring the is_external_outage
|
|
81
|
+
boundary) fail fast, while a transient condition the server signals on a
|
|
82
|
+
status this client has no opinion about is honoured.
|
|
83
|
+
|
|
84
|
+
**429 is the one status that outranks the flag**, which is why it is checked
|
|
85
|
+
before the contract rather than left to it. It is backpressure — the server's
|
|
86
|
+
buffer is full and it wants the record shed, which the caller records as
|
|
87
|
+
*dropped* rather than errored — so re-sending would amplify the very overload
|
|
88
|
+
the status exists to relieve. A general classifier cannot make that call: it
|
|
89
|
+
is a property of this path, where a drop is a legitimate outcome. The
|
|
90
|
+
contract stays free of the exemption so a poll, which has no drop to fall
|
|
91
|
+
back on, is not forced into it.
|
|
92
|
+
|
|
93
|
+
An httpx transport/timeout error (connect/read/pool timeout, connection
|
|
94
|
+
refused/reset) is transient too — there is no answer to read, and the send
|
|
95
|
+
is safe to repeat because serve dedups on ingest (traces by
|
|
96
|
+
(org_id, trace_id, start_time), spans by the spans_dedup index), so a
|
|
97
|
+
re-POST of a request that actually landed can't double-insert. Non-httpx
|
|
98
|
+
exceptions are not retried (a bug in our own build path shouldn't loop)."""
|
|
99
99
|
if isinstance(exc, httpx.HTTPStatusError):
|
|
100
|
-
|
|
101
|
-
if status == _DROP_STATUS:
|
|
100
|
+
if exc.response.status_code == _SHED_STATUS:
|
|
102
101
|
return False
|
|
103
|
-
return
|
|
102
|
+
return retry.is_retryable(exc.response)
|
|
104
103
|
return isinstance(exc, (httpx.TimeoutException, httpx.TransportError))
|
|
105
104
|
|
|
106
105
|
|
|
107
|
-
def
|
|
108
|
-
"""The
|
|
106
|
+
def _may_resend_a_minting_post(exc: BaseException) -> bool:
|
|
107
|
+
"""The same question as above, asked of a POST the server mints a record for.
|
|
108
|
+
|
|
109
|
+
The data path may re-send almost anything because serve dedups on ingest, so
|
|
110
|
+
a repeat of a request that actually landed collapses into the row already
|
|
111
|
+
there. A start POST has no such safety net — it carries the "new" sentinel
|
|
112
|
+
and the server allocates the id — so a re-send that the first attempt
|
|
113
|
+
already completed produces a *second* run, and the caller only ever learns
|
|
114
|
+
about one of them.
|
|
109
115
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
That narrows what earns a retry to the two answers which say the first
|
|
117
|
+
attempt cannot have landed: the server's own ``retryable``, emitted having
|
|
118
|
+
already failed the work, and a connection that was never established. A
|
|
119
|
+
read timeout or a bare 502/504 is excluded precisely because it looks
|
|
120
|
+
identical whether the server did the work or not.
|
|
115
121
|
"""
|
|
116
|
-
if
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
if not raw:
|
|
120
|
-
return None
|
|
121
|
-
try:
|
|
122
|
-
return max(0.0, float(raw))
|
|
123
|
-
except ValueError:
|
|
124
|
-
return None
|
|
122
|
+
if isinstance(exc, httpx.HTTPStatusError):
|
|
123
|
+
return retry.signalled_retryable(exc.response)
|
|
124
|
+
return retry.never_left_the_client(exc)
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
@dataclass
|
|
@@ -179,24 +179,6 @@ class BatchExporter:
|
|
|
179
179
|
# never open a socket. httpx.Client is safe to share across threads.
|
|
180
180
|
self._sync_client: httpx.Client | None = None
|
|
181
181
|
self._sync_client_lock = threading.Lock()
|
|
182
|
-
# Small courtesy-retry budget for a failed start POST before raising
|
|
183
|
-
# (fail-loud, no local-id fallback). Instance attrs so tests can tune.
|
|
184
|
-
self._start_post_attempts = 3
|
|
185
|
-
self._start_post_backoff = 0.5
|
|
186
|
-
|
|
187
|
-
# Bounded retry for the best-effort data POSTs (traces/spans/stats/…):
|
|
188
|
-
# a transient gateway/overload or transport blip retries with backoff
|
|
189
|
-
# instead of immediately counting a hard export error. Idempotent on the
|
|
190
|
-
# serve side (see _is_retryable_export_error). Instance attrs so tests
|
|
191
|
-
# can zero the backoff. Distinct from the start-post budget above.
|
|
192
|
-
self._data_post_attempts = 3
|
|
193
|
-
self._data_post_backoff = 0.5
|
|
194
|
-
# Ceiling on a single inter-attempt wait, including one the server asked
|
|
195
|
-
# for via Retry-After. The exporter thread is the only thing draining the
|
|
196
|
-
# queue, so an over-long wait doesn't just delay this send — it stalls
|
|
197
|
-
# every queued record behind it and risks the queue filling. Honoring the
|
|
198
|
-
# server's hint is worth it; honoring an unbounded one is not.
|
|
199
|
-
self._data_post_max_delay = 5.0
|
|
200
182
|
|
|
201
183
|
# Guard against registering multiple exit handlers on repeated start()
|
|
202
184
|
self._atexit_registered = False
|
|
@@ -209,6 +191,13 @@ class BatchExporter:
|
|
|
209
191
|
self._exported_count = 0
|
|
210
192
|
self._dropped_count = 0
|
|
211
193
|
self._error_count = 0
|
|
194
|
+
# What the wire actually answered, so a failed export is diagnosable
|
|
195
|
+
# past a bare error count: the distribution of non-2xx statuses seen, a
|
|
196
|
+
# sample of the last error body, and how many transient answers the
|
|
197
|
+
# retry budget rode through to keep the run alive.
|
|
198
|
+
self._observed_statuses: dict[int, int] = {}
|
|
199
|
+
self._last_error_body = ""
|
|
200
|
+
self._transient_ridden = 0
|
|
212
201
|
self._stats_lock = threading.Lock()
|
|
213
202
|
|
|
214
203
|
def start(self) -> None:
|
|
@@ -377,24 +366,53 @@ class BatchExporter:
|
|
|
377
366
|
def _post_start_sync(self, path: str, payload: dict[str, Any], *, id_field: str) -> str:
|
|
378
367
|
"""POST a start record synchronously and return the server-assigned id.
|
|
379
368
|
|
|
380
|
-
|
|
381
|
-
|
|
369
|
+
**The server mints the id here** — the payload carries the "new"
|
|
370
|
+
sentinel — so this POST is not idempotent and what it may re-send is
|
|
371
|
+
narrower than the data path's. Two answers earn a retry, and only two:
|
|
372
|
+
the server's own ``retryable``, which is it saying the write did not
|
|
373
|
+
land, and a connection that was never established, which carried
|
|
374
|
+
nothing to it. A bare 502/504 does not: that comes from a hop with no
|
|
375
|
+
idea whether the server processed the request, and re-sending on the
|
|
376
|
+
guess is how one run start becomes two runs.
|
|
377
|
+
|
|
378
|
+
Within that narrowing it rides the full budget, because the window this
|
|
379
|
+
exists to survive outlasts a couple of courtesy attempts. There is no
|
|
380
|
+
local-id fallback by design: a run that cannot be started fails loud.
|
|
382
381
|
"""
|
|
383
382
|
client = self._sync_http_client()
|
|
384
|
-
|
|
385
|
-
|
|
383
|
+
tally = retry.TransientTally()
|
|
384
|
+
while True:
|
|
386
385
|
try:
|
|
387
386
|
response = _send_json(client, "POST", path, payload)
|
|
388
387
|
response.raise_for_status()
|
|
389
388
|
assigned = response.json().get(id_field)
|
|
390
389
|
if not assigned:
|
|
391
390
|
raise RuntimeError(f"{path} response missing {id_field}: {response.text}")
|
|
391
|
+
if tally.count:
|
|
392
|
+
self._note_ridden_through(tally.count)
|
|
393
|
+
logger.warning(
|
|
394
|
+
"cmpnd: rode through %s to start a run via %s", tally.describe(time.monotonic()), path
|
|
395
|
+
)
|
|
392
396
|
return str(assigned)
|
|
393
397
|
except Exception as e:
|
|
394
|
-
|
|
395
|
-
if
|
|
396
|
-
|
|
397
|
-
|
|
398
|
+
resp = e.response if isinstance(e, httpx.HTTPStatusError) else None
|
|
399
|
+
if resp is not None:
|
|
400
|
+
self._note_status(resp)
|
|
401
|
+
if not _may_resend_a_minting_post(e):
|
|
402
|
+
# RuntimeError whatever the cause, because that is this
|
|
403
|
+
# method's documented failure: a run start either yields an
|
|
404
|
+
# id or fails loud, and a caller should not have to know
|
|
405
|
+
# which layer said no.
|
|
406
|
+
raise RuntimeError(f"failed to start run via {path}: {e}") from e
|
|
407
|
+
tally.record(
|
|
408
|
+
str(resp.status_code) if resp is not None else type(e).__name__,
|
|
409
|
+
at=time.monotonic(),
|
|
410
|
+
sample=str(e),
|
|
411
|
+
)
|
|
412
|
+
now = time.monotonic()
|
|
413
|
+
if tally.exhausted(now) or not self._wait_before_retry(retry.delay_for(tally.count - 1, resp)):
|
|
414
|
+
self._note_ridden_through(tally.count)
|
|
415
|
+
raise RuntimeError(f"failed to start run via {path}: gave up after {tally.describe(now)}") from e
|
|
398
416
|
|
|
399
417
|
def export_optimization_progress(self, optimization_run: tracker.OptimizationTracker) -> None:
|
|
400
418
|
"""Queue an optimization progress update for export (non-blocking).
|
|
@@ -523,45 +541,88 @@ class BatchExporter:
|
|
|
523
541
|
raise RuntimeError("BackgroundExporter has no HTTP client (not started or already shut down)")
|
|
524
542
|
return self._client
|
|
525
543
|
|
|
544
|
+
def _wait_before_retry(self, seconds: float) -> bool:
|
|
545
|
+
"""Wait out a backoff, interruptibly. False means "stop retrying".
|
|
546
|
+
|
|
547
|
+
The wait runs on the exporter's own threads, so a plain sleep would hold
|
|
548
|
+
the process open for the length of the budget at exit. Waiting on the
|
|
549
|
+
shutdown event instead ends it the moment shutdown is signalled — and
|
|
550
|
+
means the drain that runs *after* shutdown gets no retries at all, which
|
|
551
|
+
is the right trade: it must not delay the exit it is part of.
|
|
552
|
+
"""
|
|
553
|
+
if seconds <= 0:
|
|
554
|
+
return not self._shutdown.is_set()
|
|
555
|
+
return not self._shutdown.wait(seconds)
|
|
556
|
+
|
|
526
557
|
def _send_json_retrying(self, client: httpx.Client, method: str, path: str, payload: Any) -> httpx.Response:
|
|
527
|
-
"""`_send_json` + `raise_for_status`,
|
|
528
|
-
|
|
529
|
-
Retries
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
558
|
+
"""`_send_json` + `raise_for_status`, riding through transient blips.
|
|
559
|
+
|
|
560
|
+
Retries a failure the retryable-error contract calls transient (see
|
|
561
|
+
`_is_retryable_export_error`) on `cmpnd.retry`'s budget: jittered
|
|
562
|
+
exponential backoff, floored by any `Retry-After` the server sent and
|
|
563
|
+
capped so no hop can park the exporter, spanning a window sized on the
|
|
564
|
+
longest transient outage we've measured. A non-retryable failure (4xx,
|
|
565
|
+
429, a plain 500, or a 2xx that somehow raised) propagates on the first
|
|
566
|
+
attempt, so the caller's except blocks record it (429 → dropped,
|
|
567
|
+
everything else → error) with no budget burned.
|
|
568
|
+
|
|
569
|
+
An exhausted budget raises ExportRetriesExhausted naming the count and
|
|
570
|
+
the window, so a run that could not be saved says what it endured.
|
|
571
|
+
"""
|
|
572
|
+
tally = retry.TransientTally()
|
|
573
|
+
while True:
|
|
541
574
|
try:
|
|
542
575
|
response = _send_json(client, method, path, payload)
|
|
543
576
|
response.raise_for_status()
|
|
577
|
+
if tally.count:
|
|
578
|
+
self._note_ridden_through(tally.count)
|
|
579
|
+
logger.warning("cmpnd: rode through %s to %s %s", tally.describe(time.monotonic()), method, path)
|
|
544
580
|
return response
|
|
545
581
|
except Exception as e:
|
|
546
|
-
|
|
547
|
-
if
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
)
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
582
|
+
resp = e.response if isinstance(e, httpx.HTTPStatusError) else None
|
|
583
|
+
if resp is not None:
|
|
584
|
+
self._note_status(resp)
|
|
585
|
+
if not _is_retryable_export_error(e):
|
|
586
|
+
raise
|
|
587
|
+
tally.record(
|
|
588
|
+
str(resp.status_code) if resp is not None else type(e).__name__,
|
|
589
|
+
at=time.monotonic(),
|
|
590
|
+
sample=str(e),
|
|
591
|
+
)
|
|
592
|
+
now = time.monotonic()
|
|
593
|
+
if tally.exhausted(now) or not self._wait_before_retry(retry.delay_for(tally.count - 1, resp)):
|
|
594
|
+
self._note_ridden_through(tally.count)
|
|
595
|
+
raise ExportRetriesExhausted(f"{method} {path} gave up after {tally.describe(now)}: {e}") from e
|
|
596
|
+
|
|
597
|
+
def _note_status(self, response: httpx.Response) -> None:
|
|
598
|
+
"""Record a non-2xx answer for the end-of-run diagnosis."""
|
|
599
|
+
with self._stats_lock:
|
|
600
|
+
self._observed_statuses[response.status_code] = self._observed_statuses.get(response.status_code, 0) + 1
|
|
601
|
+
body = (response.text or "").strip()
|
|
602
|
+
if body:
|
|
603
|
+
self._last_error_body = body[:400]
|
|
604
|
+
|
|
605
|
+
def _note_ridden_through(self, count: int) -> None:
|
|
606
|
+
with self._stats_lock:
|
|
607
|
+
self._transient_ridden += count
|
|
608
|
+
|
|
609
|
+
def failure_summary(self) -> str:
|
|
610
|
+
"""What the wire answered, for a caller reporting a failed export.
|
|
611
|
+
|
|
612
|
+
An error count alone can't tell a saturated backend from a rejected
|
|
613
|
+
payload; the status distribution and one body can.
|
|
614
|
+
"""
|
|
615
|
+
with self._stats_lock:
|
|
616
|
+
statuses = dict(self._observed_statuses)
|
|
617
|
+
ridden = self._transient_ridden
|
|
618
|
+
body = self._last_error_body
|
|
619
|
+
if not statuses:
|
|
620
|
+
return "no non-2xx responses observed"
|
|
621
|
+
mix = ", ".join(f"{code}={n}" for code, n in sorted(statuses.items()))
|
|
622
|
+
summary = f"non-2xx observed: {mix}; transient answers ridden through: {ridden}"
|
|
623
|
+
if body:
|
|
624
|
+
summary += f"; sample body: {body}"
|
|
625
|
+
return summary
|
|
565
626
|
|
|
566
627
|
def _flush_batch(self, batch: list[ExportItem]) -> None:
|
|
567
628
|
"""Send a batch of items to the backend with parallel dispatch."""
|