cmpnd 0.25.0__tar.gz → 0.26.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cmpnd-0.25.0 → cmpnd-0.26.0}/PKG-INFO +6 -1
- {cmpnd-0.25.0 → cmpnd-0.26.0}/README.md +5 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/__init__.py +4 -2
- cmpnd-0.26.0/cmpnd/_authored_import.py +201 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/_http.py +38 -4
- cmpnd-0.26.0/cmpnd/authored.py +328 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/top.py +66 -54
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/shell.py +38 -56
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/deployment.py +69 -90
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/execution.py +5 -15
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/mcp_servers.py +118 -28
- {cmpnd-0.25.0 → cmpnd-0.26.0}/docs/cli.md +34 -26
- {cmpnd-0.25.0 → cmpnd-0.26.0}/docs/sdk-instrumentation.md +24 -15
- {cmpnd-0.25.0 → cmpnd-0.26.0}/pyproject.toml +1 -1
- cmpnd-0.26.0/tests/integration/hosted_mcp_tool_optimize_probe.py +321 -0
- cmpnd-0.26.0/tests/test_authored.py +231 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_api_client.py +42 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_main.py +17 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_schema_sample.py +13 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_shell.py +35 -4
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_top.py +80 -78
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_deploy.py +44 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_deploy_mcp_tool_references.py +14 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_mcp_tools.py +127 -1
- {cmpnd-0.25.0 → cmpnd-0.26.0}/uv.lock +31 -31
- {cmpnd-0.25.0 → cmpnd-0.26.0}/.gitignore +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/AGENTS.md +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/CLAUDE.md +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/_assessments.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/_exporter_pool.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/_traces.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/_valsets.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/callback.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/assessments.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/snippet.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/tree_help.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/client.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/configuration.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/context.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/datasets.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/decorators.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/exporter.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/helpers.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/identity.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/imaging.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/models.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/optimization.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/packaging.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/retry.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/cmpnd/traces.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/docs/client.md +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/docs/custom-proposer.md +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/docs/reading-traces.md +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/examples/local_ollama.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/ingest/README.md +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/ingest/pyproject.toml +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/_fake_clock.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/conftest.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_assessments_cli.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_traces_read.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/e2e/test_valsets_cli.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/integration/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/integration/conftest.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/load/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/load/__main__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/load/drain.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/load/ingest.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/load/sweep.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/conftest.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/test_health.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/_provider.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/_result.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/stress/test_trace_search.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_assessments_api.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_callback.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_assessments.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_signatures.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_tree_help.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cli/test_valsets.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_client.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_configuration.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_context.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_datasets.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_decorators.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_eval.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_eval_start.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_execute.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_exporter.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_identity.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_imaging.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_import_weight.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_integration.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_lm_usage_shapes.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_models.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_optimize.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_packaging.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_poll_until.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_project_payload.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_retry.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_schemas.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_trace_render.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_traces_read.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_truncation.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_valsets_api.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/tests/test_xxh64.py +0 -0
- {cmpnd-0.25.0 → cmpnd-0.26.0}/todo.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cmpnd
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.0
|
|
4
4
|
Summary: DSPy observability and deployment SDK for cmpnd
|
|
5
5
|
Project-URL: Homepage, https://cmpnd.ai
|
|
6
6
|
Author-email: cmpnd <hello@cmpnd.ai>
|
|
@@ -137,6 +137,11 @@ print(result.answer)
|
|
|
137
137
|
|
|
138
138
|
The SDK packages the module's source code into a ZIP, uploads it to the platform, and polls until the server-side parser finishes. Returns a `DeployedProgram` you can **call directly** to execute server-side; `str(deployed)` is its `deployment_id` and `deployed.deployment_id` exposes it explicitly.
|
|
139
139
|
|
|
140
|
+
For a source file, use `cmpnd deploy program.py`. The CLI uploads the file's
|
|
141
|
+
exact bytes instead of reconstructing it from a live object. Use
|
|
142
|
+
`--source-root` and repeated `--include` flags for sibling modules or data, and
|
|
143
|
+
`cmpnd inspect <id> --source` to read the deployed entry file back.
|
|
144
|
+
|
|
140
145
|
### With a metric (for optimization)
|
|
141
146
|
|
|
142
147
|
```python
|
|
@@ -89,6 +89,11 @@ print(result.answer)
|
|
|
89
89
|
|
|
90
90
|
The SDK packages the module's source code into a ZIP, uploads it to the platform, and polls until the server-side parser finishes. Returns a `DeployedProgram` you can **call directly** to execute server-side; `str(deployed)` is its `deployment_id` and `deployed.deployment_id` exposes it explicitly.
|
|
91
91
|
|
|
92
|
+
For a source file, use `cmpnd deploy program.py`. The CLI uploads the file's
|
|
93
|
+
exact bytes instead of reconstructing it from a live object. Use
|
|
94
|
+
`--source-root` and repeated `--include` flags for sibling modules or data, and
|
|
95
|
+
`cmpnd inspect <id> --source` to read the deployed entry file back.
|
|
96
|
+
|
|
92
97
|
### With a metric (for optimization)
|
|
93
98
|
|
|
94
99
|
```python
|
|
@@ -14,7 +14,7 @@ Usage:
|
|
|
14
14
|
|
|
15
15
|
import importlib.metadata
|
|
16
16
|
|
|
17
|
-
from . import traces
|
|
17
|
+
from . import authored, traces
|
|
18
18
|
from ._http import ApiError
|
|
19
19
|
from .callback import CmpndCallback, trace_id
|
|
20
20
|
from .client import Client
|
|
@@ -26,7 +26,7 @@ from .context import (
|
|
|
26
26
|
)
|
|
27
27
|
from .datasets import DatasetNotFoundError, VersionNotFoundError, load_dataset
|
|
28
28
|
from .decorators import start_span, trace
|
|
29
|
-
from .deployment import DeployFailed, deploy
|
|
29
|
+
from .deployment import DeployFailed, deploy, deploy_bundle
|
|
30
30
|
from .execution import DeployedProgram, execute
|
|
31
31
|
from .exporter import flush_exporter, shutdown_exporter
|
|
32
32
|
from .mcp_servers import (
|
|
@@ -80,6 +80,7 @@ __all__ = [
|
|
|
80
80
|
"shutdown_exporter",
|
|
81
81
|
# Programmatic backend reads (cmpnd.traces.list / cmpnd.traces.show)
|
|
82
82
|
"traces",
|
|
83
|
+
"authored",
|
|
83
84
|
"ApiError",
|
|
84
85
|
# Types
|
|
85
86
|
"SpanType",
|
|
@@ -88,6 +89,7 @@ __all__ = [
|
|
|
88
89
|
"TraceBuilder",
|
|
89
90
|
# Deploy & execute
|
|
90
91
|
"deploy",
|
|
92
|
+
"deploy_bundle",
|
|
91
93
|
"DeployFailed",
|
|
92
94
|
"execute",
|
|
93
95
|
"DeployedProgram",
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Isolated import checker for authored deployment bundles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import importlib
|
|
7
|
+
import inspect
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import traceback
|
|
12
|
+
import zipfile
|
|
13
|
+
from dataclasses import asdict
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _program(module: Any, entrypoint: str) -> tuple[Any, str]:
|
|
19
|
+
import dspy
|
|
20
|
+
|
|
21
|
+
if not hasattr(module, entrypoint):
|
|
22
|
+
raise ValueError(f"entrypoint {entrypoint!r} is not defined by the authored program")
|
|
23
|
+
value = getattr(module, entrypoint)
|
|
24
|
+
if isinstance(value, dspy.Module):
|
|
25
|
+
return value, "instance"
|
|
26
|
+
if inspect.isclass(value) and issubclass(value, dspy.Module):
|
|
27
|
+
_require_zero_arguments(value, f"entrypoint class {entrypoint!r}")
|
|
28
|
+
return value(), "class"
|
|
29
|
+
if callable(value):
|
|
30
|
+
_require_zero_arguments(value, f"entrypoint factory {entrypoint!r}")
|
|
31
|
+
built = value()
|
|
32
|
+
if isinstance(built, dspy.Module):
|
|
33
|
+
return built, "factory"
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"entrypoint {entrypoint!r} must be a dspy.Module instance, a dspy.Module class, or a zero-argument factory"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _require_zero_arguments(value: Any, label: str) -> None:
|
|
40
|
+
"""Refuse required parameters without mislabelling errors inside the call."""
|
|
41
|
+
try:
|
|
42
|
+
signature = inspect.signature(value)
|
|
43
|
+
except (TypeError, ValueError):
|
|
44
|
+
return
|
|
45
|
+
try:
|
|
46
|
+
signature.bind()
|
|
47
|
+
except TypeError as exc:
|
|
48
|
+
raise ValueError(f"{label} must accept no arguments: {exc}") from exc
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _check(request: dict[str, Any]) -> dict[str, Any]:
|
|
52
|
+
from . import configuration, deployment, identity, mcp_servers, packaging
|
|
53
|
+
from .authored import ProgramFacts
|
|
54
|
+
|
|
55
|
+
original_configure = configuration.configure
|
|
56
|
+
authored_limits: dict[str, int | None] = {"max_input_size": None, "max_output_size": None}
|
|
57
|
+
if request.get("api_key"):
|
|
58
|
+
original_configure(api_key=request["api_key"], endpoint=request.get("endpoint"))
|
|
59
|
+
|
|
60
|
+
def source_configure(api_key=None, endpoint=None, *args, **kwargs):
|
|
61
|
+
for name in authored_limits:
|
|
62
|
+
value = kwargs.get(name)
|
|
63
|
+
authored_limits[name] = value if isinstance(value, int) else None
|
|
64
|
+
return original_configure(
|
|
65
|
+
api_key=api_key or request["api_key"],
|
|
66
|
+
endpoint=endpoint or request.get("endpoint"),
|
|
67
|
+
*args,
|
|
68
|
+
**kwargs,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# The check has its own platform configuration but exposes no secret in
|
|
72
|
+
# the child environment. Let authored ``cmpnd.configure(max_...)`` keep
|
|
73
|
+
# that transport while it changes the settings the program owns.
|
|
74
|
+
configuration.configure = source_configure
|
|
75
|
+
setattr(sys.modules["cmpnd"], "configure", source_configure)
|
|
76
|
+
with tempfile.TemporaryDirectory(prefix="cmpnd-authored-") as directory:
|
|
77
|
+
root = Path(directory)
|
|
78
|
+
with zipfile.ZipFile(request["bundle"]) as archive:
|
|
79
|
+
archive.extractall(root)
|
|
80
|
+
manifest = json.loads((root / "manifest.json").read_text())
|
|
81
|
+
module_name = str(manifest["program"]).removesuffix(".py").replace("/", ".")
|
|
82
|
+
sys.path.insert(0, str(root))
|
|
83
|
+
try:
|
|
84
|
+
module = importlib.import_module(module_name)
|
|
85
|
+
finally:
|
|
86
|
+
sys.path.remove(str(root))
|
|
87
|
+
program, kind = _program(module, str(manifest["entrypoint"]))
|
|
88
|
+
|
|
89
|
+
references = deployment._mcp_tool_references(program)
|
|
90
|
+
deployment._refuse_mcp_tools_outside_rlm(program, references)
|
|
91
|
+
pinned: dict[tuple[str, str], Any] = {}
|
|
92
|
+
if references:
|
|
93
|
+
import httpx
|
|
94
|
+
|
|
95
|
+
config = configuration.require_config()
|
|
96
|
+
with httpx.Client(timeout=30) as client:
|
|
97
|
+
pinned = deployment._resolve_mcp_tool_references(client, config, references)
|
|
98
|
+
tool_references = [
|
|
99
|
+
{
|
|
100
|
+
# The child already checked an explicit credential name against the
|
|
101
|
+
# registered server binding. Serve resolves that binding again and
|
|
102
|
+
# never accepts a client-selected credential name in this declaration.
|
|
103
|
+
"server_url": item.server_url,
|
|
104
|
+
"tool": item.tool_name,
|
|
105
|
+
"schema_hash": item.schema_hash,
|
|
106
|
+
"parameters": mcp_servers.synthesized_signature(item).parameter_names,
|
|
107
|
+
}
|
|
108
|
+
for item in pinned.values()
|
|
109
|
+
]
|
|
110
|
+
tool_declarations = [
|
|
111
|
+
{
|
|
112
|
+
"server_url": item.server_url,
|
|
113
|
+
"name": item.tool_name,
|
|
114
|
+
"schema_hash": item.schema_hash,
|
|
115
|
+
"input_schema": item.input_schema,
|
|
116
|
+
"description": item.description,
|
|
117
|
+
"parameters": mcp_servers.synthesized_signature(item).parameter_names,
|
|
118
|
+
}
|
|
119
|
+
for item in pinned.values()
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
signature = packaging._get_module_signature(program)
|
|
123
|
+
fields: dict[str, Any] = {}
|
|
124
|
+
if signature is not None:
|
|
125
|
+
fields = {
|
|
126
|
+
"signature_name": packaging._signature_to_string(signature),
|
|
127
|
+
"input_fields": list(signature.input_fields),
|
|
128
|
+
"output_fields": list(signature.output_fields),
|
|
129
|
+
"input_field_types": [identity.get_type_string(f.annotation) for f in signature.input_fields.values()],
|
|
130
|
+
"output_field_types": [
|
|
131
|
+
identity.get_type_string(f.annotation) for f in signature.output_fields.values()
|
|
132
|
+
],
|
|
133
|
+
}
|
|
134
|
+
import dspy
|
|
135
|
+
|
|
136
|
+
lm = dspy.settings.lm
|
|
137
|
+
lm_kwargs = getattr(lm, "kwargs", {}) if lm is not None else {}
|
|
138
|
+
facts = ProgramFacts(
|
|
139
|
+
program_signature_hash=identity.compute_program_hash(program),
|
|
140
|
+
entrypoint_kind=kind,
|
|
141
|
+
module_class=type(program).__name__,
|
|
142
|
+
litellm_model=getattr(lm, "model", None),
|
|
143
|
+
temperature=lm_kwargs.get("temperature"),
|
|
144
|
+
max_tokens=lm_kwargs.get("max_tokens"),
|
|
145
|
+
thinking_effort=lm_kwargs.get("thinking_effort"),
|
|
146
|
+
max_input_size=authored_limits["max_input_size"],
|
|
147
|
+
max_output_size=authored_limits["max_output_size"],
|
|
148
|
+
tool_references=tool_references,
|
|
149
|
+
tool_declarations=tool_declarations,
|
|
150
|
+
**fields,
|
|
151
|
+
)
|
|
152
|
+
return asdict(facts)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main() -> int:
|
|
156
|
+
try:
|
|
157
|
+
request = json.loads(sys.stdin.read())
|
|
158
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
159
|
+
facts = _check(request)
|
|
160
|
+
print(json.dumps({"facts": facts}))
|
|
161
|
+
return 0
|
|
162
|
+
except ModuleNotFoundError as exc:
|
|
163
|
+
print(
|
|
164
|
+
json.dumps(
|
|
165
|
+
{
|
|
166
|
+
"error": (
|
|
167
|
+
f"the authored program imports {exc.name!r}, which the local import check cannot find. "
|
|
168
|
+
"If it is a sibling module, add its file or directory with --include; otherwise install "
|
|
169
|
+
"the dependency locally and ensure the worker supports it."
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
except KeyError as exc:
|
|
175
|
+
print(
|
|
176
|
+
json.dumps(
|
|
177
|
+
{
|
|
178
|
+
"error": (
|
|
179
|
+
f"authored source import failed with KeyError: {exc}. The check runs without your "
|
|
180
|
+
"environment because the worker has none. Do not read provider or MCP credentials at "
|
|
181
|
+
"module scope. Name the model without its key and use cmpnd.mcp_tools(url) for a "
|
|
182
|
+
"registered server.\n" + _concise_traceback()
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
except BaseException as exc:
|
|
188
|
+
print(
|
|
189
|
+
json.dumps(
|
|
190
|
+
{"error": (f"authored source import failed with {type(exc).__name__}: {exc}\n" + _concise_traceback())}
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
return 1
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _concise_traceback() -> str:
|
|
197
|
+
return "".join(traceback.format_exc(limit=6)).strip()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
raise SystemExit(main())
|
|
@@ -30,6 +30,27 @@ class ApiError(Exception):
|
|
|
30
30
|
self.status_code = status_code
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def server_detail(response: httpx.Response) -> str:
|
|
34
|
+
"""The server's own message for a failed request, for the error text.
|
|
35
|
+
|
|
36
|
+
Both shapes the platform emits: ``{"error": {"message": …}}`` (a worker's
|
|
37
|
+
refusal, passed through by serve) and ``{"error": "…"}`` (serve's own
|
|
38
|
+
``writeError``). Falls back to the raw text, trimmed, because an unparseable
|
|
39
|
+
body still beats no body — and returns "" rather than guessing when there is
|
|
40
|
+
nothing, so the caller can supply its own wording.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
data = response.json()
|
|
44
|
+
except ValueError:
|
|
45
|
+
return (response.text or "").strip()[:2000]
|
|
46
|
+
error = data.get("error") if isinstance(data, dict) else None
|
|
47
|
+
if isinstance(error, dict):
|
|
48
|
+
return str(error.get("message") or error).strip()[:2000]
|
|
49
|
+
if error:
|
|
50
|
+
return str(error).strip()[:2000]
|
|
51
|
+
return (response.text or "").strip()[:2000]
|
|
52
|
+
|
|
53
|
+
|
|
33
54
|
class ApiClient:
|
|
34
55
|
"""Thin HTTP client for the cmpnd backend API."""
|
|
35
56
|
|
|
@@ -41,7 +62,7 @@ class ApiClient:
|
|
|
41
62
|
return self._request("GET", path, params=params)
|
|
42
63
|
|
|
43
64
|
def get_text(self, path: str, params: dict[str, str] | None = None) -> str:
|
|
44
|
-
"""GET a route that does not answer JSON, returning
|
|
65
|
+
"""GET a route that does not answer JSON, returning its decoded text.
|
|
45
66
|
|
|
46
67
|
Most of this API is JSON and `get` is right for it; a handful of routes
|
|
47
68
|
serve another media type (a deployment's OpenAPI document is YAML), and
|
|
@@ -52,6 +73,10 @@ class ApiClient:
|
|
|
52
73
|
"""
|
|
53
74
|
return self._request("GET", path, params=params, decode=False)
|
|
54
75
|
|
|
76
|
+
def get_bytes(self, path: str, params: dict[str, str] | None = None) -> bytes:
|
|
77
|
+
"""GET a route whose response bytes must survive unchanged."""
|
|
78
|
+
return self._request("GET", path, params=params, decode=None)
|
|
79
|
+
|
|
55
80
|
def post(self, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
56
81
|
return self._request("POST", path, json=json)
|
|
57
82
|
|
|
@@ -73,7 +98,7 @@ class ApiClient:
|
|
|
73
98
|
path: str,
|
|
74
99
|
params: dict[str, str] | None = None,
|
|
75
100
|
json: dict[str, Any] | None = None,
|
|
76
|
-
decode: bool = True,
|
|
101
|
+
decode: bool | None = True,
|
|
77
102
|
) -> Any:
|
|
78
103
|
url = f"{self._endpoint}{path}"
|
|
79
104
|
# 180s default: deploy/optimize/run wait in-band on an LM round-trip
|
|
@@ -111,7 +136,12 @@ class ApiClient:
|
|
|
111
136
|
if response.status_code == 401:
|
|
112
137
|
raise ApiError("Invalid or missing API key. Check CMPND_API_KEY or --api-key.", status_code=401)
|
|
113
138
|
if response.status_code == 404:
|
|
114
|
-
|
|
139
|
+
# The server's own words, not a generic stand-in. Every route here 404s
|
|
140
|
+
# for a reason it already states — "signature not found", "deployment
|
|
141
|
+
# not found", "no such organization" — and flattening them into one
|
|
142
|
+
# sentence makes a missing record and a mistyped URL read identically,
|
|
143
|
+
# which is the difference a user needs most and had the least of.
|
|
144
|
+
raise ApiError(server_detail(response) or "Resource not found.", status_code=404)
|
|
115
145
|
if response.status_code >= 500:
|
|
116
146
|
# 5xx usually means the server is up but blew up handling the
|
|
117
147
|
# request — a database hiccup, an unhandled exception, etc.
|
|
@@ -131,7 +161,11 @@ class ApiClient:
|
|
|
131
161
|
detail = response.text
|
|
132
162
|
raise ApiError(f"Request failed ({response.status_code}): {detail}", status_code=response.status_code)
|
|
133
163
|
|
|
134
|
-
# A
|
|
164
|
+
# A bytes route hands the raw body back before httpx decodes it.
|
|
165
|
+
if decode is None:
|
|
166
|
+
return response.content
|
|
167
|
+
|
|
168
|
+
# A non-JSON text route hands its decoded body back. Checked before the
|
|
135
169
|
# empty-body arm so such a route answers "" rather than None, which is
|
|
136
170
|
# what lets a caller print it without a None guard.
|
|
137
171
|
if not decode:
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Build and check source bundles for ``cmpnd deploy <file>``.
|
|
2
|
+
|
|
3
|
+
The CLI copies authored files into a deterministic ZIP. It never imports the
|
|
4
|
+
program in the CLI process. A short-lived child imports the exact staged ZIP,
|
|
5
|
+
resolves its declared entrypoint, and returns the facts the deployment API needs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.metadata
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import os
|
|
14
|
+
import platform
|
|
15
|
+
import re
|
|
16
|
+
import stat
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import tempfile
|
|
20
|
+
import warnings
|
|
21
|
+
import zipfile
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from io import BytesIO
|
|
24
|
+
from pathlib import Path, PurePosixPath
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
_MAX_BUNDLE_BYTES = 50 * 1024 * 1024
|
|
28
|
+
_CHECK_TIMEOUT_SECONDS = 60.0
|
|
29
|
+
_CHECK_TIMEOUT_ENV = "CMPND_AUTHORED_IMPORT_TIMEOUT_SECONDS"
|
|
30
|
+
_WORKER_DSPY_BASELINE = (3, 3, 1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SourceCheckError(ValueError):
|
|
34
|
+
"""The isolated authored-source import check refused a bundle."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ProgramFacts:
|
|
39
|
+
"""Facts derived by importing the staged program without calling forward."""
|
|
40
|
+
|
|
41
|
+
program_signature_hash: int | None = None
|
|
42
|
+
entrypoint_kind: str = "instance"
|
|
43
|
+
module_class: str | None = None
|
|
44
|
+
signature_name: str | None = None
|
|
45
|
+
input_fields: list[str] = field(default_factory=list)
|
|
46
|
+
output_fields: list[str] = field(default_factory=list)
|
|
47
|
+
input_field_types: list[str] = field(default_factory=list)
|
|
48
|
+
output_field_types: list[str] = field(default_factory=list)
|
|
49
|
+
litellm_model: str | None = None
|
|
50
|
+
temperature: float | None = None
|
|
51
|
+
max_tokens: int | None = None
|
|
52
|
+
thinking_effort: str | None = None
|
|
53
|
+
max_input_size: int | None = None
|
|
54
|
+
max_output_size: int | None = None
|
|
55
|
+
tool_references: list[dict[str, Any]] = field(default_factory=list)
|
|
56
|
+
tool_declarations: list[dict[str, Any]] = field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def minimal(cls) -> ProgramFacts:
|
|
60
|
+
"""Return a small fixture-friendly value."""
|
|
61
|
+
return cls()
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_json(cls, value: dict[str, Any]) -> ProgramFacts:
|
|
65
|
+
return cls(**{name: value[name] for name in cls.__dataclass_fields__ if name in value})
|
|
66
|
+
|
|
67
|
+
def deploy_body(self) -> dict[str, Any]:
|
|
68
|
+
"""Return fields the deployment create route accepts."""
|
|
69
|
+
from . import identity
|
|
70
|
+
|
|
71
|
+
body: dict[str, Any] = {}
|
|
72
|
+
if self.program_signature_hash is not None:
|
|
73
|
+
body["program_signature_hash"] = identity.format_signature_hash(self.program_signature_hash)
|
|
74
|
+
for name in (
|
|
75
|
+
"module_class",
|
|
76
|
+
"signature_name",
|
|
77
|
+
"litellm_model",
|
|
78
|
+
"temperature",
|
|
79
|
+
"max_tokens",
|
|
80
|
+
"thinking_effort",
|
|
81
|
+
"max_input_size",
|
|
82
|
+
"max_output_size",
|
|
83
|
+
):
|
|
84
|
+
value = getattr(self, name)
|
|
85
|
+
if value is not None and value != "":
|
|
86
|
+
body[name] = value
|
|
87
|
+
if self.signature_name is not None:
|
|
88
|
+
for name in ("input_fields", "output_fields", "input_field_types", "output_field_types"):
|
|
89
|
+
body[name] = getattr(self, name)
|
|
90
|
+
if self.tool_references:
|
|
91
|
+
body["tool_references"] = self.tool_references
|
|
92
|
+
return body
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class PreparedSource:
|
|
97
|
+
bundle: bytes
|
|
98
|
+
facts: ProgramFacts
|
|
99
|
+
dependency_versions: dict[str, str]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def prepare_file(
|
|
103
|
+
source: str | Path,
|
|
104
|
+
*,
|
|
105
|
+
source_root: str | Path | None = None,
|
|
106
|
+
includes: list[str | Path] | None = None,
|
|
107
|
+
entrypoint: str = "program",
|
|
108
|
+
api_key: str | None = None,
|
|
109
|
+
endpoint: str | None = None,
|
|
110
|
+
) -> PreparedSource:
|
|
111
|
+
"""Stage one authored Python file and its explicit includes."""
|
|
112
|
+
source_path = Path(source).expanduser()
|
|
113
|
+
root = Path(source_root).expanduser() if source_root is not None else source_path.parent
|
|
114
|
+
root = _plain_directory(root, "source root")
|
|
115
|
+
source_path = _plain_file(source_path, "program")
|
|
116
|
+
members: dict[str, bytes] = {}
|
|
117
|
+
_add_file(members, root, source_path)
|
|
118
|
+
for included in includes or []:
|
|
119
|
+
_add_path(members, root, Path(included).expanduser())
|
|
120
|
+
program = _relative_name(root, source_path)
|
|
121
|
+
return _prepare(members, program, entrypoint, api_key=api_key, endpoint=endpoint)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def prepare_stdin(
|
|
125
|
+
source: bytes | str,
|
|
126
|
+
*,
|
|
127
|
+
entrypoint: str = "program",
|
|
128
|
+
api_key: str | None = None,
|
|
129
|
+
endpoint: str | None = None,
|
|
130
|
+
) -> PreparedSource:
|
|
131
|
+
"""Stage stdin as the single authored ``program.py`` member."""
|
|
132
|
+
raw = source.encode() if isinstance(source, str) else source
|
|
133
|
+
return _prepare({"program.py": raw}, "program.py", entrypoint, api_key=api_key, endpoint=endpoint)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _prepare(
|
|
137
|
+
members: dict[str, bytes],
|
|
138
|
+
program: str,
|
|
139
|
+
entrypoint: str,
|
|
140
|
+
*,
|
|
141
|
+
api_key: str | None,
|
|
142
|
+
endpoint: str | None,
|
|
143
|
+
) -> PreparedSource:
|
|
144
|
+
if not members.get(program, b"").strip():
|
|
145
|
+
raise ValueError("the authored program is empty")
|
|
146
|
+
if not entrypoint or not entrypoint.isidentifier():
|
|
147
|
+
raise ValueError("entrypoint must be a Python identifier")
|
|
148
|
+
versions = _dependency_versions()
|
|
149
|
+
local_dspy = _release_tuple(versions["dspy"])
|
|
150
|
+
if local_dspy is not None and local_dspy > _WORKER_DSPY_BASELINE:
|
|
151
|
+
worker_dspy = ".".join(str(part) for part in _WORKER_DSPY_BASELINE)
|
|
152
|
+
warnings.warn(
|
|
153
|
+
f"local DSPy {versions['dspy']} is newer than the worker's {worker_dspy} compatibility baseline. "
|
|
154
|
+
"The local import check can pass even when the worker rejects a newer DSPy spelling.",
|
|
155
|
+
RuntimeWarning,
|
|
156
|
+
stacklevel=2,
|
|
157
|
+
)
|
|
158
|
+
bundle_members = ["manifest.json", *sorted(members)]
|
|
159
|
+
manifest: dict[str, Any] = {
|
|
160
|
+
"program": program,
|
|
161
|
+
"entrypoint": entrypoint,
|
|
162
|
+
"members": bundle_members,
|
|
163
|
+
"authored": True,
|
|
164
|
+
"dependency_versions": versions,
|
|
165
|
+
}
|
|
166
|
+
bundle = _zip(manifest, members)
|
|
167
|
+
facts = _check_bundle(bundle, api_key=api_key, endpoint=endpoint)
|
|
168
|
+
if facts.tool_declarations:
|
|
169
|
+
# The child pinned these declarations while importing this exact source.
|
|
170
|
+
# Serve checks each schema hash against the registry again on create, so
|
|
171
|
+
# adding the declarations to the manifest does not need a second import.
|
|
172
|
+
manifest["tools"] = facts.tool_declarations
|
|
173
|
+
bundle = _zip(manifest, members)
|
|
174
|
+
return PreparedSource(bundle=bundle, facts=facts, dependency_versions=versions)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _dependency_versions() -> dict[str, str]:
|
|
178
|
+
def version(name: str) -> str:
|
|
179
|
+
try:
|
|
180
|
+
return importlib.metadata.version(name)
|
|
181
|
+
except importlib.metadata.PackageNotFoundError:
|
|
182
|
+
return "unknown"
|
|
183
|
+
|
|
184
|
+
return {"python": platform.python_version(), "dspy": version("dspy"), "cmpnd": version("cmpnd")}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _release_tuple(value: str) -> tuple[int, int, int] | None:
|
|
188
|
+
"""Return the numeric release prefix needed for the compatibility warning."""
|
|
189
|
+
matched = re.match(r"^(\d+)\.(\d+)\.(\d+)", value)
|
|
190
|
+
if matched is None:
|
|
191
|
+
return None
|
|
192
|
+
return (int(matched.group(1)), int(matched.group(2)), int(matched.group(3)))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _zip(manifest: dict[str, Any], members: dict[str, bytes]) -> bytes:
|
|
196
|
+
manifest_bytes = json.dumps(manifest, indent=2, sort_keys=True).encode() + b"\n"
|
|
197
|
+
uncompressed_size = len(manifest_bytes) + sum(len(contents) for contents in members.values())
|
|
198
|
+
if uncompressed_size > _MAX_BUNDLE_BYTES:
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"source bundle is {uncompressed_size} bytes uncompressed; the limit is {_MAX_BUNDLE_BYTES} bytes"
|
|
201
|
+
)
|
|
202
|
+
output = BytesIO()
|
|
203
|
+
with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
|
204
|
+
_write_member(archive, "manifest.json", manifest_bytes)
|
|
205
|
+
for name in sorted(members):
|
|
206
|
+
_write_member(archive, name, members[name])
|
|
207
|
+
bundle = output.getvalue()
|
|
208
|
+
if len(bundle) > _MAX_BUNDLE_BYTES:
|
|
209
|
+
raise ValueError(f"source bundle is {len(bundle)} bytes; the limit is {_MAX_BUNDLE_BYTES} bytes")
|
|
210
|
+
return bundle
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _write_member(archive: zipfile.ZipFile, name: str, contents: bytes) -> None:
|
|
214
|
+
info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
|
|
215
|
+
info.compress_type = zipfile.ZIP_DEFLATED
|
|
216
|
+
info.external_attr = (stat.S_IFREG | 0o644) << 16
|
|
217
|
+
archive.writestr(info, contents)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _plain_directory(path: Path, label: str) -> Path:
|
|
221
|
+
if path.is_symlink():
|
|
222
|
+
raise ValueError(f"{label} may not be a symbolic link: {path}")
|
|
223
|
+
resolved = path.resolve()
|
|
224
|
+
if not resolved.is_dir():
|
|
225
|
+
raise ValueError(f"{label} is not a directory: {path}")
|
|
226
|
+
return resolved
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _plain_file(path: Path, label: str) -> Path:
|
|
230
|
+
if path.is_symlink():
|
|
231
|
+
raise ValueError(f"{label} may not be a symbolic link: {path}")
|
|
232
|
+
resolved = path.resolve()
|
|
233
|
+
if not resolved.is_file():
|
|
234
|
+
raise ValueError(f"{label} is not a regular file: {path}")
|
|
235
|
+
return resolved
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _relative_name(root: Path, path: Path) -> str:
|
|
239
|
+
try:
|
|
240
|
+
relative = path.relative_to(root)
|
|
241
|
+
except ValueError as exc:
|
|
242
|
+
raise ValueError(f"{path} is outside source root {root}") from exc
|
|
243
|
+
name = PurePosixPath(*relative.parts).as_posix()
|
|
244
|
+
if name in ("", ".") or name == "manifest.json":
|
|
245
|
+
raise ValueError(f"{path} cannot become a source-bundle member")
|
|
246
|
+
return name
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _add_file(members: dict[str, bytes], root: Path, path: Path) -> None:
|
|
250
|
+
path = _plain_file(path, "included path")
|
|
251
|
+
name = _relative_name(root, path)
|
|
252
|
+
if name in members:
|
|
253
|
+
raise ValueError(f"source bundle member is included more than once: {name}")
|
|
254
|
+
members[name] = path.read_bytes()
|
|
255
|
+
source_size = sum(len(value) for value in members.values())
|
|
256
|
+
if source_size > _MAX_BUNDLE_BYTES:
|
|
257
|
+
raise ValueError(f"source members are {source_size} bytes; the limit is {_MAX_BUNDLE_BYTES} bytes")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _add_path(members: dict[str, bytes], root: Path, path: Path) -> None:
|
|
261
|
+
if path.is_symlink():
|
|
262
|
+
raise ValueError(f"included path may not be a symbolic link: {path}")
|
|
263
|
+
resolved = path.resolve()
|
|
264
|
+
_relative_name(root, resolved)
|
|
265
|
+
if resolved.is_file():
|
|
266
|
+
_add_file(members, root, resolved)
|
|
267
|
+
return
|
|
268
|
+
if not resolved.is_dir():
|
|
269
|
+
raise ValueError(f"included path is not a regular file or directory: {path}")
|
|
270
|
+
for directory, directory_names, file_names in os.walk(resolved, followlinks=False):
|
|
271
|
+
base = Path(directory)
|
|
272
|
+
for name in directory_names:
|
|
273
|
+
child = base / name
|
|
274
|
+
if child.is_symlink():
|
|
275
|
+
raise ValueError(f"included path may not contain a symbolic link: {child}")
|
|
276
|
+
for name in file_names:
|
|
277
|
+
_add_file(members, root, base / name)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _check_bundle(bundle: bytes, *, api_key: str | None, endpoint: str | None) -> ProgramFacts:
|
|
281
|
+
"""Import the exact ZIP in a fresh child with no user environment."""
|
|
282
|
+
timeout = _check_timeout()
|
|
283
|
+
with tempfile.TemporaryDirectory(prefix="cmpnd-source-check-") as directory:
|
|
284
|
+
bundle_path = Path(directory) / "source.zip"
|
|
285
|
+
bundle_path.write_bytes(bundle)
|
|
286
|
+
request = json.dumps({"bundle": str(bundle_path), "api_key": api_key, "endpoint": endpoint})
|
|
287
|
+
environment = {
|
|
288
|
+
key: value
|
|
289
|
+
for key, value in os.environ.items()
|
|
290
|
+
if key in {"PATH", "SYSTEMROOT", "TMPDIR", "TEMP", "TMP", "LANG", "LC_ALL"}
|
|
291
|
+
}
|
|
292
|
+
environment.update({"PYTHONNOUSERSITE": "1", "PYTHONDONTWRITEBYTECODE": "1"})
|
|
293
|
+
try:
|
|
294
|
+
checked = subprocess.run(
|
|
295
|
+
[sys.executable, "-m", "cmpnd._authored_import"],
|
|
296
|
+
input=request,
|
|
297
|
+
text=True,
|
|
298
|
+
capture_output=True,
|
|
299
|
+
env=environment,
|
|
300
|
+
timeout=timeout,
|
|
301
|
+
check=False,
|
|
302
|
+
)
|
|
303
|
+
except subprocess.TimeoutExpired as exc:
|
|
304
|
+
raise SourceCheckError(
|
|
305
|
+
f"the authored program did not finish importing within {timeout:g}s. "
|
|
306
|
+
f"Raise {_CHECK_TIMEOUT_ENV} if module-scope setup needs longer."
|
|
307
|
+
) from exc
|
|
308
|
+
try:
|
|
309
|
+
answer = json.loads(checked.stdout)
|
|
310
|
+
except json.JSONDecodeError as exc:
|
|
311
|
+
detail = checked.stderr.strip() or "the import checker returned no readable answer"
|
|
312
|
+
raise SourceCheckError(detail) from exc
|
|
313
|
+
if checked.returncode != 0 or answer.get("error"):
|
|
314
|
+
raise SourceCheckError(str(answer.get("error") or checked.stderr.strip()))
|
|
315
|
+
return ProgramFacts.from_json(answer["facts"])
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _check_timeout() -> float:
|
|
319
|
+
raw = os.environ.get(_CHECK_TIMEOUT_ENV)
|
|
320
|
+
if raw is None:
|
|
321
|
+
return _CHECK_TIMEOUT_SECONDS
|
|
322
|
+
try:
|
|
323
|
+
timeout = float(raw)
|
|
324
|
+
except ValueError as exc:
|
|
325
|
+
raise ValueError(f"{_CHECK_TIMEOUT_ENV} must be a positive number of seconds") from exc
|
|
326
|
+
if not math.isfinite(timeout) or timeout <= 0:
|
|
327
|
+
raise ValueError(f"{_CHECK_TIMEOUT_ENV} must be a positive number of seconds")
|
|
328
|
+
return timeout
|