cmpnd 0.53.0__tar.gz → 0.54.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.53.0 → cmpnd-0.54.0}/AGENTS.md +4 -2
- {cmpnd-0.53.0 → cmpnd-0.54.0}/PKG-INFO +1 -1
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/admin.py +172 -3
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/optimizations.py +35 -1
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/optimization.py +18 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/cli.md +51 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/sdk-instrumentation.md +1 -1
- {cmpnd-0.53.0 → cmpnd-0.54.0}/pyproject.toml +1 -1
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_admin.py +135 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_optimizations.py +35 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize.py +34 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/uv.lock +1 -1
- {cmpnd-0.53.0 → cmpnd-0.54.0}/.gitignore +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/CLAUDE.md +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/README.md +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_assessments.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_authored_import.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_deployments.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_exporter_pool.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_http.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_runtime_tools.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_traces.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/_valsets.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/authored.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/callback.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/assessments.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/tree_help.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/client.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/configuration.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/context.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/datasets.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/decorators.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/deployment.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/deployment_metadata.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/deployment_runs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/execution.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/exporter.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/helpers.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/identity.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/imaging.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/lm_configuration.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/mcp_servers.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/models.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/named_programs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/packaging.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/programs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/references.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/retry.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/cmpnd/traces.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/client.md +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/custom-proposer.md +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/docs/reading-traces.md +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/examples/local_ollama.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/README.md +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/program_names.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/ingest/pyproject.toml +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/check-pip-install.sh +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/release-stress.sh +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/stubs/dspy/utils/dummies.pyi +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/_deployment_signature_helpers.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/_fake_clock.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/_resolve_refs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/_traced_run.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/conftest.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_assessments_cli.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_deploy_pandas.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_traces_read.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/e2e/test_valsets_cli.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/authored_optimize/judge.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/conftest.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/__main__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/drain.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/ingest.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/rlm_size_report.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/sweep.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/load/test_rlm_size_report.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/conftest.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_health.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/span_reference_fixture.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/_provider.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/_readiness.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/_result.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_readiness.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/stress/test_trace_search.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_adapter_reentrancy.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_assessments_api.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_authored.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_authored_bundle_collisions.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_authored_metric_contract.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_authored_named_models.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_authored_optimize.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_callback.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_assessments.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_deployment_runs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_signatures.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_tree_help.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cli/test_valsets.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_client.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_client_floor.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_client_program_names.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_configuration.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_context.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_datasets.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_decision_client_spans.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_decorators.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_deploy.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_deploy_mcp_tool_references.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_deploy_program_id.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_deployment_metadata.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_deployment_runs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_deployments_read.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_eval.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_eval_start.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_execute.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_execute_run_identity.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_exporter.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_exporter_program_names.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_gepa_output_normalization.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_identity.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_imaging.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_import_weight.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_integration.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_lm15_native_calls.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_lm_configuration.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_lm_usage_shapes.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_models.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_multi_client_stress_readiness.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_packaging.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_poll_until.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_program_id_deprecation.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_program_identity.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_program_names_in_deploy.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_programs_create.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_project_payload.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_real_provider_twins_readiness.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_release_stress_script.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_retry.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_run_attribution_paths.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_runtime_tools.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_schemas.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_signature_inputs.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_span_references.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_span_sent_once.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_system_one.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_trace_context_concurrency.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_trace_fact_wire_contract.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_trace_facts.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_trace_render.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_traces_read.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_truncation.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_upload_auth_headers.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_valsets_api.py +0 -0
- {cmpnd-0.53.0 → cmpnd-0.54.0}/tests/test_xxh64.py +0 -0
|
@@ -145,6 +145,8 @@ Before you touch either `pyproject.toml`:
|
|
|
145
145
|
|
|
146
146
|
- `ingest/cmpnd_ingest/exporter_http.py`: the ingest transport (`cmpnd.exporter_http`)
|
|
147
147
|
- `ingest/cmpnd_ingest/retry.py`: the client half of the retryable-error contract (`cmpnd.retry`)
|
|
148
|
+
- `ingest/cmpnd_ingest/client_floor.py`: warns once when serve's `X-Cmpnd-Minimum-Client-Version` header names a floor above the installed release
|
|
149
|
+
- `ingest/cmpnd_ingest/program_names.py`: keeps the program ids serve gave for the program names a process declared
|
|
148
150
|
- `cmpnd/identity.py`: hash computations (xxhash, SHA256). See the `cmpnd-hash-handling` skill.
|
|
149
151
|
- `cmpnd/ir/`: the structural program-identity IR (algebra, encoder, hash, reflector)
|
|
150
152
|
- `cmpnd/callback.py`: DSPy instrumentation and signature extraction
|
|
@@ -195,8 +197,8 @@ its `add_parser` call in `top.register()`, and add the entity method or the
|
|
|
195
197
|
When you touch a handler:
|
|
196
198
|
|
|
197
199
|
- **Write user-facing hints with the `_hint_*` helpers** in
|
|
198
|
-
`cli/commands/top.py` (`_hint_ps`, `_hint_run`, `
|
|
199
|
-
`_hint_inspect`, `_hint_traces_list`). They check `_SHELL_MODE` and emit
|
|
200
|
+
`cli/commands/top.py` (`_hint_ps`, `_hint_run`, `_hint_program_name`,
|
|
201
|
+
`_hint_status`, `_hint_inspect`, `_hint_traces_list`). They check `_SHELL_MODE` and emit
|
|
200
202
|
`deps.d_xxx.run({...})` in the shell or `cmpnd run d_xxx '{...}'` outside
|
|
201
203
|
it. Never hardcode a `cmpnd <verb>` string in handler output.
|
|
202
204
|
- **Add a shell entity** by subclassing `Entity` in `cli/shell.py`. Add a
|
|
@@ -8,7 +8,11 @@ adjust its member cap or GitHub link, and grant ownership from the election
|
|
|
8
8
|
queue. `cmpnd admin user …` is the operator's user-lifecycle surface (list,
|
|
9
9
|
inspect, create, approve/reject the pending-signup queue, promote/demote admin,
|
|
10
10
|
disable/enable, reset a password, and manage a user's API-key scopes). `cmpnd
|
|
11
|
-
admin
|
|
11
|
+
admin org plan …` and `cmpnd admin org overrides …` move an org between plans
|
|
12
|
+
and override one of its limits, `cmpnd admin plans …` edits each plan's
|
|
13
|
+
default limits and feature flags, and `cmpnd admin signup …` sets who may sign up
|
|
14
|
+
and the plan a new account's org lands on. `cmpnd admin feature-flags …` reads and
|
|
15
|
+
toggles the server's runtime flags. These hit
|
|
12
16
|
/api/v1/admin/* and require an API key carrying the admin:* scopes — the server
|
|
13
17
|
is the real authority. The CMPND_CLI_ADMIN env gate (mirroring CMPND_CLI_DEV)
|
|
14
18
|
only keeps these out of customers' --help; it grants nothing.
|
|
@@ -16,7 +20,7 @@ only keeps these out of customers' --help; it grants nothing.
|
|
|
16
20
|
|
|
17
21
|
from __future__ import annotations
|
|
18
22
|
|
|
19
|
-
from argparse import ArgumentParser, _SubParsersAction
|
|
23
|
+
from argparse import ArgumentParser, ArgumentTypeError, _SubParsersAction
|
|
20
24
|
from typing import Any
|
|
21
25
|
|
|
22
26
|
from .. import api_client, deprecation
|
|
@@ -28,9 +32,29 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
28
32
|
|
|
29
33
|
_register_org(sub)
|
|
30
34
|
_register_user(sub)
|
|
35
|
+
_register_plans(sub)
|
|
36
|
+
_register_signup(sub)
|
|
31
37
|
_register_feature_flags(sub)
|
|
32
38
|
|
|
33
39
|
|
|
40
|
+
# The signup modes, as serve names them. argparse refuses a typo before the request,
|
|
41
|
+
# and the server still validates it. Plan names are serve's alone: the CLI sends the
|
|
42
|
+
# name as typed, and serve's refusal lists the plans it has, so a new plan needs no
|
|
43
|
+
# CLI release.
|
|
44
|
+
SIGNUP_MODES = ("closed", "invite", "open")
|
|
45
|
+
PLAN_HELP = "The plan, as `cmpnd admin plans list` names it"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _limit_value(raw: str) -> int | None:
|
|
49
|
+
"""A limit is a whole number, or ``none`` for no limit, which sends null."""
|
|
50
|
+
if raw.lower() == "none":
|
|
51
|
+
return None
|
|
52
|
+
try:
|
|
53
|
+
return int(raw)
|
|
54
|
+
except ValueError:
|
|
55
|
+
raise ArgumentTypeError(f"{raw!r} is not a whole number or 'none'") from None
|
|
56
|
+
|
|
57
|
+
|
|
34
58
|
def _register_org(sub: _SubParsersAction[ArgumentParser]) -> None:
|
|
35
59
|
org = sub.add_parser("org", help="Organization bootstrap")
|
|
36
60
|
osub = org.add_subparsers(dest="org_action")
|
|
@@ -71,6 +95,7 @@ def _register_org(sub: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
71
95
|
set_owner.set_defaults(func=handle_org_set_owner)
|
|
72
96
|
|
|
73
97
|
ls = osub.add_parser("list", help="List organizations")
|
|
98
|
+
ls.add_argument("--plan", help="Only the organizations on this plan")
|
|
74
99
|
ls.set_defaults(func=handle_org_list)
|
|
75
100
|
|
|
76
101
|
def _org_id_arg(p: ArgumentParser) -> None:
|
|
@@ -97,6 +122,32 @@ def _register_org(sub: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
97
122
|
elect_owner.add_argument("user_id", help="User id to make owner")
|
|
98
123
|
elect_owner.set_defaults(func=handle_org_elect_owner)
|
|
99
124
|
|
|
125
|
+
plan = osub.add_parser("plan", help="An org's plan and its limits")
|
|
126
|
+
plsub = plan.add_subparsers(dest="org_plan_action")
|
|
127
|
+
plan_show = plsub.add_parser("show", help="Show the org's plan and every limit as it resolves for the org")
|
|
128
|
+
_org_id_arg(plan_show)
|
|
129
|
+
plan_show.set_defaults(func=handle_org_plan_show)
|
|
130
|
+
plan_update = plsub.add_parser("update", help="Move the org to a plan")
|
|
131
|
+
_org_id_arg(plan_update)
|
|
132
|
+
plan_update.add_argument("plan", help=PLAN_HELP)
|
|
133
|
+
plan_update.set_defaults(func=handle_org_plan_update)
|
|
134
|
+
|
|
135
|
+
overrides = osub.add_parser("overrides", help="An org's overrides of its plan's limits")
|
|
136
|
+
ovsub = overrides.add_subparsers(dest="org_overrides_action")
|
|
137
|
+
override_update = ovsub.add_parser(
|
|
138
|
+
"update", help="Override one of the org's limits; 'none' lifts the plan's limit for the org"
|
|
139
|
+
)
|
|
140
|
+
_org_id_arg(override_update)
|
|
141
|
+
override_update.add_argument("name", help="Limit name, such as optimizations.concurrent")
|
|
142
|
+
override_update.add_argument("value", type=_limit_value, help="A whole number, or 'none' for no limit")
|
|
143
|
+
override_update.set_defaults(func=handle_org_overrides_update)
|
|
144
|
+
override_delete = ovsub.add_parser(
|
|
145
|
+
"delete", help="Delete the org's override so it follows its plan for the limit again"
|
|
146
|
+
)
|
|
147
|
+
_org_id_arg(override_delete)
|
|
148
|
+
override_delete.add_argument("name", help="Limit name")
|
|
149
|
+
override_delete.set_defaults(func=handle_org_overrides_delete)
|
|
150
|
+
|
|
100
151
|
|
|
101
152
|
def _register_user(sub: _SubParsersAction[ArgumentParser]) -> None:
|
|
102
153
|
user = sub.add_parser("user", help="User lifecycle management")
|
|
@@ -173,6 +224,57 @@ def _register_user(sub: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
173
224
|
set_scopes.set_defaults(func=handle_user_set_scopes)
|
|
174
225
|
|
|
175
226
|
|
|
227
|
+
def _register_plans(sub: _SubParsersAction[ArgumentParser]) -> None:
|
|
228
|
+
plans = sub.add_parser("plans", help="Each plan's default limits and feature flags")
|
|
229
|
+
psub = plans.add_subparsers(dest="plans_action")
|
|
230
|
+
|
|
231
|
+
ls = psub.add_parser("list", help="List every plan's limits and feature flags")
|
|
232
|
+
ls.set_defaults(func=handle_plans_list)
|
|
233
|
+
|
|
234
|
+
limits = psub.add_parser("limits", help="A plan's default limits")
|
|
235
|
+
lsub = limits.add_subparsers(dest="plans_limits_action")
|
|
236
|
+
limit_update = lsub.add_parser("update", help="Set a plan's default for a limit; 'none' means no limit")
|
|
237
|
+
limit_update.add_argument("plan", help=PLAN_HELP)
|
|
238
|
+
limit_update.add_argument("name", help="Limit name, such as optimizations.concurrent")
|
|
239
|
+
limit_update.add_argument("value", type=_limit_value, help="A whole number, or 'none' for no limit")
|
|
240
|
+
limit_update.set_defaults(func=handle_plans_limits_update)
|
|
241
|
+
limit_delete = lsub.add_parser("delete", help="Delete a plan's default for a limit")
|
|
242
|
+
limit_delete.add_argument("plan", help=PLAN_HELP)
|
|
243
|
+
limit_delete.add_argument("name", help="Limit name")
|
|
244
|
+
limit_delete.set_defaults(func=handle_plans_limits_delete)
|
|
245
|
+
|
|
246
|
+
flags = psub.add_parser("flags", help="A plan's feature flags")
|
|
247
|
+
fsub = flags.add_subparsers(dest="plans_flags_action")
|
|
248
|
+
flag_update = fsub.add_parser(
|
|
249
|
+
"update", help="Pin a feature flag on (or, with --disabled, off) for every org on a plan"
|
|
250
|
+
)
|
|
251
|
+
flag_update.add_argument("plan", help=PLAN_HELP)
|
|
252
|
+
flag_update.add_argument("name", help="Flag name; the flag must already exist")
|
|
253
|
+
flag_update.add_argument("--disabled", action="store_true", help="Pin the flag off (default is on)")
|
|
254
|
+
flag_update.set_defaults(func=handle_plans_flags_update)
|
|
255
|
+
flag_delete = fsub.add_parser("delete", help="Delete a plan's row for a flag so its orgs follow the global default")
|
|
256
|
+
flag_delete.add_argument("plan", help=PLAN_HELP)
|
|
257
|
+
flag_delete.add_argument("name", help="Flag name")
|
|
258
|
+
flag_delete.set_defaults(func=handle_plans_flags_delete)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _register_signup(sub: _SubParsersAction[ArgumentParser]) -> None:
|
|
262
|
+
signup = sub.add_parser("signup", help="Who may sign up, and the plan a new account's org lands on")
|
|
263
|
+
ssub = signup.add_subparsers(dest="signup_action")
|
|
264
|
+
|
|
265
|
+
show = ssub.add_parser("show", help="Show the signup mode in force and the plan for new orgs")
|
|
266
|
+
show.set_defaults(func=handle_signup_show)
|
|
267
|
+
|
|
268
|
+
set_signup = ssub.add_parser("update", help="Change the signup mode, the plan for new orgs, or both")
|
|
269
|
+
set_signup.add_argument(
|
|
270
|
+
"--mode",
|
|
271
|
+
choices=(*SIGNUP_MODES, "installation"),
|
|
272
|
+
help="closed, invite or open; 'installation' follows CMPND_SERVE_SIGNUP_MODE again",
|
|
273
|
+
)
|
|
274
|
+
set_signup.add_argument("--default-plan", dest="default_plan", help="The plan for new orgs")
|
|
275
|
+
set_signup.set_defaults(func=handle_signup_update)
|
|
276
|
+
|
|
277
|
+
|
|
176
278
|
def _register_feature_flags(sub: _SubParsersAction[ArgumentParser]) -> None:
|
|
177
279
|
ff = sub.add_parser("feature-flags", help="Runtime feature flags")
|
|
178
280
|
ffsub = ff.add_subparsers(dest="ff_action")
|
|
@@ -218,7 +320,10 @@ def handle_org_set_owner(client: api_client.ApiClient, args: Any) -> Any:
|
|
|
218
320
|
return {"org_id": args.org_id, "owner": args.email}
|
|
219
321
|
|
|
220
322
|
|
|
221
|
-
def handle_org_list(client: api_client.ApiClient,
|
|
323
|
+
def handle_org_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
324
|
+
plan = getattr(args, "plan", None)
|
|
325
|
+
if plan:
|
|
326
|
+
return client.get("/api/v1/admin/organizations", params={"plan": plan})
|
|
222
327
|
return client.get("/api/v1/admin/organizations")
|
|
223
328
|
|
|
224
329
|
|
|
@@ -245,6 +350,70 @@ def handle_org_elect_owner(client: api_client.ApiClient, args: Any) -> Any:
|
|
|
245
350
|
return {"elected": args.user_id, "github_login": args.github_login}
|
|
246
351
|
|
|
247
352
|
|
|
353
|
+
def handle_org_plan_show(client: api_client.ApiClient, args: Any) -> Any:
|
|
354
|
+
return client.get(f"/api/v1/admin/organizations/{args.org_id}/plan")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def handle_org_plan_update(client: api_client.ApiClient, args: Any) -> Any:
|
|
358
|
+
return client.put(f"/api/v1/admin/organizations/{args.org_id}/plan", json={"plan": args.plan})
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def handle_org_overrides_update(client: api_client.ApiClient, args: Any) -> Any:
|
|
362
|
+
return client.put(
|
|
363
|
+
f"/api/v1/admin/organizations/{args.org_id}/limits/{args.name}",
|
|
364
|
+
json={"value": args.value},
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def handle_org_overrides_delete(client: api_client.ApiClient, args: Any) -> Any:
|
|
369
|
+
return client.delete(f"/api/v1/admin/organizations/{args.org_id}/limits/{args.name}")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# --- plan handlers --------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def handle_plans_list(client: api_client.ApiClient, _args: Any) -> Any:
|
|
376
|
+
return client.get("/api/v1/admin/plans")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def handle_plans_limits_update(client: api_client.ApiClient, args: Any) -> Any:
|
|
380
|
+
return client.put(f"/api/v1/admin/plans/{args.plan}/limits/{args.name}", json={"value": args.value})
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def handle_plans_limits_delete(client: api_client.ApiClient, args: Any) -> Any:
|
|
384
|
+
return client.delete(f"/api/v1/admin/plans/{args.plan}/limits/{args.name}")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def handle_plans_flags_update(client: api_client.ApiClient, args: Any) -> Any:
|
|
388
|
+
return client.put(
|
|
389
|
+
f"/api/v1/admin/plans/{args.plan}/feature-flags/{args.name}",
|
|
390
|
+
json={"enabled": not args.disabled},
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def handle_plans_flags_delete(client: api_client.ApiClient, args: Any) -> Any:
|
|
395
|
+
return client.delete(f"/api/v1/admin/plans/{args.plan}/feature-flags/{args.name}")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# --- signup handlers ------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def handle_signup_show(client: api_client.ApiClient, _args: Any) -> Any:
|
|
402
|
+
return client.get("/api/v1/admin/signup")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def handle_signup_update(client: api_client.ApiClient, args: Any) -> Any:
|
|
406
|
+
# Send only what was given, so changing one setting leaves the other alone.
|
|
407
|
+
body: dict[str, Any] = {}
|
|
408
|
+
if args.mode is not None:
|
|
409
|
+
body["mode"] = None if args.mode == "installation" else args.mode
|
|
410
|
+
if args.default_plan is not None:
|
|
411
|
+
body["default_plan"] = args.default_plan
|
|
412
|
+
if not body:
|
|
413
|
+
raise SystemExit("cmpnd admin signup update: give --mode, --default-plan, or both")
|
|
414
|
+
return client.patch("/api/v1/admin/signup", json=body)
|
|
415
|
+
|
|
416
|
+
|
|
248
417
|
# --- user handlers --------------------------------------------------------
|
|
249
418
|
|
|
250
419
|
|
|
@@ -9,7 +9,11 @@ from .. import api_client, deprecation, pagination
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _add_list_args(p: ArgumentParser) -> None:
|
|
12
|
-
p.add_argument(
|
|
12
|
+
p.add_argument(
|
|
13
|
+
"--status",
|
|
14
|
+
help="Filter by status (queued, running, completed, failed, cancelled). "
|
|
15
|
+
"queued lists every run waiting for a slot, in the order they will start, without paging",
|
|
16
|
+
)
|
|
13
17
|
p.add_argument("--program", dest="program_name", help="Filter by program name")
|
|
14
18
|
p.add_argument("--optimizer-type", help="Filter by optimizer type")
|
|
15
19
|
p.add_argument("--min-score", type=float, help="Minimum best score")
|
|
@@ -77,6 +81,13 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
77
81
|
show.add_argument("id", help="Optimization run ID (UUID)")
|
|
78
82
|
show.set_defaults(func=handle_show)
|
|
79
83
|
|
|
84
|
+
cancel = sub.add_parser(
|
|
85
|
+
"cancel",
|
|
86
|
+
help="Cancel an open optimization run. A hosted run stops searching within seconds",
|
|
87
|
+
)
|
|
88
|
+
cancel.add_argument("id", help="Optimization run ID (op_...)")
|
|
89
|
+
cancel.set_defaults(func=handle_cancel)
|
|
90
|
+
|
|
80
91
|
delete = sub.add_parser("delete", help="Delete an optimization run and everything recorded under it")
|
|
81
92
|
delete.add_argument("id", help="Optimization run ID (op_...)")
|
|
82
93
|
delete.set_defaults(func=handle_delete)
|
|
@@ -160,6 +171,8 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
160
171
|
|
|
161
172
|
|
|
162
173
|
def handle_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
174
|
+
if getattr(args, "status", None) == "queued":
|
|
175
|
+
return _list_queued(client, args)
|
|
163
176
|
# This route paginates by page/per_page; the shared helper maps the
|
|
164
177
|
# user-facing --limit/--offset onto that pair.
|
|
165
178
|
body: dict[str, Any] = pagination.wire(args, "per_page")
|
|
@@ -175,10 +188,31 @@ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
|
175
188
|
return client.post("/api/v1/optimizations/search", json=body)
|
|
176
189
|
|
|
177
190
|
|
|
191
|
+
def _list_queued(client: api_client.ApiClient, args: Any) -> Any:
|
|
192
|
+
# A queued run waits for a slot and has no run record until it starts, so the run
|
|
193
|
+
# search cannot find it. Its own route lists the org's waiting runs in line order.
|
|
194
|
+
# The list is short and unfiltered, so the search filters do not apply.
|
|
195
|
+
search_only = [("program_name", "--program"), ("optimizer_type", "--optimizer-type"), ("min_score", "--min-score")]
|
|
196
|
+
for attr, flag in search_only:
|
|
197
|
+
if getattr(args, attr, None) is not None:
|
|
198
|
+
raise api_client.CliError(f"{flag} does not apply to --status queued, which lists every waiting run")
|
|
199
|
+
return client.get("/api/v1/optimizations/queued")
|
|
200
|
+
|
|
201
|
+
|
|
178
202
|
def handle_show(client: api_client.ApiClient, args: Any) -> Any:
|
|
179
203
|
return client.get(f"/api/v1/optimizations/{args.id}")
|
|
180
204
|
|
|
181
205
|
|
|
206
|
+
def handle_cancel(client: api_client.ApiClient, args: Any) -> Any:
|
|
207
|
+
"""Cancel an open run and return its id and status.
|
|
208
|
+
|
|
209
|
+
The run's record stays, marked cancelled. Serve stops a hosted run's search
|
|
210
|
+
on its next watcher tick, which also frees the run's place among its org's
|
|
211
|
+
running optimizations. A run that already ended answers 409 with its status.
|
|
212
|
+
"""
|
|
213
|
+
return client.post(f"/api/v1/optimizations/{args.id}/cancel")
|
|
214
|
+
|
|
215
|
+
|
|
182
216
|
def handle_delete(client: api_client.ApiClient, args: Any) -> Any:
|
|
183
217
|
# 204 No Content → delete() returns None; synthesize a confirmation.
|
|
184
218
|
client.delete(f"/api/v1/optimizations/{args.id}")
|
|
@@ -1467,6 +1467,11 @@ def _upload_bundle(
|
|
|
1467
1467
|
raise OptimizeError(f"failed to {what}: HTTP {resp.status_code}: {body}")
|
|
1468
1468
|
|
|
1469
1469
|
|
|
1470
|
+
# The `queued_reason` serve gives a run it holds because the run's org already runs as
|
|
1471
|
+
# many hosted optimizations as its plan allows.
|
|
1472
|
+
_QUEUED_FOR_ORG_LIMIT = "org_concurrency_limit"
|
|
1473
|
+
|
|
1474
|
+
|
|
1470
1475
|
def _poll_until_terminal(
|
|
1471
1476
|
client: httpx.Client,
|
|
1472
1477
|
endpoint: str,
|
|
@@ -1495,6 +1500,9 @@ def _poll_until_terminal(
|
|
|
1495
1500
|
# The last status the worker reported, so a timeout discovered at the wire can
|
|
1496
1501
|
# say what the run was doing when the clock ran out.
|
|
1497
1502
|
last_status: object = None
|
|
1503
|
+
# Whether the caller has been told the run waits on its org's cap. Once is enough,
|
|
1504
|
+
# because the poll repeats the same answer until a place frees.
|
|
1505
|
+
warned_org_limit = False
|
|
1498
1506
|
|
|
1499
1507
|
def _tolerate(resp: httpx.Response) -> bool:
|
|
1500
1508
|
nonlocal first_missing
|
|
@@ -1556,6 +1564,16 @@ def _poll_until_terminal(
|
|
|
1556
1564
|
return status_data
|
|
1557
1565
|
if status == "failed":
|
|
1558
1566
|
raise OptimizeFailed(status_data.get("error"), optimization_id=optimization_id)
|
|
1567
|
+
if status == "queued" and status_data.get("queued_reason") == _QUEUED_FOR_ORG_LIMIT and not warned_org_limit:
|
|
1568
|
+
warned_org_limit = True
|
|
1569
|
+
logger.warning(
|
|
1570
|
+
"optimization %s is waiting: your org's plan allows %s hosted optimization(s) at once "
|
|
1571
|
+
"and %s are running. It starts when one finishes. To free a place now, run "
|
|
1572
|
+
"`cmpnd optimizations list --status running` and `cmpnd optimizations cancel <id>`.",
|
|
1573
|
+
optimization_id,
|
|
1574
|
+
status_data.get("concurrency_limit"),
|
|
1575
|
+
status_data.get("running_optimizations"),
|
|
1576
|
+
)
|
|
1559
1577
|
if time.monotonic() - start > timeout:
|
|
1560
1578
|
raise _ran_out_of_time(status)
|
|
1561
1579
|
# Clipped like a backoff, for the same reason: this wait is inside the
|
|
@@ -526,6 +526,30 @@ takes named models through its `lms` argument. An authored file's `lms`
|
|
|
526
526
|
binding cannot be combined with `lms` in the parent call or `config["lms"]`.
|
|
527
527
|
The CLI has no named-model flag.
|
|
528
528
|
|
|
529
|
+
### Cancel a run
|
|
530
|
+
|
|
531
|
+
`cmpnd optimizations cancel RUN_ID` stops an open run and keeps its record,
|
|
532
|
+
marked `cancelled`. A hosted run stops searching within a few seconds, and its
|
|
533
|
+
place among the org's running optimizations frees at the same moment. The web
|
|
534
|
+
app's Cancel run button does the same thing. A run that already ended answers
|
|
535
|
+
with its status and changes nothing. A poll of a cancelled run reports
|
|
536
|
+
`status: failed` with `settlement: cancelled`, so a released SDK's wait loop
|
|
537
|
+
ends.
|
|
538
|
+
|
|
539
|
+
An org's plan may cap how many hosted optimizations run at once. A run started
|
|
540
|
+
over the cap waits in serve's queue and starts when one of the org's runs
|
|
541
|
+
finishes. Its poll answers `status: queued` with `queued_reason:
|
|
542
|
+
org_concurrency_limit`, and the SDK logs one warning that names the cap and
|
|
543
|
+
the commands that list and cancel running runs. A plan may also limit how many
|
|
544
|
+
runs wait. A run that would wait past it is refused with a 409 that names the
|
|
545
|
+
limit and the queued runs' ids.
|
|
546
|
+
|
|
547
|
+
`cmpnd optimizations list --status queued` lists the org's queued runs in the
|
|
548
|
+
order they will start, and `cmpnd optimizations cancel <id>` cancels one. A
|
|
549
|
+
queued run has no run record until it starts, so the other statuses never show
|
|
550
|
+
it. The web app shows the same list on its queued optimizations page, which the
|
|
551
|
+
optimizations page links to while runs wait.
|
|
552
|
+
|
|
529
553
|
## Platform administration
|
|
530
554
|
|
|
531
555
|
Platform-operator commands (`cmpnd admin …`) appear only when
|
|
@@ -556,6 +580,33 @@ EMAIL` withdraws the open owner invites sent to that address, and the links
|
|
|
556
580
|
stop working. `cmpnd admin org show` lists pending addresses under
|
|
557
581
|
`seed.pending_owner_emails`.
|
|
558
582
|
|
|
583
|
+
### Plans, limits and signups
|
|
584
|
+
|
|
585
|
+
Every org has a plan, `free`, `pro` or `enterprise`, and each plan sets
|
|
586
|
+
default limits and feature flags. An org's own override beats its plan.
|
|
587
|
+
`cmpnd admin org list` shows each org's plan, and `--plan free` keeps only the
|
|
588
|
+
orgs on Free. `cmpnd admin org plan show ORG_ID` shows the org's plan and every
|
|
589
|
+
limit, with the layer that set it. `cmpnd admin org plan update ORG_ID PLAN` moves the
|
|
590
|
+
org. `cmpnd admin org overrides update ORG_ID NAME VALUE` overrides one limit,
|
|
591
|
+
and a value of `none` lifts the plan's limit for that org.
|
|
592
|
+
`cmpnd admin org overrides delete ORG_ID NAME` deletes the override, so the org
|
|
593
|
+
follows its plan again.
|
|
594
|
+
|
|
595
|
+
`cmpnd admin plans list` shows every plan's limits and feature flags.
|
|
596
|
+
`cmpnd admin plans limits update PLAN NAME VALUE` and `limits delete PLAN NAME`
|
|
597
|
+
change a plan's default for every org on it. `cmpnd admin plans flags update
|
|
598
|
+
PLAN NAME [--disabled]` pins a flag for the plan, and `flags delete` hands the
|
|
599
|
+
plan back to the global default. A plan's flag must already exist, so create
|
|
600
|
+
it first with `cmpnd admin feature-flags set`.
|
|
601
|
+
|
|
602
|
+
`cmpnd admin signup show` reports who may sign up and the plan a new account's
|
|
603
|
+
org lands on. `cmpnd admin signup update --mode closed|invite|open` overrides
|
|
604
|
+
the installation's `CMPND_SERVE_SIGNUP_MODE`, and `--mode installation` follows
|
|
605
|
+
it again. `--default-plan PLAN` sets the plan for new orgs. Changing the mode
|
|
606
|
+
needs a key with `admin:users:provision`, and changing the plan one with
|
|
607
|
+
`admin:orgs:write`. The operator pages at `/ui/admin/plans`,
|
|
608
|
+
`/ui/admin/feature_flags` and `/ui/admin/signup` write the same settings.
|
|
609
|
+
|
|
559
610
|
### Preparing an org for a customer
|
|
560
611
|
|
|
561
612
|
This procedure keeps every write under the operator's own identity. The
|
|
@@ -786,7 +786,7 @@ The id rides every body that can declare a program, beside `program_signature_ha
|
|
|
786
786
|
| Body | Where | Built by |
|
|
787
787
|
|---|---|---|
|
|
788
788
|
| trace start, single and batch | top-level `program_id`, and `started_fact.program_id` | `TraceBuilder.to_start_dict()` / `to_api_dict()` |
|
|
789
|
-
| deployment create | top-level `program_id`,
|
|
789
|
+
| deployment create | top-level `program_id`, when the deployment resolves one | `deployment.deploy()` / `deploy_bundle()` |
|
|
790
790
|
| optimize step | top-level `program_id`, on the steps that optimize the caller's deployment | `optimization._initiate_step()` |
|
|
791
791
|
| optimization-run upsert | top-level `program_id` | `OptimizationTracker.to_api_dict()` |
|
|
792
792
|
|
|
@@ -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.54.0"
|
|
9
9
|
description = "DSPy observability and deployment SDK for cmpnd"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.11"
|
|
@@ -85,6 +85,19 @@ class TestAdminOrg:
|
|
|
85
85
|
mock_client.get.assert_called_once_with("/api/v1/admin/organizations")
|
|
86
86
|
assert result == {"organizations": []}
|
|
87
87
|
|
|
88
|
+
def test_list_filters_by_plan(self, mock_client):
|
|
89
|
+
mock_client.get.return_value = {"organizations": [{"id": "o_1", "plan": "free"}]}
|
|
90
|
+
args = _parse("org", "list", "--plan", "free")
|
|
91
|
+
result = args.func(mock_client, args)
|
|
92
|
+
mock_client.get.assert_called_once_with("/api/v1/admin/organizations", params={"plan": "free"})
|
|
93
|
+
assert result["organizations"][0]["plan"] == "free"
|
|
94
|
+
|
|
95
|
+
def test_list_sends_any_plan_name_for_serve_to_check(self, mock_client):
|
|
96
|
+
mock_client.get.return_value = {"organizations": []}
|
|
97
|
+
args = _parse("org", "list", "--plan", "platinum")
|
|
98
|
+
args.func(mock_client, args)
|
|
99
|
+
mock_client.get.assert_called_once_with("/api/v1/admin/organizations", params={"plan": "platinum"})
|
|
100
|
+
|
|
88
101
|
def test_get_calls_endpoint(self, mock_client):
|
|
89
102
|
mock_client.get.return_value = {"id": "o_1", "name": "Acme"}
|
|
90
103
|
result = admin.handle_org_show(mock_client, Namespace(org_id="o_1"))
|
|
@@ -260,3 +273,125 @@ class TestAdminFeatureFlags:
|
|
|
260
273
|
result = admin.handle_feature_flags_set(mock_client, Namespace(name="beta", disabled=True))
|
|
261
274
|
mock_client.put.assert_called_once_with("/api/v1/admin/feature-flags/beta", json={"enabled": False})
|
|
262
275
|
assert result["enabled"] is False
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _parse(*argv: str) -> Namespace:
|
|
279
|
+
from argparse import ArgumentParser
|
|
280
|
+
|
|
281
|
+
parser = ArgumentParser()
|
|
282
|
+
admin.register(parser.add_subparsers())
|
|
283
|
+
return parser.parse_args(["admin", *argv])
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class TestAdminOrgPlan:
|
|
287
|
+
def test_plan_shows_the_orgs_plan(self, mock_client):
|
|
288
|
+
mock_client.get.return_value = {"org_id": "o_1", "plan": "free", "limits": []}
|
|
289
|
+
args = _parse("org", "plan", "show", "o_1")
|
|
290
|
+
result = args.func(mock_client, args)
|
|
291
|
+
mock_client.get.assert_called_once_with("/api/v1/admin/organizations/o_1/plan")
|
|
292
|
+
assert result["plan"] == "free"
|
|
293
|
+
|
|
294
|
+
def test_set_plan_puts_the_plan(self, mock_client):
|
|
295
|
+
args = _parse("org", "plan", "update", "o_1", "pro")
|
|
296
|
+
args.func(mock_client, args)
|
|
297
|
+
mock_client.put.assert_called_once_with("/api/v1/admin/organizations/o_1/plan", json={"plan": "pro"})
|
|
298
|
+
|
|
299
|
+
def test_override_limit_sends_a_number(self, mock_client):
|
|
300
|
+
args = _parse("org", "overrides", "update", "o_1", "optimizations.concurrent", "3")
|
|
301
|
+
args.func(mock_client, args)
|
|
302
|
+
mock_client.put.assert_called_once_with(
|
|
303
|
+
"/api/v1/admin/organizations/o_1/limits/optimizations.concurrent", json={"value": 3}
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def test_override_limit_none_lifts_the_limit(self, mock_client):
|
|
307
|
+
# "none" sends null, which lifts the plan's limit for this org. That is a
|
|
308
|
+
# different state from clearing the override.
|
|
309
|
+
args = _parse("org", "overrides", "update", "o_1", "optimizations.concurrent", "none")
|
|
310
|
+
args.func(mock_client, args)
|
|
311
|
+
mock_client.put.assert_called_once_with(
|
|
312
|
+
"/api/v1/admin/organizations/o_1/limits/optimizations.concurrent", json={"value": None}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def test_override_limit_rejects_a_word_that_is_not_none(self):
|
|
316
|
+
import pytest
|
|
317
|
+
|
|
318
|
+
with pytest.raises(SystemExit):
|
|
319
|
+
_parse("org", "overrides", "update", "o_1", "optimizations.concurrent", "lots")
|
|
320
|
+
|
|
321
|
+
def test_clear_limit_override_deletes_it(self, mock_client):
|
|
322
|
+
args = _parse("org", "overrides", "delete", "o_1", "optimizations.concurrent")
|
|
323
|
+
args.func(mock_client, args)
|
|
324
|
+
mock_client.delete.assert_called_once_with("/api/v1/admin/organizations/o_1/limits/optimizations.concurrent")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class TestAdminPlans:
|
|
328
|
+
def test_list(self, mock_client):
|
|
329
|
+
mock_client.get.return_value = {"plans": []}
|
|
330
|
+
args = _parse("plans", "list")
|
|
331
|
+
assert args.func(mock_client, args) == {"plans": []}
|
|
332
|
+
mock_client.get.assert_called_once_with("/api/v1/admin/plans")
|
|
333
|
+
|
|
334
|
+
def test_set_limit_and_none(self, mock_client):
|
|
335
|
+
args = _parse("plans", "limits", "update", "pro", "optimizations.concurrent", "5")
|
|
336
|
+
args.func(mock_client, args)
|
|
337
|
+
mock_client.put.assert_called_with("/api/v1/admin/plans/pro/limits/optimizations.concurrent", json={"value": 5})
|
|
338
|
+
args = _parse("plans", "limits", "update", "free", "optimizations.concurrent", "none")
|
|
339
|
+
args.func(mock_client, args)
|
|
340
|
+
mock_client.put.assert_called_with(
|
|
341
|
+
"/api/v1/admin/plans/free/limits/optimizations.concurrent", json={"value": None}
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def test_clear_limit(self, mock_client):
|
|
345
|
+
args = _parse("plans", "limits", "delete", "pro", "optimizations.concurrent")
|
|
346
|
+
args.func(mock_client, args)
|
|
347
|
+
mock_client.delete.assert_called_once_with("/api/v1/admin/plans/pro/limits/optimizations.concurrent")
|
|
348
|
+
|
|
349
|
+
def test_set_flag_enables_by_default_and_disables_with_the_flag(self, mock_client):
|
|
350
|
+
args = _parse("plans", "flags", "update", "free", "beta")
|
|
351
|
+
args.func(mock_client, args)
|
|
352
|
+
mock_client.put.assert_called_with("/api/v1/admin/plans/free/feature-flags/beta", json={"enabled": True})
|
|
353
|
+
args = _parse("plans", "flags", "update", "free", "beta", "--disabled")
|
|
354
|
+
args.func(mock_client, args)
|
|
355
|
+
mock_client.put.assert_called_with("/api/v1/admin/plans/free/feature-flags/beta", json={"enabled": False})
|
|
356
|
+
|
|
357
|
+
def test_clear_flag(self, mock_client):
|
|
358
|
+
args = _parse("plans", "flags", "delete", "free", "beta")
|
|
359
|
+
args.func(mock_client, args)
|
|
360
|
+
mock_client.delete.assert_called_once_with("/api/v1/admin/plans/free/feature-flags/beta")
|
|
361
|
+
|
|
362
|
+
def test_plan_names_are_serves_to_check(self, mock_client):
|
|
363
|
+
args = _parse("plans", "limits", "update", "platinum", "optimizations.concurrent", "5")
|
|
364
|
+
args.func(mock_client, args)
|
|
365
|
+
mock_client.put.assert_called_once_with(
|
|
366
|
+
"/api/v1/admin/plans/platinum/limits/optimizations.concurrent", json={"value": 5}
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class TestAdminSignup:
|
|
371
|
+
def test_show(self, mock_client):
|
|
372
|
+
mock_client.get.return_value = {"mode": "invite", "mode_source": "installation"}
|
|
373
|
+
args = _parse("signup", "show")
|
|
374
|
+
assert args.func(mock_client, args)["mode"] == "invite"
|
|
375
|
+
mock_client.get.assert_called_once_with("/api/v1/admin/signup")
|
|
376
|
+
|
|
377
|
+
def test_set_sends_only_what_was_given(self, mock_client):
|
|
378
|
+
args = _parse("signup", "update", "--mode", "closed")
|
|
379
|
+
args.func(mock_client, args)
|
|
380
|
+
mock_client.patch.assert_called_with("/api/v1/admin/signup", json={"mode": "closed"})
|
|
381
|
+
args = _parse("signup", "update", "--default-plan", "free")
|
|
382
|
+
args.func(mock_client, args)
|
|
383
|
+
mock_client.patch.assert_called_with("/api/v1/admin/signup", json={"default_plan": "free"})
|
|
384
|
+
|
|
385
|
+
def test_installation_mode_sends_null(self, mock_client):
|
|
386
|
+
# null hands signups back to the installation's CMPND_SERVE_SIGNUP_MODE.
|
|
387
|
+
args = _parse("signup", "update", "--mode", "installation", "--default-plan", "pro")
|
|
388
|
+
args.func(mock_client, args)
|
|
389
|
+
mock_client.patch.assert_called_once_with("/api/v1/admin/signup", json={"mode": None, "default_plan": "pro"})
|
|
390
|
+
|
|
391
|
+
def test_set_with_nothing_to_change_exits(self, mock_client):
|
|
392
|
+
import pytest
|
|
393
|
+
|
|
394
|
+
args = _parse("signup", "update")
|
|
395
|
+
with pytest.raises(SystemExit):
|
|
396
|
+
args.func(mock_client, args)
|
|
397
|
+
mock_client.patch.assert_not_called()
|
|
@@ -9,6 +9,7 @@ import pytest
|
|
|
9
9
|
from cmpnd.cli.api_client import CliError
|
|
10
10
|
from cmpnd.cli.commands import optimizations
|
|
11
11
|
from cmpnd.cli.commands.optimizations import (
|
|
12
|
+
handle_cancel,
|
|
12
13
|
handle_candidates,
|
|
13
14
|
handle_compare,
|
|
14
15
|
handle_compare_failed_examples,
|
|
@@ -58,6 +59,26 @@ class TestOptimizationsList:
|
|
|
58
59
|
assert body["status"] == "completed"
|
|
59
60
|
assert body["min_score"] == 0.8
|
|
60
61
|
|
|
62
|
+
def test_queued_lists_the_waiting_runs(self, mock_client):
|
|
63
|
+
# A queued run has no run record yet, so the run search cannot find it. The
|
|
64
|
+
# queued status reads the org's waiting runs from their own route.
|
|
65
|
+
mock_client.get.return_value = {"optimizations": [{"optimization_id": "op_1", "position": 1}]}
|
|
66
|
+
args = argparse.Namespace(
|
|
67
|
+
status="queued", program_name=None, optimizer_type=None, min_score=None, limit=50, offset=0
|
|
68
|
+
)
|
|
69
|
+
result = handle_list(mock_client, args)
|
|
70
|
+
mock_client.get.assert_called_once_with("/api/v1/optimizations/queued")
|
|
71
|
+
mock_client.post.assert_not_called()
|
|
72
|
+
assert result["optimizations"][0]["optimization_id"] == "op_1"
|
|
73
|
+
|
|
74
|
+
def test_queued_refuses_filters_it_cannot_apply(self, mock_client):
|
|
75
|
+
args = argparse.Namespace(
|
|
76
|
+
status="queued", program_name="RAG", optimizer_type=None, min_score=None, limit=50, offset=0
|
|
77
|
+
)
|
|
78
|
+
with pytest.raises(CliError, match="--status queued"):
|
|
79
|
+
handle_list(mock_client, args)
|
|
80
|
+
mock_client.get.assert_not_called()
|
|
81
|
+
|
|
61
82
|
def test_deprecated_search_verb_dispatches_and_is_marked(self, mock_client):
|
|
62
83
|
# The warning is the dispatcher's job (see test_main.py); the wiring is this one's.
|
|
63
84
|
parser = _build_parser()
|
|
@@ -152,6 +173,20 @@ class TestOptimizationsDatasetAnalysis:
|
|
|
152
173
|
mock_client.get.assert_called_once_with("/api/v1/optimizations/op_abc/dataset-analysis")
|
|
153
174
|
|
|
154
175
|
|
|
176
|
+
class TestOptimizationsCancel:
|
|
177
|
+
def test_posts_to_the_cancel_route(self, mock_client):
|
|
178
|
+
mock_client.post.return_value = {"optimization_id": "op_abc", "status": "cancelled"}
|
|
179
|
+
args = argparse.Namespace(id="op_abc")
|
|
180
|
+
result = handle_cancel(mock_client, args)
|
|
181
|
+
mock_client.post.assert_called_once_with("/api/v1/optimizations/op_abc/cancel")
|
|
182
|
+
assert result == {"optimization_id": "op_abc", "status": "cancelled"}
|
|
183
|
+
|
|
184
|
+
def test_parses_from_the_command_line(self):
|
|
185
|
+
args = _build_parser().parse_args(["optimizations", "cancel", "op_abc"])
|
|
186
|
+
assert args.func is handle_cancel
|
|
187
|
+
assert args.id == "op_abc"
|
|
188
|
+
|
|
189
|
+
|
|
155
190
|
class TestOptimizationsDelete:
|
|
156
191
|
def test_calls_correct_endpoint(self, mock_client):
|
|
157
192
|
mock_client.delete.return_value = None
|