ellf-cli 6.0.4__tar.gz → 6.0.7__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.
- {ellf_cli-6.0.4/ellf_cli.egg-info → ellf_cli-6.0.7}/PKG-INFO +1 -1
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/README.md +105 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/about.json +1 -1
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/__init__.py +2 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/_recipe_subcommand.py +31 -1
- ellf_cli-6.0.7/ellf_cli/commands/services.py +390 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/config.py +8 -2
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf.json +604 -1
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/query.py +63 -6
- {ellf_cli-6.0.4 → ellf_cli-6.0.7/ellf_cli.egg-info}/PKG-INFO +1 -1
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli.egg-info/SOURCES.txt +1 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/LICENSE +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/MANIFEST.in +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/__init__.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/__main__.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/about.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/appdirs.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/auth.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/cli.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/cloud/__init__.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/cloud/gcp.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/cluster_config.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/_cluster_select.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/_org_select.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/_recipe_file.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/_state.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/actions.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/agents.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/assets.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/auth.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/clusters.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/config.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/datasets.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/files/__init__.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/files/cp.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/files/ls.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/files/rm.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/files/rsync.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/files/stats.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/general.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/import_export.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/infra/__init__.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/infra/_helpers.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/infra/deploy.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/infra/init_values.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/infra/provision.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/infra/register.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/infra/setup.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/infra/start.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/infra/terraform.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/infra/tls.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/jobs.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/packages.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/paths.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/plans.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/projects.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/publish_code.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/publish_data.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/recipes.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/secrets.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/support.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/tasks.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/commands/todos.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/.claude-plugin/plugin.json +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/.gitignore +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skill_variants.json +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/references/annotation_audit.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/references/builtin_ellf_annotation_recipes.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/annotation_audit.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/builtin_ellf_annotation_recipes.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/builtin_prodigy_recipes.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-ask/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-handoff/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/references/annotation_metrics.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/references/training_monitoring.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/references/annotation_metrics.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/references/training_monitoring.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/scripts/check_training.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-ops.assistant/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-ops.coding/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-ops.coding/references/data_infra_cli.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-ops.coding/scripts/run_job.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-patterns/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-patterns/references/pattern_strategies.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_action_recipe.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_agent_recipe.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_blocks_ui.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_correct.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_custom_ui.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_manual.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_pages_ui.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_routing.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_task_recipe.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_teach.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/builtin_recipes.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/ellf_recipe_sdk.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/lint_recipe.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/prodigy_recipe_api.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/template_index.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-project.assistant/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/consulting_patterns.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/explosion_strategy.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/prodigy_llm_bot.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-project.coding/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/consulting_patterns.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/explosion_strategy.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/prodigy_llm_bot.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-support.assistant/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-support.coding/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-todo/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.assistant/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/diagnostics.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/evaluation_guide.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/model_selection.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/training_paradigms.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/workflow.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.coding/SKILL.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_advanced.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_architectures.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_training.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/diagnostics.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/evaluation_guide.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/experiment_patterns.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/model_selection.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/training_paradigms.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/training_troubleshooting.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/workflow.md +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ellf_skills/skills/ellf-train.coding/scripts/ellf_logger.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/errors.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/helm.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/key_pair.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/main.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/messages.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/recipes_cookiecutter/cookiecutter.json +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/.gitignore +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/README.md.tmpl +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/requirements-dev.in +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/requirements.in +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/setup.py.tmpl +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/__init__.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/about.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/recipes/__init__.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/recipes/example_task.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/testing/__init__.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ty.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/ui.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/url.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli/util.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli.egg-info/dependency_links.txt +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli.egg-info/entry_points.txt +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli.egg-info/not-zip-safe +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli.egg-info/requires.txt +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/ellf_cli.egg-info/top_level.txt +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/pyproject.toml +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/setup.cfg +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/setup.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_appdirs.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_auth.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_config.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_errors.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_files_cp.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_files_cp_helpers.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_info.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_invalid_secrets.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_key_pair.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_login.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_logout.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_main.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_org_select.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_plans.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_projects.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_query.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_recipe_file.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_recipes.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_state.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_support.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_ty.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_ui.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_ui_extras.py +0 -0
- {ellf_cli-6.0.4 → ellf_cli-6.0.7}/tests/test_util.py +0 -0
|
@@ -1248,6 +1248,111 @@ Manage TLS certificates for local cluster access. Use --self-signed when you hav
|
|
|
1248
1248
|
| `--namespace` | `str` | Kubernetes namespace | `'ellf'` |
|
|
1249
1249
|
| `--output` | `str` | Output format for trust commands: human, json, shell | `'human'` |
|
|
1250
1250
|
|
|
1251
|
+
### `ellf services`
|
|
1252
|
+
|
|
1253
|
+
#### `ellf services create`
|
|
1254
|
+
|
|
1255
|
+
Create a new service. The available service recipes are fetched from your cluster and are added as dynamic subcommands. You can see more details and available arguments by calling the subcommand with --help, e.g. create [name] --help
|
|
1256
|
+
|
|
1257
|
+
| Argument | Type | Description | Default |
|
|
1258
|
+
| --- | --- | --- | --- |
|
|
1259
|
+
| `--exists-ok` | `bool` | Don't raise an error if it exists | `False` |
|
|
1260
|
+
| `--no-start` | `bool` | Don't start {noun} after creation | `False` |
|
|
1261
|
+
| `--no-wait` | `bool` | Don't wait for the service to be ready after starting | `False` |
|
|
1262
|
+
| `--help`, `-h` | `bool` | Show help message | `False` |
|
|
1263
|
+
| `_extra` | `str` | | `[]` |
|
|
1264
|
+
|
|
1265
|
+
#### `ellf services list`
|
|
1266
|
+
|
|
1267
|
+
List the services on the cluster.
|
|
1268
|
+
|
|
1269
|
+
| Argument | Type | Description | Default |
|
|
1270
|
+
| --- | --- | --- | --- |
|
|
1271
|
+
| `--select` | `list[str]` | Comma-separated fields to select and show in output. Available: ['id', 'created', 'updated', 'cluster_id', 'name', 'project_id', 'job_type', 'plan', 'cli_command', 'state'] | `['id', 'name', 'state', 'project_name']` |
|
|
1272
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1273
|
+
|
|
1274
|
+
#### `ellf services info`
|
|
1275
|
+
|
|
1276
|
+
Print information about a service on the cluster.
|
|
1277
|
+
|
|
1278
|
+
| Argument | Type | Description | Default |
|
|
1279
|
+
| --- | --- | --- | --- |
|
|
1280
|
+
| `name_or_id` | `Union[str, UUID]` | Name or ID of the service | |
|
|
1281
|
+
| `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
|
|
1282
|
+
| `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
|
|
1283
|
+
| `--select` | `list[str]` | Comma-separated fields to select and show in output. Available: ['id', 'created', 'updated', 'cluster_id', 'name', 'project_id', 'job_type', 'plan', 'cli_command', 'recipe_name', 'recipe_title', 'project_name', 'error', 'last_execution_id', 'url', 'url_ui', 'url_logs', 'created_by_user'] | `None` |
|
|
1284
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1285
|
+
|
|
1286
|
+
#### `ellf services logs`
|
|
1287
|
+
|
|
1288
|
+
Get logs for a service on the cluster.
|
|
1289
|
+
|
|
1290
|
+
| Argument | Type | Description | Default |
|
|
1291
|
+
| --- | --- | --- | --- |
|
|
1292
|
+
| `name_or_id` | `str` | Name or ID of the service (or the last service if not set) | `None` |
|
|
1293
|
+
| `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
|
|
1294
|
+
| `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
|
|
1295
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1296
|
+
| `--errors` | `bool` | Show structured error information instead of full logs | `False` |
|
|
1297
|
+
| `--query` | `str` | Filter log lines matching this text (requires Loki) | `None` |
|
|
1298
|
+
|
|
1299
|
+
#### `ellf services start`
|
|
1300
|
+
|
|
1301
|
+
Start a service on the cluster.
|
|
1302
|
+
|
|
1303
|
+
| Argument | Type | Description | Default |
|
|
1304
|
+
| --- | --- | --- | --- |
|
|
1305
|
+
| `name_or_id` | `str` | Name or ID of the service (or the last service if not set) | `None` |
|
|
1306
|
+
| `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
|
|
1307
|
+
| `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
|
|
1308
|
+
| `--worker-class` | `str` | Worker class to launch the service on. Generally one of: ['medium', 'large', 'gpu'] | `None` |
|
|
1309
|
+
| `--no-wait` | `bool` | Don't wait for the service to be ready after starting | `False` |
|
|
1310
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1311
|
+
|
|
1312
|
+
#### `ellf services stop`
|
|
1313
|
+
|
|
1314
|
+
Stop a service on the cluster.
|
|
1315
|
+
|
|
1316
|
+
| Argument | Type | Description | Default |
|
|
1317
|
+
| --- | --- | --- | --- |
|
|
1318
|
+
| `name_or_id` | `str` | Name or ID of the service (or the last service if not set) | `None` |
|
|
1319
|
+
| `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
|
|
1320
|
+
| `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
|
|
1321
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1322
|
+
|
|
1323
|
+
#### `ellf services delete`
|
|
1324
|
+
|
|
1325
|
+
Delete a service by name or ID.
|
|
1326
|
+
|
|
1327
|
+
| Argument | Type | Description | Default |
|
|
1328
|
+
| --- | --- | --- | --- |
|
|
1329
|
+
| `name_or_id` | `Union[str, UUID]` | Name or ID of the service | |
|
|
1330
|
+
| `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
|
|
1331
|
+
| `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
|
|
1332
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1333
|
+
|
|
1334
|
+
#### `ellf services url`
|
|
1335
|
+
|
|
1336
|
+
Print the bearer-token-authenticated URL for a service. Mints a fresh 90-day token via ``POST /v1/service/issue-token`` and formats it together with the service's public URL so the result can be dropped into an MCP client config or curl invocation: curl -H "Authorization: Bearer <token>" <url> Use ``services rotate-token`` to mint a new token without printing the URL again.
|
|
1337
|
+
|
|
1338
|
+
| Argument | Type | Description | Default |
|
|
1339
|
+
| --- | --- | --- | --- |
|
|
1340
|
+
| `name_or_id` | `Union[str, UUID]` | Name or ID of the service | |
|
|
1341
|
+
| `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
|
|
1342
|
+
| `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
|
|
1343
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1344
|
+
|
|
1345
|
+
#### `ellf services rotate-token`
|
|
1346
|
+
|
|
1347
|
+
Mint a fresh 90-day token for an existing service. Issuing a new token does not invalidate previous ones -- they remain valid until their own expiration. Use this when the previous token is leaked, or when you want a longer remaining window for an MCP client config.
|
|
1348
|
+
|
|
1349
|
+
| Argument | Type | Description | Default |
|
|
1350
|
+
| --- | --- | --- | --- |
|
|
1351
|
+
| `name_or_id` | `Union[str, UUID]` | Name or ID of the service | |
|
|
1352
|
+
| `project_id` | `str` | Name, slug, or ID of service's project (or the last project if not set) | `None` |
|
|
1353
|
+
| `cluster_id` | `str` | Name, slug, or ID of the cluster to search for service name (or the last cluster if not set) | `None` |
|
|
1354
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1355
|
+
|
|
1251
1356
|
### `ellf support`
|
|
1252
1357
|
|
|
1253
1358
|
#### `ellf support create`
|
|
@@ -25,6 +25,7 @@ from ellf_pam_sdk.models import (
|
|
|
25
25
|
RecipeDetail,
|
|
26
26
|
RecipeListingLatest,
|
|
27
27
|
RecipePlanCreating,
|
|
28
|
+
ServiceCreating,
|
|
28
29
|
TaskCreating,
|
|
29
30
|
)
|
|
30
31
|
from ellf_pam_sdk.recipe_utils import (
|
|
@@ -53,7 +54,7 @@ def get_converter(arg_type: type) -> ConverterType | None:
|
|
|
53
54
|
def _get_noun(recipe: RecipeDetail) -> str:
|
|
54
55
|
"""Derive a human-readable noun from a recipe's job_type or is_action flag."""
|
|
55
56
|
jt = getattr(recipe, "job_type", None)
|
|
56
|
-
if jt in ("task", "action", "agent"):
|
|
57
|
+
if jt in ("task", "action", "agent", "service"):
|
|
57
58
|
return jt
|
|
58
59
|
return "action" if recipe.is_action else "task"
|
|
59
60
|
|
|
@@ -135,6 +136,10 @@ def create_from_recipe(
|
|
|
135
136
|
job_id = _create_agent(
|
|
136
137
|
auth.client, task_or_action_name, project_id, plan, exists_ok=exists_ok
|
|
137
138
|
)
|
|
139
|
+
elif jt == "service":
|
|
140
|
+
job_id = _create_service(
|
|
141
|
+
auth.client, task_or_action_name, project_id, plan, exists_ok=exists_ok
|
|
142
|
+
)
|
|
138
143
|
elif recipe.is_action:
|
|
139
144
|
job_id = _create_action(
|
|
140
145
|
auth.client, task_or_action_name, project_id, plan, exists_ok=exists_ok
|
|
@@ -381,6 +386,31 @@ def _create_agent(
|
|
|
381
386
|
return JobID(id=agent.id, job_type=JobType.agent)
|
|
382
387
|
|
|
383
388
|
|
|
389
|
+
def _create_service(
|
|
390
|
+
client: Client,
|
|
391
|
+
name: str | None,
|
|
392
|
+
project_id: UUID,
|
|
393
|
+
plan: RecipePlanCreating,
|
|
394
|
+
exists_ok: bool = False,
|
|
395
|
+
) -> JobID:
|
|
396
|
+
body = ServiceCreating(name=name, project_id=project_id, plan=plan)
|
|
397
|
+
try:
|
|
398
|
+
service = client.service.create(body)
|
|
399
|
+
except EllfErrors.ProjectNotFound:
|
|
400
|
+
err = Messages.E013.format(name=project_id)
|
|
401
|
+
raise CLIError(err)
|
|
402
|
+
except EllfErrors.ServiceExists:
|
|
403
|
+
if exists_ok:
|
|
404
|
+
msg.info(Messages.T001.format(noun="service", name=name))
|
|
405
|
+
service = client.service.read(name=name, project_id=project_id)
|
|
406
|
+
return JobID(id=service.id, job_type=JobType.service)
|
|
407
|
+
err = Messages.E014.format(noun="service", name=name, project=project_id)
|
|
408
|
+
raise CLIError(err)
|
|
409
|
+
except EllfErrors.ServiceInvalid:
|
|
410
|
+
raise CLIError(Messages.E004.format(noun="service", name=name))
|
|
411
|
+
return JobID(id=service.id, job_type=JobType.service)
|
|
412
|
+
|
|
413
|
+
|
|
384
414
|
def _uuid7() -> UUID:
|
|
385
415
|
"""Generate a UUIDv7 (time-sortable) as a stdlib uuid.UUID."""
|
|
386
416
|
return UUID(int=uuid_utils.uuid7().int)
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import Optional, Union
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from radicli import Arg
|
|
7
|
+
from wasabi import msg
|
|
8
|
+
|
|
9
|
+
from ellf_pam_sdk.models import ServiceDetail, ServiceSummary
|
|
10
|
+
|
|
11
|
+
from ..cli import cli
|
|
12
|
+
from ..errors import BrokerError, CLIError, EllfError, HTTPXErrors
|
|
13
|
+
from ..messages import Messages
|
|
14
|
+
from ..query import (
|
|
15
|
+
delete_job,
|
|
16
|
+
resolve_recipe,
|
|
17
|
+
resolve_service,
|
|
18
|
+
start_job,
|
|
19
|
+
stop_job,
|
|
20
|
+
)
|
|
21
|
+
from ..ui import (
|
|
22
|
+
print_args_table,
|
|
23
|
+
print_info_table,
|
|
24
|
+
print_logs,
|
|
25
|
+
print_mutation_result,
|
|
26
|
+
print_recipes_help,
|
|
27
|
+
print_table_with_select,
|
|
28
|
+
)
|
|
29
|
+
from ._recipe_subcommand import (
|
|
30
|
+
create_from_recipe,
|
|
31
|
+
create_from_recipe_file,
|
|
32
|
+
request_recipes,
|
|
33
|
+
)
|
|
34
|
+
from ._state import get_auth_state, get_saved_settings
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@cli.subcommand_with_extra(
|
|
38
|
+
"services",
|
|
39
|
+
"create",
|
|
40
|
+
exists_ok=Arg("--exists-ok", help=Messages.exists_ok),
|
|
41
|
+
no_start=Arg("--no-start", help=Messages.no_start),
|
|
42
|
+
no_wait=Arg("--no-wait", help=Messages.no_wait.format(noun="service")),
|
|
43
|
+
_show_help=Arg("--help", "-h", help=Messages.help),
|
|
44
|
+
)
|
|
45
|
+
def create(
|
|
46
|
+
exists_ok: bool = False,
|
|
47
|
+
no_start: bool = False,
|
|
48
|
+
no_wait: bool = False,
|
|
49
|
+
_show_help: bool = False,
|
|
50
|
+
_extra: builtins.list[str] = [],
|
|
51
|
+
) -> UUID | None:
|
|
52
|
+
"""
|
|
53
|
+
Create a new service. The available service recipes are fetched from your
|
|
54
|
+
cluster and are added as dynamic subcommands. You can see more details
|
|
55
|
+
and available arguments by calling the subcommand with --help, e.g. create
|
|
56
|
+
[name] --help
|
|
57
|
+
"""
|
|
58
|
+
auth = get_auth_state()
|
|
59
|
+
settings = get_saved_settings()
|
|
60
|
+
is_file_mode = settings.recipes_file is not None
|
|
61
|
+
try:
|
|
62
|
+
schemas = request_recipes(auth=auth, job_type="service")
|
|
63
|
+
except EllfError as e:
|
|
64
|
+
raise CLIError(Messages.E009, e)
|
|
65
|
+
except HTTPXErrors as e:
|
|
66
|
+
raise CLIError(Messages.E009, e)
|
|
67
|
+
if not _extra:
|
|
68
|
+
print_recipes_help(schemas, "Create a new service", "services create")
|
|
69
|
+
return None
|
|
70
|
+
args = [*_extra]
|
|
71
|
+
name = args.pop(0)
|
|
72
|
+
if name not in schemas:
|
|
73
|
+
opts = f"Available: {', '.join(schemas.keys())}"
|
|
74
|
+
raise CLIError(Messages.E010.format(noun="service", name=name), opts)
|
|
75
|
+
if is_file_mode:
|
|
76
|
+
schema = schemas[name]
|
|
77
|
+
job_spec = create_from_recipe_file(
|
|
78
|
+
schema, args, command="services", show_help=_show_help, job_type="service"
|
|
79
|
+
)
|
|
80
|
+
msg.good(Messages.T002.format(noun="service", name=job_spec.name))
|
|
81
|
+
print_args_table(job_spec.plan.args or {}, job_spec.plan.cli_names or {})
|
|
82
|
+
if not no_start:
|
|
83
|
+
response = auth.broker_client.jobs.start_job(job_spec)
|
|
84
|
+
if response.cluster_change is not None:
|
|
85
|
+
msg.good(Messages.T005.format(noun="service", name=job_spec.job_id))
|
|
86
|
+
elif response.cluster_error is not None:
|
|
87
|
+
raise CLIError(
|
|
88
|
+
Messages.E025.format(noun="service", name=job_spec.job_id),
|
|
89
|
+
response.cluster_error,
|
|
90
|
+
)
|
|
91
|
+
return job_spec.job_id
|
|
92
|
+
schema = resolve_recipe(name, cluster_id=auth.cluster_id)
|
|
93
|
+
service_id, plan = create_from_recipe(
|
|
94
|
+
schema, args, command="services", show_help=_show_help, exists_ok=exists_ok
|
|
95
|
+
)
|
|
96
|
+
msg.good(Messages.T002.format(noun="service", name=service_id))
|
|
97
|
+
print_args_table(plan.args, schema.form_schema.cli_names)
|
|
98
|
+
if not no_start:
|
|
99
|
+
try:
|
|
100
|
+
start(service_id, no_wait=no_wait)
|
|
101
|
+
except Exception:
|
|
102
|
+
auth.client.service.delete(id=service_id)
|
|
103
|
+
raise
|
|
104
|
+
return service_id
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@cli.subcommand(
|
|
108
|
+
"services",
|
|
109
|
+
"list",
|
|
110
|
+
select=Arg(
|
|
111
|
+
"--select",
|
|
112
|
+
help=Messages.select.format(
|
|
113
|
+
opts=[*builtins.list(ServiceSummary.model_fields), "state"]
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
117
|
+
)
|
|
118
|
+
def list(
|
|
119
|
+
select: builtins.list[str] = ["id", "name", "state", "project_name"],
|
|
120
|
+
as_json: bool = False,
|
|
121
|
+
) -> Sequence[ServiceSummary]:
|
|
122
|
+
"""List the services on the cluster."""
|
|
123
|
+
auth = get_auth_state()
|
|
124
|
+
items = builtins.list(auth.client.service.all(page_size=100))
|
|
125
|
+
if "state" in select and items:
|
|
126
|
+
job_ids = [item.id for item in items]
|
|
127
|
+
try:
|
|
128
|
+
statuses = auth.broker_client.jobs.get_batch_status(job_ids).statuses
|
|
129
|
+
except (BrokerError, *HTTPXErrors):
|
|
130
|
+
statuses = {}
|
|
131
|
+
enriched = []
|
|
132
|
+
for item in items:
|
|
133
|
+
d = item.model_dump(include=set(s for s in select if s != "state"))
|
|
134
|
+
status = statuses.get(str(item.id))
|
|
135
|
+
d["state"] = status.state.value if status else "unknown"
|
|
136
|
+
enriched.append(d)
|
|
137
|
+
print_table_with_select(enriched, select=select, as_json=as_json)
|
|
138
|
+
else:
|
|
139
|
+
print_table_with_select(items, select=select, as_json=as_json)
|
|
140
|
+
return items
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@cli.subcommand(
|
|
144
|
+
"services",
|
|
145
|
+
"info",
|
|
146
|
+
name_or_id=Arg(help=Messages.name_or_id.format(noun="service")),
|
|
147
|
+
project_id=Arg(help=Messages.project_id.format(noun="service")),
|
|
148
|
+
cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
|
|
149
|
+
select=Arg(
|
|
150
|
+
"--select",
|
|
151
|
+
help=Messages.select.format(opts=builtins.list(ServiceDetail.model_fields)),
|
|
152
|
+
),
|
|
153
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
154
|
+
)
|
|
155
|
+
def info(
|
|
156
|
+
name_or_id: Union[str, UUID],
|
|
157
|
+
cluster_id: Optional[Union[str, UUID]] = None,
|
|
158
|
+
project_id: Optional[Union[str, UUID]] = None,
|
|
159
|
+
select: Optional[builtins.list[str]] = None,
|
|
160
|
+
as_json: bool = False,
|
|
161
|
+
) -> ServiceDetail:
|
|
162
|
+
"""Print information about a service on the cluster."""
|
|
163
|
+
auth = get_auth_state()
|
|
164
|
+
res = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
|
|
165
|
+
try:
|
|
166
|
+
status = auth.broker_client.jobs.get_status(res.id)
|
|
167
|
+
msg.text(f"State: {status.state.value}", icon="info")
|
|
168
|
+
except (BrokerError, *HTTPXErrors):
|
|
169
|
+
pass
|
|
170
|
+
print_info_table(res, exclude=["plan"], as_json=as_json, select=select)
|
|
171
|
+
return res
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@cli.subcommand(
|
|
175
|
+
"services",
|
|
176
|
+
"logs",
|
|
177
|
+
name_or_id=Arg(help=Messages.name_or_id_optional.format(noun="service")),
|
|
178
|
+
project_id=Arg(help=Messages.project_id.format(noun="service")),
|
|
179
|
+
cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
|
|
180
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
181
|
+
show_errors=Arg(
|
|
182
|
+
"--errors", help="Show structured error information instead of full logs"
|
|
183
|
+
),
|
|
184
|
+
query=Arg("--query", help="Filter log lines matching this text (requires Loki)"),
|
|
185
|
+
)
|
|
186
|
+
def logs(
|
|
187
|
+
name_or_id: Optional[Union[str, UUID]] = None,
|
|
188
|
+
project_id: Optional[Union[str, UUID]] = None,
|
|
189
|
+
cluster_id: Optional[Union[str, UUID]] = None,
|
|
190
|
+
as_json: bool = False,
|
|
191
|
+
show_errors: bool = False,
|
|
192
|
+
query: Optional[str] = None,
|
|
193
|
+
) -> str | None:
|
|
194
|
+
"""Get logs for a service on the cluster."""
|
|
195
|
+
job = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
|
|
196
|
+
auth = get_auth_state()
|
|
197
|
+
if job.last_execution_id is None:
|
|
198
|
+
raise CLIError(Messages.E051.format(noun="service", name=job.name, id=job.id))
|
|
199
|
+
if show_errors:
|
|
200
|
+
try:
|
|
201
|
+
resp = auth.broker_client.jobs.errors(job_id=job.last_execution_id)
|
|
202
|
+
except BrokerError as e:
|
|
203
|
+
raise CLIError(Messages.E011.format(noun="service"), e)
|
|
204
|
+
if as_json:
|
|
205
|
+
print(resp.model_dump_json(indent=2))
|
|
206
|
+
else:
|
|
207
|
+
msg.text(f"State: {resp.state}", icon="info")
|
|
208
|
+
if resp.errors:
|
|
209
|
+
for err in resp.errors:
|
|
210
|
+
msg.fail(err.message)
|
|
211
|
+
if err.traceback:
|
|
212
|
+
print(err.traceback)
|
|
213
|
+
print()
|
|
214
|
+
elif resp.tail:
|
|
215
|
+
msg.warn("No structured errors found. Last 50 lines:")
|
|
216
|
+
print(resp.tail)
|
|
217
|
+
else:
|
|
218
|
+
msg.good("No errors found")
|
|
219
|
+
return None
|
|
220
|
+
try:
|
|
221
|
+
text = auth.broker_client.jobs.logs(job_id=job.last_execution_id, query=query)
|
|
222
|
+
except BrokerError as e:
|
|
223
|
+
raise CLIError(Messages.E011.format(noun="service"), e)
|
|
224
|
+
print_logs(text, as_json=as_json)
|
|
225
|
+
return text
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@cli.subcommand(
|
|
229
|
+
"services",
|
|
230
|
+
"start",
|
|
231
|
+
name_or_id=Arg(help=Messages.name_or_id_optional.format(noun="service")),
|
|
232
|
+
project_id=Arg(help=Messages.project_id.format(noun="service")),
|
|
233
|
+
cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
|
|
234
|
+
worker_class=Arg(
|
|
235
|
+
"--worker-class",
|
|
236
|
+
help=Messages.recipe_worker_class.format(noun="service"),
|
|
237
|
+
),
|
|
238
|
+
no_wait=Arg("--no-wait", help=Messages.no_wait.format(noun="service")),
|
|
239
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
240
|
+
)
|
|
241
|
+
def start(
|
|
242
|
+
name_or_id: Optional[Union[str, UUID]] = None,
|
|
243
|
+
project_id: Optional[Union[str, UUID]] = None,
|
|
244
|
+
cluster_id: Optional[Union[str, UUID]] = None,
|
|
245
|
+
worker_class: Optional[str] = None,
|
|
246
|
+
no_wait: bool = False,
|
|
247
|
+
as_json: bool = False,
|
|
248
|
+
) -> UUID:
|
|
249
|
+
"""Start a service on the cluster."""
|
|
250
|
+
job = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
|
|
251
|
+
start_job(
|
|
252
|
+
job,
|
|
253
|
+
worker_class,
|
|
254
|
+
get_auth_state(),
|
|
255
|
+
wait=not no_wait,
|
|
256
|
+
quiet=as_json,
|
|
257
|
+
)
|
|
258
|
+
if as_json:
|
|
259
|
+
print_mutation_result(
|
|
260
|
+
{"id": str(job.id), "status": "started"}, "", as_json=True
|
|
261
|
+
)
|
|
262
|
+
return job.id
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@cli.subcommand(
|
|
266
|
+
"services",
|
|
267
|
+
"stop",
|
|
268
|
+
name_or_id=Arg(help=Messages.name_or_id_optional.format(noun="service")),
|
|
269
|
+
project_id=Arg(help=Messages.project_id.format(noun="service")),
|
|
270
|
+
cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
|
|
271
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
272
|
+
)
|
|
273
|
+
def stop(
|
|
274
|
+
name_or_id: Optional[Union[str, UUID]] = None,
|
|
275
|
+
project_id: Optional[Union[str, UUID]] = None,
|
|
276
|
+
cluster_id: Optional[Union[str, UUID]] = None,
|
|
277
|
+
as_json: bool = False,
|
|
278
|
+
) -> UUID:
|
|
279
|
+
"""Stop a service on the cluster."""
|
|
280
|
+
job = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
|
|
281
|
+
stop_job(job, get_auth_state(), quiet=as_json)
|
|
282
|
+
if as_json:
|
|
283
|
+
print_mutation_result(
|
|
284
|
+
{"id": str(job.id), "status": "stopped"}, "", as_json=True
|
|
285
|
+
)
|
|
286
|
+
return job.id
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@cli.subcommand(
|
|
290
|
+
"services",
|
|
291
|
+
"delete",
|
|
292
|
+
name_or_id=Arg(help=Messages.name_or_id.format(noun="service")),
|
|
293
|
+
project_id=Arg(help=Messages.project_id.format(noun="service")),
|
|
294
|
+
cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
|
|
295
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
296
|
+
)
|
|
297
|
+
def delete(
|
|
298
|
+
name_or_id: Union[str, UUID],
|
|
299
|
+
project_id: Optional[Union[str, UUID]] = None,
|
|
300
|
+
cluster_id: Optional[Union[str, UUID]] = None,
|
|
301
|
+
as_json: bool = False,
|
|
302
|
+
) -> UUID:
|
|
303
|
+
"""Delete a service by name or ID."""
|
|
304
|
+
job = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
|
|
305
|
+
delete_job(job, get_auth_state(), quiet=as_json)
|
|
306
|
+
if as_json:
|
|
307
|
+
print_mutation_result({"id": str(job.id), "deleted": True}, "", as_json=True)
|
|
308
|
+
return job.id
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@cli.subcommand(
|
|
312
|
+
"services",
|
|
313
|
+
"url",
|
|
314
|
+
name_or_id=Arg(help=Messages.name_or_id.format(noun="service")),
|
|
315
|
+
project_id=Arg(help=Messages.project_id.format(noun="service")),
|
|
316
|
+
cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
|
|
317
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
318
|
+
)
|
|
319
|
+
def url(
|
|
320
|
+
name_or_id: Union[str, UUID],
|
|
321
|
+
project_id: Optional[Union[str, UUID]] = None,
|
|
322
|
+
cluster_id: Optional[Union[str, UUID]] = None,
|
|
323
|
+
as_json: bool = False,
|
|
324
|
+
) -> dict[str, str]:
|
|
325
|
+
"""Print the bearer-token-authenticated URL for a service.
|
|
326
|
+
|
|
327
|
+
Mints a fresh 90-day token via ``POST /v1/service/issue-token`` and
|
|
328
|
+
formats it together with the service's public URL so the result can
|
|
329
|
+
be dropped into an MCP client config or curl invocation:
|
|
330
|
+
|
|
331
|
+
curl -H "Authorization: Bearer <token>" <url>
|
|
332
|
+
|
|
333
|
+
Use ``services rotate-token`` to mint a new token without printing
|
|
334
|
+
the URL again.
|
|
335
|
+
"""
|
|
336
|
+
auth = get_auth_state()
|
|
337
|
+
service = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
|
|
338
|
+
token = auth.client.service.issue_token(id=service.id)
|
|
339
|
+
result = {
|
|
340
|
+
"url": service.url,
|
|
341
|
+
"access_token": token.access_token,
|
|
342
|
+
"expires_at": token.expires_at.isoformat(),
|
|
343
|
+
}
|
|
344
|
+
if as_json:
|
|
345
|
+
print_mutation_result(result, "", as_json=True)
|
|
346
|
+
else:
|
|
347
|
+
msg.good(f"URL: {service.url}")
|
|
348
|
+
msg.text(f"Token: {token.access_token}")
|
|
349
|
+
msg.text(f"Expires at: {token.expires_at.isoformat()}")
|
|
350
|
+
msg.text(
|
|
351
|
+
f"Try it: curl -H 'Authorization: Bearer {token.access_token}' "
|
|
352
|
+
f"{service.url}"
|
|
353
|
+
)
|
|
354
|
+
return result
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@cli.subcommand(
|
|
358
|
+
"services",
|
|
359
|
+
"rotate-token",
|
|
360
|
+
name_or_id=Arg(help=Messages.name_or_id.format(noun="service")),
|
|
361
|
+
project_id=Arg(help=Messages.project_id.format(noun="service")),
|
|
362
|
+
cluster_id=Arg(help=Messages.cluster_id.format(noun="service")),
|
|
363
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
364
|
+
)
|
|
365
|
+
def rotate_token(
|
|
366
|
+
name_or_id: Union[str, UUID],
|
|
367
|
+
project_id: Optional[Union[str, UUID]] = None,
|
|
368
|
+
cluster_id: Optional[Union[str, UUID]] = None,
|
|
369
|
+
as_json: bool = False,
|
|
370
|
+
) -> dict[str, str]:
|
|
371
|
+
"""Mint a fresh 90-day token for an existing service.
|
|
372
|
+
|
|
373
|
+
Issuing a new token does not invalidate previous ones -- they remain
|
|
374
|
+
valid until their own expiration. Use this when the previous token
|
|
375
|
+
is leaked, or when you want a longer remaining window for an MCP
|
|
376
|
+
client config.
|
|
377
|
+
"""
|
|
378
|
+
auth = get_auth_state()
|
|
379
|
+
service = resolve_service(name_or_id, cluster_id=cluster_id, project_id=project_id)
|
|
380
|
+
token = auth.client.service.issue_token(id=service.id)
|
|
381
|
+
result = {
|
|
382
|
+
"access_token": token.access_token,
|
|
383
|
+
"expires_at": token.expires_at.isoformat(),
|
|
384
|
+
}
|
|
385
|
+
if as_json:
|
|
386
|
+
print_mutation_result(result, "", as_json=True)
|
|
387
|
+
else:
|
|
388
|
+
msg.good(f"Token: {token.access_token}")
|
|
389
|
+
msg.text(f"Expires at: {token.expires_at.isoformat()}")
|
|
390
|
+
return result
|
|
@@ -38,6 +38,7 @@ class SavedSettings(BaseModel):
|
|
|
38
38
|
task: UUID | None = None
|
|
39
39
|
action: UUID | None = None
|
|
40
40
|
agent: UUID | None = None
|
|
41
|
+
service: UUID | None = None
|
|
41
42
|
pam_host: str | None = DEFAULT_PAM_HOST
|
|
42
43
|
cluster_id: UUID | None = None
|
|
43
44
|
org_id: UUID | None = None
|
|
@@ -51,6 +52,7 @@ class SavedSettings(BaseModel):
|
|
|
51
52
|
task=None,
|
|
52
53
|
action=None,
|
|
53
54
|
agent=None,
|
|
55
|
+
service=None,
|
|
54
56
|
pam_host=DEFAULT_PAM_HOST,
|
|
55
57
|
cluster_id=None,
|
|
56
58
|
org_id=None,
|
|
@@ -68,11 +70,12 @@ class SavedSettings(BaseModel):
|
|
|
68
70
|
raise
|
|
69
71
|
|
|
70
72
|
def reset_defaults(self) -> None:
|
|
71
|
-
"""Reset defaut project/task/action/agent, e.g. on host changes."""
|
|
73
|
+
"""Reset defaut project/task/action/agent/service, e.g. on host changes."""
|
|
72
74
|
self.project = None
|
|
73
75
|
self.task = None
|
|
74
76
|
self.action = None
|
|
75
77
|
self.agent = None
|
|
78
|
+
self.service = None
|
|
76
79
|
|
|
77
80
|
def to_json(self) -> JSONableDict:
|
|
78
81
|
data = {}
|
|
@@ -98,7 +101,9 @@ class SavedSettings(BaseModel):
|
|
|
98
101
|
@overload
|
|
99
102
|
def update(
|
|
100
103
|
self,
|
|
101
|
-
field: Literal[
|
|
104
|
+
field: Literal[
|
|
105
|
+
"project", "task", "action", "agent", "service", "cluster_id", "org_id"
|
|
106
|
+
],
|
|
102
107
|
value: UUID | None = None,
|
|
103
108
|
) -> UUID | None: ...
|
|
104
109
|
|
|
@@ -111,6 +116,7 @@ class SavedSettings(BaseModel):
|
|
|
111
116
|
"task",
|
|
112
117
|
"action",
|
|
113
118
|
"agent",
|
|
119
|
+
"service",
|
|
114
120
|
"cluster_id",
|
|
115
121
|
"org_id",
|
|
116
122
|
"recipes_file",
|