cmpnd 0.55.0__tar.gz → 0.56.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.55.0 → cmpnd-0.56.0}/PKG-INFO +1 -1
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/programs.py +139 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/top.py +5 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/verbs.py +3 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/programs.py +1 -1
- {cmpnd-0.55.0 → cmpnd-0.56.0}/docs/cli.md +44 -2
- {cmpnd-0.55.0 → cmpnd-0.56.0}/docs/sdk-instrumentation.md +3 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/pyproject.toml +1 -1
- cmpnd-0.56.0/tests/test_cli/test_programs.py +290 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/uv.lock +1 -1
- cmpnd-0.55.0/tests/test_cli/test_programs.py +0 -143
- {cmpnd-0.55.0 → cmpnd-0.56.0}/.gitignore +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/AGENTS.md +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/CLAUDE.md +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/README.md +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/_assessments.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/_authored_import.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/_deployments.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/_exporter_pool.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/_http.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/_runtime_tools.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/_traces.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/_valsets.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/authored.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/callback.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/assessments.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/cli/tree_help.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/client.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/configuration.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/context.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/datasets.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/decorators.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/deployment.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/deployment_metadata.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/deployment_runs.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/execution.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/exporter.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/helpers.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/identity.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/imaging.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/lm_configuration.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/mcp_servers.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/models.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/named_programs.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/optimization.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/packaging.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/references.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/retry.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/cmpnd/traces.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/docs/client.md +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/docs/custom-proposer.md +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/docs/reading-traces.md +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/examples/local_ollama.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/README.md +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/cmpnd_ingest/program_names.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/ingest/pyproject.toml +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/scripts/check-pip-install.sh +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/scripts/release-stress.sh +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/scripts/selfhost_compat.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/stubs/dspy/utils/dummies.pyi +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/_deployment_signature_helpers.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/_fake_clock.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/_resolve_refs.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/_traced_run.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/conftest.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_assessments_cli.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_deploy_pandas.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_traces_read.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/e2e/test_valsets_cli.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/integration/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/integration/authored_optimize/judge.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/integration/conftest.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/load/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/load/__main__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/load/drain.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/load/ingest.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/load/rlm_size_report.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/load/sweep.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/load/test_rlm_size_report.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/conftest.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/test_health.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/span_reference_fixture.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/_provider.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/_readiness.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/_result.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_readiness.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/stress/test_trace_search.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_adapter_reentrancy.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_assessments_api.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_authored.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_authored_bundle_collisions.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_authored_metric_contract.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_authored_named_models.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_authored_optimize.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_callback.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_assessments.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_deployment_runs.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_signatures.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_tree_help.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cli/test_valsets.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_client.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_client_floor.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_client_program_names.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_configuration.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_context.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_datasets.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_decision_client_spans.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_decorators.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_deploy.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_deploy_mcp_tool_references.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_deploy_program_id.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_deployment_metadata.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_deployment_runs.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_deployments_read.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_eval.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_eval_start.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_execute.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_execute_run_identity.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_exporter.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_exporter_program_names.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_gepa_output_normalization.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_identity.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_imaging.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_import_weight.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_integration.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_lm15_native_calls.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_lm_configuration.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_lm_usage_shapes.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_models.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_multi_client_stress_readiness.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_optimize.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_packaging.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_poll_until.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_program_id_deprecation.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_program_identity.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_program_names_in_deploy.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_programs_create.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_project_payload.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_real_provider_twins_readiness.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_release_stress_script.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_replaced_names.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_retry.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_run_attribution_paths.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_runtime_tools.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_schemas.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_signature_inputs.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_span_references.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_span_sent_once.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_system_one.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_trace_context_concurrency.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_trace_fact_wire_contract.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_trace_facts.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_trace_render.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_traces_read.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_truncation.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_upload_auth_headers.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_valsets_api.py +0 -0
- {cmpnd-0.55.0 → cmpnd-0.56.0}/tests/test_xxh64.py +0 -0
|
@@ -4,10 +4,12 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import sys
|
|
7
|
+
import urllib.parse
|
|
7
8
|
from argparse import ArgumentParser, _SubParsersAction
|
|
8
9
|
from typing import Any
|
|
9
10
|
|
|
10
11
|
from .. import api_client, pagination
|
|
12
|
+
from . import provider_credentials
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
# Every id in this group is the value `programs list` prints as `program_hash`,
|
|
@@ -49,6 +51,11 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
49
51
|
ls = sub.add_parser("list", help="List programs")
|
|
50
52
|
ls.add_argument("--name", dest="program_name", help="Filter by program name")
|
|
51
53
|
ls.add_argument("--model", dest="model_name", help="Filter by model name")
|
|
54
|
+
ls.add_argument(
|
|
55
|
+
"--trashed",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="List the programs in the Trash instead, with when each is purged",
|
|
58
|
+
)
|
|
52
59
|
ls.add_argument("--order-by", default="run_count", help="Sort field (default: run_count)")
|
|
53
60
|
ls.add_argument("--order", default="desc", choices=["asc", "desc"], help="Sort direction")
|
|
54
61
|
pagination.add_args(ls, with_page_aliases=True, default_limit=50)
|
|
@@ -93,6 +100,37 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
93
100
|
reg_show.add_argument("id", help="Registry ID")
|
|
94
101
|
reg_show.set_defaults(func=handle_registry_show)
|
|
95
102
|
|
|
103
|
+
# The Trash. Each command takes a program name or a p_ id. A program in the
|
|
104
|
+
# Trash leaves every view and frees its place under the plan's program limit,
|
|
105
|
+
# and stays restorable until it is purged.
|
|
106
|
+
trash = sub.add_parser(
|
|
107
|
+
"trash",
|
|
108
|
+
help="Move a program to the Trash",
|
|
109
|
+
description=(
|
|
110
|
+
"Move PROGRAM to the Trash. It leaves every view and no longer counts toward your plan's program "
|
|
111
|
+
"limit. Restore it with `cmpnd programs restore PROGRAM` before it is purged, 7 days later."
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
trash.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
|
|
115
|
+
trash.set_defaults(func=handle_trash)
|
|
116
|
+
|
|
117
|
+
restore = sub.add_parser("restore", help="Restore a program from the Trash")
|
|
118
|
+
restore.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
|
|
119
|
+
restore.set_defaults(func=handle_restore)
|
|
120
|
+
|
|
121
|
+
purge = sub.add_parser(
|
|
122
|
+
"purge",
|
|
123
|
+
help="Delete a program in the Trash for good",
|
|
124
|
+
description=(
|
|
125
|
+
"Delete PROGRAM, which must be in the Trash, for good, or every program in the Trash with --all. "
|
|
126
|
+
"Nothing restores a purged program."
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
purge.add_argument("program", metavar="PROGRAM", nargs="?", default=None, help="Program name or p_ id")
|
|
130
|
+
purge.add_argument("--all", action="store_true", help="Purge every program in the Trash")
|
|
131
|
+
purge.add_argument("--force", action="store_true", help="Skip the confirmation (required when not on a terminal)")
|
|
132
|
+
purge.set_defaults(func=handle_purge)
|
|
133
|
+
|
|
96
134
|
select = sub.add_parser("select", help="Pin an optimization run as this program's chosen version")
|
|
97
135
|
select.add_argument("id", help="Program hash, as `programs list` prints it in the program_hash column")
|
|
98
136
|
select.add_argument("--run", dest="run_id", required=True, help="Optimization run ID (op_...) to pin")
|
|
@@ -140,6 +178,22 @@ def handle_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
|
140
178
|
|
|
141
179
|
|
|
142
180
|
def handle_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
181
|
+
if getattr(args, "trashed", False):
|
|
182
|
+
# The Trash is its own short list, unpaged and newest first, so the search filters, sort and paging do
|
|
183
|
+
# not apply. A flag left at its default cannot be told apart from one not passed.
|
|
184
|
+
for attr, flag, default in [
|
|
185
|
+
("program_name", "--name", None),
|
|
186
|
+
("model_name", "--model", None),
|
|
187
|
+
("order_by", "--order-by", "run_count"),
|
|
188
|
+
("order", "--order", "desc"),
|
|
189
|
+
("limit", "--limit", 50),
|
|
190
|
+
("offset", "--offset", 0),
|
|
191
|
+
("per_page", "--per-page", None),
|
|
192
|
+
("page", "--page", None),
|
|
193
|
+
]:
|
|
194
|
+
if getattr(args, attr, default) != default:
|
|
195
|
+
raise api_client.CliError(f"{flag} does not apply to --trashed, which lists the whole Trash")
|
|
196
|
+
return client.get("/api/v1/programs/trash")
|
|
143
197
|
params: dict[str, str] = pagination.wire(args, "per_page", as_str=True)
|
|
144
198
|
params["order_by"] = args.order_by
|
|
145
199
|
params["order_direction"] = args.order
|
|
@@ -189,3 +243,88 @@ def handle_select(client: api_client.ApiClient, args: Any) -> Any:
|
|
|
189
243
|
audit trail records who made it.
|
|
190
244
|
"""
|
|
191
245
|
return client.post(f"/api/v1/programs/{args.id}/select", json={"optimization_run_id": args.run_id})
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _resolve(client: api_client.ApiClient, ref: str) -> tuple[str, bool | None]:
|
|
249
|
+
"""Return the program id ``ref`` names, and whether it is in the Trash when known.
|
|
250
|
+
|
|
251
|
+
A ``p_`` id is used as given, because no program name starts with ``p_``, and its
|
|
252
|
+
Trash state is unknown until the route answers. A name is looked up, which never
|
|
253
|
+
makes a program, and the answer says whether it is in the Trash.
|
|
254
|
+
"""
|
|
255
|
+
from ... import programs
|
|
256
|
+
|
|
257
|
+
if ref.lower().startswith("p_"):
|
|
258
|
+
return ref, None
|
|
259
|
+
try:
|
|
260
|
+
programs.validate_program_name(ref)
|
|
261
|
+
except ValueError as exc:
|
|
262
|
+
raise api_client.CliError(str(exc)) from exc
|
|
263
|
+
try:
|
|
264
|
+
answer = client.get("/api/v1/programs/by-name/" + urllib.parse.quote(ref, safe=""))
|
|
265
|
+
except api_client.CliError as exc:
|
|
266
|
+
if exc.status_code == 404:
|
|
267
|
+
raise api_client.CliError(f"no program in your organization is named {ref!r}") from exc
|
|
268
|
+
if exc.status_code == 410:
|
|
269
|
+
raise api_client.CliError(f"{ref!r} belongs to a purged program, which nothing restores") from exc
|
|
270
|
+
raise
|
|
271
|
+
return answer["program_id"], bool(answer.get("trashed"))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def handle_trash(client: api_client.ApiClient, args: Any) -> Any:
|
|
275
|
+
"""Move a program to the Trash, which frees its place under the plan's program limit."""
|
|
276
|
+
from . import top
|
|
277
|
+
|
|
278
|
+
program_id, trashed = _resolve(client, args.program)
|
|
279
|
+
if trashed:
|
|
280
|
+
print(
|
|
281
|
+
f"{args.program} is already in the Trash. Restore it with `{top.hint_programs('restore', args.program)}`.",
|
|
282
|
+
file=sys.stderr,
|
|
283
|
+
)
|
|
284
|
+
return {"program_id": program_id, "trashed": True}
|
|
285
|
+
return client.post(f"/api/v1/programs/{program_id}/trash")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def handle_restore(client: api_client.ApiClient, args: Any) -> Any:
|
|
289
|
+
"""Return a program from the Trash. The server refuses it when the plan's program limit is full."""
|
|
290
|
+
from . import top
|
|
291
|
+
|
|
292
|
+
program_id, trashed = _resolve(client, args.program)
|
|
293
|
+
if trashed is False:
|
|
294
|
+
# A name another program took from a Trashed one reaches the new, live program. The Trashed one is
|
|
295
|
+
# restored by its id.
|
|
296
|
+
raise api_client.CliError(
|
|
297
|
+
f"{args.program} is not in the Trash. If its name now belongs to another program, restore the one "
|
|
298
|
+
f"in the Trash by the p_ id that `{top.hint_programs('list', '--trashed')}` prints."
|
|
299
|
+
)
|
|
300
|
+
return client.post(f"/api/v1/programs/{program_id}/restore")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def handle_purge(client: api_client.ApiClient, args: Any) -> Any:
|
|
304
|
+
"""Delete one program in the Trash, or the whole Trash with ``--all``, for good."""
|
|
305
|
+
from . import top
|
|
306
|
+
|
|
307
|
+
consequence = "Nothing restores a purged program, and its traces stay hidden."
|
|
308
|
+
if args.all:
|
|
309
|
+
if args.program is not None:
|
|
310
|
+
raise api_client.CliError("name either one PROGRAM or --all, not both")
|
|
311
|
+
# The count is for the person confirming. --force skips the listing, which needs traces:read, so a key
|
|
312
|
+
# that may only write can still empty the Trash.
|
|
313
|
+
if not args.force:
|
|
314
|
+
listed = client.get("/api/v1/programs/trash")
|
|
315
|
+
count = len(listed.get("programs") or [])
|
|
316
|
+
if count == 0:
|
|
317
|
+
return {"purged": 0, "kept": 0}
|
|
318
|
+
noun = "program" if count == 1 else "programs"
|
|
319
|
+
provider_credentials.confirm(args, f"the {count} {noun} in the Trash for good", consequence)
|
|
320
|
+
return client.post("/api/v1/programs/trash/empty")
|
|
321
|
+
if args.program is None:
|
|
322
|
+
raise api_client.CliError("name the PROGRAM to purge, or pass --all to empty the Trash")
|
|
323
|
+
program_id, trashed = _resolve(client, args.program)
|
|
324
|
+
if trashed is False:
|
|
325
|
+
raise api_client.CliError(
|
|
326
|
+
f"{args.program} is live, and only a program in the Trash can be purged. "
|
|
327
|
+
f"Move it there first with `{top.hint_programs('trash', args.program)}`."
|
|
328
|
+
)
|
|
329
|
+
provider_credentials.confirm(args, f"the program {args.program} for good", consequence)
|
|
330
|
+
return client.post(f"/api/v1/programs/{program_id}/purge")
|
|
@@ -213,6 +213,11 @@ def hint_programs_create(name: str = "capital-qa") -> str:
|
|
|
213
213
|
return f"cmpnd programs create {name}"
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def hint_programs(verb: str, *args: str) -> str:
|
|
217
|
+
"""How to run a ``cmpnd programs`` verb with ARGS. The shell has no programs verb, so this is the CLI form."""
|
|
218
|
+
return " ".join(("cmpnd programs", verb, *args))
|
|
219
|
+
|
|
220
|
+
|
|
216
221
|
def _hint_status(deployment_id: str, follow: bool = False) -> str:
|
|
217
222
|
short = _short_id(deployment_id)
|
|
218
223
|
if _SHELL_MODE:
|
|
@@ -160,6 +160,9 @@ DOMAIN_VERBS: dict[str, str] = {
|
|
|
160
160
|
"deny": "the negative half of allow; the tool stays listed and stops being callable",
|
|
161
161
|
"switch": "moves a local pointer among stored keys; nothing on the server changes",
|
|
162
162
|
"leave": "removes your own membership, which is not deleting the org or anyone else's place in it",
|
|
163
|
+
"trash": "moves a program to the Trash, where it stays restorable, and delete would read as the permanent purge",
|
|
164
|
+
"restore": "returns a program from the Trash with its history, which create would not",
|
|
165
|
+
"purge": "deletes a program in the Trash for good, the irreversible step after trash",
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
#: Leaves naming a related collection or sub-record rather than an action.
|
|
@@ -29,7 +29,7 @@ def send_resolve(client: _http.ApiClient, name: str) -> dict[str, Any] | None:
|
|
|
29
29
|
|
|
30
30
|
The answer is ``{"program_id", "name", "created"}``. ``created`` is true when this
|
|
31
31
|
request made the program. Returns None when the server has no program names, and
|
|
32
|
-
raises :class:`cmpnd.ApiError` for any other refusal, such as
|
|
32
|
+
raises :class:`cmpnd.ApiError` for any other refusal, such as the plan's program limit.
|
|
33
33
|
|
|
34
34
|
The request is idempotent: serve gives one name one program, so a retried request
|
|
35
35
|
that lost its answer finds the program the first request made.
|
|
@@ -41,6 +41,47 @@ The CLI refuses `--program-id`, `cmpnd programs create` without a program
|
|
|
41
41
|
name, and its `--name`. These inputs are deprecated, and the error shows the
|
|
42
42
|
program name form.
|
|
43
43
|
|
|
44
|
+
## The Trash
|
|
45
|
+
|
|
46
|
+
A program in the Trash leaves every view, and its traces with it, until it is
|
|
47
|
+
restored or purged. The web app's program page moves a program there with Move
|
|
48
|
+
to Trash, and these commands do the same from a terminal. Each takes a program
|
|
49
|
+
name or a `p_` id. The CLI looks a name up with
|
|
50
|
+
`GET /api/v1/programs/by-name/{name}`, which finds a program in the Trash too,
|
|
51
|
+
and sends the id.
|
|
52
|
+
|
|
53
|
+
- `cmpnd programs trash PROGRAM` moves a live program to the Trash. A program
|
|
54
|
+
already there is left alone.
|
|
55
|
+
- `cmpnd programs list --trashed` lists the whole Trash, newest first, with
|
|
56
|
+
when each program is purged. It takes no filter, sort or paging flag.
|
|
57
|
+
- `cmpnd programs restore PROGRAM` returns a program from the Trash.
|
|
58
|
+
- `cmpnd programs purge PROGRAM` deletes a program in the Trash for good, and
|
|
59
|
+
`cmpnd programs purge --all` empties the Trash. Nothing restores a purged
|
|
60
|
+
program. Both ask for confirmation at a terminal and need `--force` anywhere
|
|
61
|
+
else. A live program is never purged.
|
|
62
|
+
A program created by name cannot be purged yet. It stays in the Trash, where
|
|
63
|
+
it no longer counts toward a plan's program cap, and `purge --all` answers
|
|
64
|
+
how many such programs it kept.
|
|
65
|
+
|
|
66
|
+
A program in the Trash or purged gives up its name. The next
|
|
67
|
+
`cmpnd programs create`, `cmpnd deploy` or SDK run under that name gets a new
|
|
68
|
+
program. Restore the old one by its `p_` id, which `cmpnd programs list
|
|
69
|
+
--trashed` prints, and it comes back without the name.
|
|
70
|
+
|
|
71
|
+
The purge job purges a program 7 days after it entered the Trash. Listing the
|
|
72
|
+
Trash needs `traces:read`, and the other commands need `traces:write` or
|
|
73
|
+
`optimizations:write`.
|
|
74
|
+
|
|
75
|
+
A plan may cap an organization's live programs, the ones outside the Trash
|
|
76
|
+
that no optimization produced. At the cap, a new program name in
|
|
77
|
+
`cmpnd programs create`, `cmpnd deploy` or `cmpnd optimize` and
|
|
78
|
+
`cmpnd programs restore` are refused with a message that says how many the
|
|
79
|
+
plan allows and how to free one. A live program's name always resolves. A name
|
|
80
|
+
that only a program in the Trash answers to goes to a new program, which needs
|
|
81
|
+
a free place like any other. Moving a program to the Trash frees its place at
|
|
82
|
+
once. `cmpnd programs list --trashed` reports `live_programs` and
|
|
83
|
+
`program_limit`, which is null when no cap applies.
|
|
84
|
+
|
|
44
85
|
## When the server no longer supports your release
|
|
45
86
|
|
|
46
87
|
Every answer from the platform names the oldest `cmpnd` release it supports.
|
|
@@ -276,8 +317,9 @@ The `signatures` and `programs` groups name programs differently.
|
|
|
276
317
|
hash, which identifies a program's inputs and outputs and everything recorded
|
|
277
318
|
under them: traces, examples, evals and revisions. The CLI passes an id
|
|
278
319
|
through unchanged, and serve tells the two apart by the `p_` prefix.
|
|
279
|
-
`cmpnd programs`
|
|
280
|
-
`program_hash` column of `cmpnd programs list
|
|
320
|
+
`cmpnd programs` reads take a compiled program hash, the value in the
|
|
321
|
+
`program_hash` column of `cmpnd programs list`, and its Trash commands take a
|
|
322
|
+
program name or id ("The Trash" above). Its routes resolve the hash
|
|
281
323
|
through optimization runs, so they do not find a signature hash. Each group's
|
|
282
324
|
`--help` names the column its IDs come from.
|
|
283
325
|
|
|
@@ -843,6 +843,9 @@ trace bodies, at the top level and in `started_fact`, before it compares or send
|
|
|
843
843
|
- **A refusal settles the name with no id.** Serve answers 404 or 405 when it has no
|
|
844
844
|
program names, as self-host release 2026-001 does, and 409, 422 or 403 for a name it
|
|
845
845
|
will not resolve. The traces go without an id, and the exporter warns once per name.
|
|
846
|
+
An organization with as many live programs as its plan allows gets a 409 for a new
|
|
847
|
+
name, whose text says how to free one. The name stays settled for the process, so a
|
|
848
|
+
process started after a program was moved to the Trash resolves it.
|
|
846
849
|
- **An outage leaves the name unsettled.** A 5xx, a 429 or an unreachable server is
|
|
847
850
|
retried with backoff, wider than the data sends' rule because the request is a
|
|
848
851
|
get-or-create that a repeat cannot double. The names in one batch share one retry
|
|
@@ -5,7 +5,7 @@ name = "cmpnd"
|
|
|
5
5
|
# it's new) — no git tags. Bump with `uv version --bump {minor,patch}` or by
|
|
6
6
|
# hand. Runtime `cmpnd.__version__` reads installed metadata, so it tracks
|
|
7
7
|
# this automatically. See docs/plans/2026-07-02-sdk-pypi-tag-free-publish.md.
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.56.0"
|
|
9
9
|
description = "DSPy observability and deployment SDK for cmpnd"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Tests for cmpnd.cli.commands.programs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from cmpnd.cli.api_client import CliError
|
|
10
|
+
from cmpnd.cli.commands import programs
|
|
11
|
+
from cmpnd.cli.commands.programs import (
|
|
12
|
+
handle_list,
|
|
13
|
+
handle_registry_list,
|
|
14
|
+
handle_registry_show,
|
|
15
|
+
handle_select,
|
|
16
|
+
handle_show,
|
|
17
|
+
handle_stats,
|
|
18
|
+
handle_traces,
|
|
19
|
+
handle_usage,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
24
|
+
parser = argparse.ArgumentParser(prog="cmpnd")
|
|
25
|
+
sub = parser.add_subparsers(dest="resource")
|
|
26
|
+
programs.register(sub)
|
|
27
|
+
return parser
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestProgramsList:
|
|
31
|
+
def test_default_params(self, mock_client):
|
|
32
|
+
mock_client.get.return_value = {"programs": []}
|
|
33
|
+
args = argparse.Namespace(
|
|
34
|
+
program_name=None, model_name=None, order_by="run_count", order="desc", limit=50, offset=0
|
|
35
|
+
)
|
|
36
|
+
result = handle_list(mock_client, args)
|
|
37
|
+
mock_client.get.assert_called_once_with(
|
|
38
|
+
"/api/v1/programs/v2",
|
|
39
|
+
params={"page": "1", "per_page": "50", "order_by": "run_count", "order_direction": "desc"},
|
|
40
|
+
)
|
|
41
|
+
assert result == {"programs": []}
|
|
42
|
+
|
|
43
|
+
def test_with_filters(self, mock_client):
|
|
44
|
+
mock_client.get.return_value = {"programs": []}
|
|
45
|
+
args = argparse.Namespace(
|
|
46
|
+
program_name="RAG", model_name="gpt-4o", order_by="run_count", order="asc", limit=10, offset=10
|
|
47
|
+
)
|
|
48
|
+
handle_list(mock_client, args)
|
|
49
|
+
params = mock_client.get.call_args[1]["params"]
|
|
50
|
+
assert params["program_name"] == "RAG"
|
|
51
|
+
assert params["model_name"] == "gpt-4o"
|
|
52
|
+
assert params["page"] == "2"
|
|
53
|
+
|
|
54
|
+
def test_deprecated_paging_still_works_and_warns(self, mock_client, capsys):
|
|
55
|
+
parser = _build_parser()
|
|
56
|
+
args = parser.parse_args(["programs", "list", "--page", "2", "--per-page", "5"])
|
|
57
|
+
mock_client.get.return_value = {"programs": []}
|
|
58
|
+
handle_list(mock_client, args)
|
|
59
|
+
params = mock_client.get.call_args[1]["params"]
|
|
60
|
+
assert params["page"] == "2"
|
|
61
|
+
assert params["per_page"] == "5"
|
|
62
|
+
assert "deprecated" in capsys.readouterr().err
|
|
63
|
+
|
|
64
|
+
def test_non_multiple_offset_errors(self, mock_client):
|
|
65
|
+
parser = _build_parser()
|
|
66
|
+
args = parser.parse_args(["programs", "list", "--limit", "10", "--offset", "5"])
|
|
67
|
+
with pytest.raises(CliError):
|
|
68
|
+
handle_list(mock_client, args)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestProgramsShow:
|
|
72
|
+
def test_calls_correct_endpoint(self, mock_client):
|
|
73
|
+
mock_client.get.return_value = {"program_hash": 123}
|
|
74
|
+
args = argparse.Namespace(id="123")
|
|
75
|
+
result = handle_show(mock_client, args)
|
|
76
|
+
mock_client.get.assert_called_once_with("/api/v1/programs/123")
|
|
77
|
+
assert result["program_hash"] == 123
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestProgramsTraces:
|
|
81
|
+
def test_calls_correct_endpoint(self, mock_client):
|
|
82
|
+
mock_client.get.return_value = {"traces": []}
|
|
83
|
+
args = argparse.Namespace(id="456", limit=10, offset=5)
|
|
84
|
+
result = handle_traces(mock_client, args)
|
|
85
|
+
mock_client.get.assert_called_once_with("/api/v1/programs/456/traces", params={"limit": "10", "offset": "5"})
|
|
86
|
+
assert result == {"traces": []}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestProgramsStats:
|
|
90
|
+
def test_calls_correct_endpoint(self, mock_client):
|
|
91
|
+
mock_client.get.return_value = {"trace_count": 12}
|
|
92
|
+
result = handle_stats(mock_client, argparse.Namespace(id="789"))
|
|
93
|
+
mock_client.get.assert_called_once_with("/api/v1/programs/789/execution-stats")
|
|
94
|
+
assert result == {"trace_count": 12}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TestProgramsUsage:
|
|
98
|
+
def test_sends_the_default_window(self, mock_client):
|
|
99
|
+
mock_client.get.return_value = {"history": []}
|
|
100
|
+
handle_usage(mock_client, argparse.Namespace(id="789", hours=24, period=None))
|
|
101
|
+
mock_client.get.assert_called_once_with("/api/v1/programs/789/usage-history", params={"hours": "24"})
|
|
102
|
+
|
|
103
|
+
def test_period_is_sent_only_when_given(self, mock_client):
|
|
104
|
+
mock_client.get.return_value = {"history": []}
|
|
105
|
+
handle_usage(mock_client, argparse.Namespace(id="789", hours=6, period="minute"))
|
|
106
|
+
assert mock_client.get.call_args[1]["params"] == {"hours": "6", "period": "minute"}
|
|
107
|
+
|
|
108
|
+
def test_parser_rejects_an_unknown_period(self):
|
|
109
|
+
# An unrecognised period buckets hourly server-side rather than failing,
|
|
110
|
+
# so a typo would answer a different question than the one asked.
|
|
111
|
+
parser = _build_parser()
|
|
112
|
+
with pytest.raises(SystemExit):
|
|
113
|
+
parser.parse_args(["programs", "usage", "789", "--period", "fortnight"])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestProgramsRegistry:
|
|
117
|
+
def test_list_uses_limit_offset(self, mock_client):
|
|
118
|
+
mock_client.get.return_value = {"programs": [], "total": 0}
|
|
119
|
+
handle_registry_list(mock_client, argparse.Namespace(limit=25, offset=50))
|
|
120
|
+
mock_client.get.assert_called_once_with("/api/v1/programs/registry", params={"limit": "25", "offset": "50"})
|
|
121
|
+
|
|
122
|
+
def test_show_calls_correct_endpoint(self, mock_client):
|
|
123
|
+
mock_client.get.return_value = {"registry": {}}
|
|
124
|
+
handle_registry_show(mock_client, argparse.Namespace(id="reg-1"))
|
|
125
|
+
mock_client.get.assert_called_once_with("/api/v1/programs/registry/reg-1")
|
|
126
|
+
|
|
127
|
+
def test_registry_is_a_nested_group(self):
|
|
128
|
+
parser = _build_parser()
|
|
129
|
+
assert parser.parse_args(["programs", "registry", "list"]).func is programs.handle_registry_list
|
|
130
|
+
assert parser.parse_args(["programs", "registry", "show", "reg-1"]).func is programs.handle_registry_show
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TestProgramsSelect:
|
|
134
|
+
def test_posts_the_key_the_server_reads(self, mock_client):
|
|
135
|
+
mock_client.post.return_value = {"status": "selected"}
|
|
136
|
+
result = handle_select(mock_client, argparse.Namespace(id="789", run_id="op_abc"))
|
|
137
|
+
mock_client.post.assert_called_once_with("/api/v1/programs/789/select", json={"optimization_run_id": "op_abc"})
|
|
138
|
+
assert result == {"status": "selected"}
|
|
139
|
+
|
|
140
|
+
def test_run_id_is_required(self):
|
|
141
|
+
parser = _build_parser()
|
|
142
|
+
with pytest.raises(SystemExit):
|
|
143
|
+
parser.parse_args(["programs", "select", "789"])
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _named(program_id: str, name: str, *, trashed: bool) -> dict[str, object]:
|
|
147
|
+
return {"program_id": program_id, "name": name, "created": False, "trashed": trashed}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TestProgramsTrash:
|
|
151
|
+
def test_a_name_resolves_then_moves_to_the_trash(self, mock_client):
|
|
152
|
+
mock_client.get.return_value = _named("p_0123456789Ab", "triage", trashed=False)
|
|
153
|
+
mock_client.post.return_value = {"program_id": "p_0123456789Ab", "trashed": True}
|
|
154
|
+
args = _build_parser().parse_args(["programs", "trash", "triage"])
|
|
155
|
+
result = args.func(mock_client, args)
|
|
156
|
+
mock_client.get.assert_called_once_with("/api/v1/programs/by-name/triage")
|
|
157
|
+
mock_client.post.assert_called_once_with("/api/v1/programs/p_0123456789Ab/trash")
|
|
158
|
+
assert result == {"program_id": "p_0123456789Ab", "trashed": True}
|
|
159
|
+
|
|
160
|
+
def test_a_program_id_goes_straight_to_the_route(self, mock_client):
|
|
161
|
+
mock_client.post.return_value = {"program_id": "p_0123456789Ab", "trashed": True}
|
|
162
|
+
args = _build_parser().parse_args(["programs", "trash", "p_0123456789Ab"])
|
|
163
|
+
args.func(mock_client, args)
|
|
164
|
+
mock_client.get.assert_not_called()
|
|
165
|
+
mock_client.post.assert_called_once_with("/api/v1/programs/p_0123456789Ab/trash")
|
|
166
|
+
|
|
167
|
+
def test_a_name_already_in_the_trash_is_not_moved_again(self, mock_client, capsys):
|
|
168
|
+
mock_client.get.return_value = _named("p_0123456789Ab", "triage", trashed=True)
|
|
169
|
+
args = _build_parser().parse_args(["programs", "trash", "triage"])
|
|
170
|
+
result = args.func(mock_client, args)
|
|
171
|
+
mock_client.post.assert_not_called()
|
|
172
|
+
assert result == {"program_id": "p_0123456789Ab", "trashed": True}
|
|
173
|
+
assert "already in the Trash" in capsys.readouterr().err
|
|
174
|
+
|
|
175
|
+
def test_an_unknown_name_says_so(self, mock_client):
|
|
176
|
+
mock_client.get.side_effect = CliError("no program has this name", status_code=404)
|
|
177
|
+
args = _build_parser().parse_args(["programs", "trash", "nobody"])
|
|
178
|
+
with pytest.raises(CliError, match="no program in your organization is named 'nobody'"):
|
|
179
|
+
args.func(mock_client, args)
|
|
180
|
+
mock_client.post.assert_not_called()
|
|
181
|
+
|
|
182
|
+
def test_a_name_resolves_through_the_by_name_route(self, mock_client):
|
|
183
|
+
mock_client.get.return_value = _named("p_0123456789Ab", "a.b_c", trashed=False)
|
|
184
|
+
args = _build_parser().parse_args(["programs", "trash", "a.b_c"])
|
|
185
|
+
args.func(mock_client, args)
|
|
186
|
+
mock_client.get.assert_called_once_with("/api/v1/programs/by-name/a.b_c")
|
|
187
|
+
|
|
188
|
+
def test_a_bad_name_fails_before_any_request(self, mock_client):
|
|
189
|
+
args = _build_parser().parse_args(["programs", "trash", "has space"])
|
|
190
|
+
with pytest.raises(CliError, match="not a valid program name"):
|
|
191
|
+
args.func(mock_client, args)
|
|
192
|
+
mock_client.get.assert_not_called()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class TestProgramsRestore:
|
|
196
|
+
def test_restores_a_trashed_name(self, mock_client):
|
|
197
|
+
mock_client.get.return_value = _named("p_0123456789Ab", "triage", trashed=True)
|
|
198
|
+
mock_client.post.return_value = {"program_id": "p_0123456789Ab", "trashed": False}
|
|
199
|
+
args = _build_parser().parse_args(["programs", "restore", "triage"])
|
|
200
|
+
args.func(mock_client, args)
|
|
201
|
+
mock_client.post.assert_called_once_with("/api/v1/programs/p_0123456789Ab/restore")
|
|
202
|
+
|
|
203
|
+
def test_a_live_name_is_not_in_the_trash(self, mock_client):
|
|
204
|
+
mock_client.get.return_value = _named("p_0123456789Ab", "triage", trashed=False)
|
|
205
|
+
args = _build_parser().parse_args(["programs", "restore", "triage"])
|
|
206
|
+
with pytest.raises(CliError, match="not in the Trash") as refused:
|
|
207
|
+
args.func(mock_client, args)
|
|
208
|
+
assert "`cmpnd programs list --trashed`" in str(refused.value)
|
|
209
|
+
mock_client.post.assert_not_called()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class TestProgramsPurge:
|
|
213
|
+
def test_purge_needs_force_off_a_terminal(self, mock_client, monkeypatch):
|
|
214
|
+
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
|
215
|
+
mock_client.get.return_value = _named("p_0123456789Ab", "triage", trashed=True)
|
|
216
|
+
args = _build_parser().parse_args(["programs", "purge", "triage"])
|
|
217
|
+
with pytest.raises(CliError, match="--force"):
|
|
218
|
+
args.func(mock_client, args)
|
|
219
|
+
mock_client.post.assert_not_called()
|
|
220
|
+
|
|
221
|
+
def test_purge_with_force_purges(self, mock_client):
|
|
222
|
+
mock_client.get.return_value = _named("p_0123456789Ab", "triage", trashed=True)
|
|
223
|
+
mock_client.post.return_value = {"program_id": "p_0123456789Ab", "purged": True}
|
|
224
|
+
args = _build_parser().parse_args(["programs", "purge", "triage", "--force"])
|
|
225
|
+
assert args.func(mock_client, args) == {"program_id": "p_0123456789Ab", "purged": True}
|
|
226
|
+
mock_client.post.assert_called_once_with("/api/v1/programs/p_0123456789Ab/purge")
|
|
227
|
+
|
|
228
|
+
def test_a_live_program_is_never_purged(self, mock_client):
|
|
229
|
+
mock_client.get.return_value = _named("p_0123456789Ab", "triage", trashed=False)
|
|
230
|
+
args = _build_parser().parse_args(["programs", "purge", "triage", "--force"])
|
|
231
|
+
with pytest.raises(CliError, match="cmpnd programs trash triage"):
|
|
232
|
+
args.func(mock_client, args)
|
|
233
|
+
mock_client.post.assert_not_called()
|
|
234
|
+
|
|
235
|
+
def test_all_with_force_empties_the_trash_without_listing_it(self, mock_client):
|
|
236
|
+
# Listing needs traces:read, so --force skips it and a key that may only write still empties the Trash.
|
|
237
|
+
mock_client.post.return_value = {"purged": 1, "kept": 0}
|
|
238
|
+
args = _build_parser().parse_args(["programs", "purge", "--all", "--force"])
|
|
239
|
+
assert args.func(mock_client, args) == {"purged": 1, "kept": 0}
|
|
240
|
+
mock_client.get.assert_not_called()
|
|
241
|
+
mock_client.post.assert_called_once_with("/api/v1/programs/trash/empty")
|
|
242
|
+
|
|
243
|
+
def test_all_confirms_with_the_count_at_a_terminal(self, mock_client, monkeypatch):
|
|
244
|
+
mock_client.get.return_value = {"programs": [{"program_id": "p_0123456789Ab"}] * 2, "retention_days": 7}
|
|
245
|
+
mock_client.post.return_value = {"purged": 2, "kept": 0}
|
|
246
|
+
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
|
|
247
|
+
prompts: list[str] = []
|
|
248
|
+
monkeypatch.setattr("builtins.input", lambda prompt: prompts.append(prompt) or "yes")
|
|
249
|
+
args = _build_parser().parse_args(["programs", "purge", "--all"])
|
|
250
|
+
assert args.func(mock_client, args) == {"purged": 2, "kept": 0}
|
|
251
|
+
mock_client.get.assert_called_once_with("/api/v1/programs/trash")
|
|
252
|
+
mock_client.post.assert_called_once_with("/api/v1/programs/trash/empty")
|
|
253
|
+
assert prompts
|
|
254
|
+
|
|
255
|
+
def test_all_with_an_empty_trash_sends_nothing(self, mock_client):
|
|
256
|
+
mock_client.get.return_value = {"programs": [], "retention_days": 7}
|
|
257
|
+
args = _build_parser().parse_args(["programs", "purge", "--all"])
|
|
258
|
+
assert args.func(mock_client, args) == {"purged": 0, "kept": 0}
|
|
259
|
+
mock_client.post.assert_not_called()
|
|
260
|
+
|
|
261
|
+
def test_all_and_a_program_together_are_refused(self, mock_client):
|
|
262
|
+
args = _build_parser().parse_args(["programs", "purge", "triage", "--all", "--force"])
|
|
263
|
+
with pytest.raises(CliError, match="either"):
|
|
264
|
+
args.func(mock_client, args)
|
|
265
|
+
mock_client.get.assert_not_called()
|
|
266
|
+
mock_client.post.assert_not_called()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class TestProgramsListTrashed:
|
|
270
|
+
def test_lists_the_trash(self, mock_client):
|
|
271
|
+
mock_client.get.return_value = {"programs": [], "retention_days": 7}
|
|
272
|
+
args = _build_parser().parse_args(["programs", "list", "--trashed"])
|
|
273
|
+
assert args.func(mock_client, args) == {"programs": [], "retention_days": 7}
|
|
274
|
+
mock_client.get.assert_called_once_with("/api/v1/programs/trash")
|
|
275
|
+
|
|
276
|
+
def test_search_filters_do_not_apply(self, mock_client):
|
|
277
|
+
args = _build_parser().parse_args(["programs", "list", "--trashed", "--name", "x"])
|
|
278
|
+
with pytest.raises(CliError, match="--name does not apply"):
|
|
279
|
+
args.func(mock_client, args)
|
|
280
|
+
mock_client.get.assert_not_called()
|
|
281
|
+
|
|
282
|
+
@pytest.mark.parametrize(
|
|
283
|
+
"flag",
|
|
284
|
+
[["--order-by", "name"], ["--order", "asc"], ["--limit", "5"], ["--offset", "10"]],
|
|
285
|
+
)
|
|
286
|
+
def test_paging_and_sort_do_not_apply(self, mock_client, flag):
|
|
287
|
+
args = _build_parser().parse_args(["programs", "list", "--trashed", *flag])
|
|
288
|
+
with pytest.raises(CliError, match=f"{flag[0]} does not apply"):
|
|
289
|
+
args.func(mock_client, args)
|
|
290
|
+
mock_client.get.assert_not_called()
|