ellf-cli 14.0.0__tar.gz → 17.0.4__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-14.0.0/ellf_cli.egg-info → ellf_cli-17.0.4}/PKG-INFO +1 -1
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/README.md +89 -2
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/about.json +1 -1
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/cli.py +1 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/__init__.py +2 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/actions.py +2 -2
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/agents.py +2 -2
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/services.py +2 -2
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/tasks.py +2 -2
- ellf_cli-17.0.4/ellf_cli/commands/users.py +369 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/config.py +5 -1
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf.json +453 -7
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/.gitignore +1 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.assistant/SKILL.md +29 -1
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.coding/SKILL.md +29 -1
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/messages.py +15 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4/ellf_cli.egg-info}/PKG-INFO +1 -1
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli.egg-info/SOURCES.txt +1 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_config.py +13 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/LICENSE +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/MANIFEST.in +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/__init__.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/__main__.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/about.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/appdirs.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/auth.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/cloud/__init__.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/cloud/gcp.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/cluster_config.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/_cluster_select.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/_org_select.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/_recipe_file.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/_recipe_subcommand.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/_state.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/assets.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/auth.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/clusters.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/config.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/datasets.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/files/__init__.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/files/cp.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/files/ls.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/files/rm.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/files/rsync.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/files/stats.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/general.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/import_export.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/__init__.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/_helpers.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/deploy.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/init_values.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/provision.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/register.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/setup.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/start.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/terraform.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/tls.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/jobs.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/packages.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/paths.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/plans.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/projects.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/publish_code.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/publish_data.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/recipes.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/secrets.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/support.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/todos.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/.claude-plugin/plugin.json +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skill_variants.json +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/references/annotation_audit.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/references/builtin_ellf_annotation_recipes.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/annotation_audit.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/builtin_ellf_annotation_recipes.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/builtin_prodigy_recipes.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-ask/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-handoff/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/references/annotation_metrics.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/references/training_monitoring.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/references/annotation_metrics.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/references/training_monitoring.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/scripts/check_training.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-ops.assistant/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-ops.coding/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-ops.coding/references/data_infra_cli.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-ops.coding/scripts/run_job.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-patterns/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-patterns/references/pattern_strategies.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_action_recipe.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_agent_recipe.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_blocks_ui.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_correct.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_custom_ui.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_manual.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_pages_ui.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_routing.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_task_recipe.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_teach.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/builtin_recipes.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/ellf_recipe_sdk.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/lint_recipe.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/prodigy_recipe_api.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/template_index.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/consulting_patterns.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/explosion_strategy.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/prodigy_llm_bot.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/consulting_patterns.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/explosion_strategy.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/prodigy_llm_bot.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-support.assistant/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-support.coding/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-todo/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/diagnostics.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/evaluation_guide.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/model_selection.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/training_paradigms.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/workflow.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/SKILL.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_advanced.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_architectures.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_training.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/diagnostics.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/evaluation_guide.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/experiment_patterns.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/model_selection.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/training_paradigms.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/training_troubleshooting.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/workflow.md +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/scripts/ellf_logger.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/errors.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/helm.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/key_pair.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/main.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/query.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/cookiecutter.json +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/.gitignore +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/README.md.tmpl +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/requirements-dev.in +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/requirements.in +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/setup.py.tmpl +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/__init__.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/about.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/recipes/__init__.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/recipes/example_task.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/testing/__init__.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ty.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ui.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/url.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/util.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli.egg-info/dependency_links.txt +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli.egg-info/entry_points.txt +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli.egg-info/not-zip-safe +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli.egg-info/requires.txt +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli.egg-info/top_level.txt +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/pyproject.toml +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/setup.cfg +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/setup.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_appdirs.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_auth.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_errors.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_files_cp.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_files_cp_helpers.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_info.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_invalid_secrets.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_key_pair.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_login.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_logout.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_main.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_org_select.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_plans.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_projects.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_query.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_recipe_file.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_recipes.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_state.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_support.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_ty.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_ui.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_ui_extras.py +0 -0
- {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_util.py +0 -0
|
@@ -297,7 +297,7 @@ Create an asset on the cluster and register it with Ellf. Assets point to files
|
|
|
297
297
|
| Argument | Type | Description | Default |
|
|
298
298
|
| --- | --- | --- | --- |
|
|
299
299
|
| `name` | `str` | Name of the asset | |
|
|
300
|
-
| `--kind` | `str` | Kind of the asset. Generally one of: ['
|
|
300
|
+
| `--kind` | `str` | Kind of the asset. Generally one of: ['input', 'model', 'patterns'] | |
|
|
301
301
|
| `path` | `str` | Path of the asset | |
|
|
302
302
|
| `--version` | `str` | Version of the asset | `'0.0.0'` |
|
|
303
303
|
| `--meta` | `str` | Asset meta, formatted as a JSON string | `'{}'` |
|
|
@@ -1290,6 +1290,93 @@ Fetch a batch of questions from a running annotation task. Mirrors the web app:
|
|
|
1290
1290
|
| `--cluster` | `str` | Name, slug, or ID of the cluster to search for task name (or the last cluster if not set) | `None` |
|
|
1291
1291
|
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1292
1292
|
|
|
1293
|
+
### `ellf users`
|
|
1294
|
+
|
|
1295
|
+
View and manage users and invitations in your organization
|
|
1296
|
+
|
|
1297
|
+
#### `ellf users list`
|
|
1298
|
+
|
|
1299
|
+
List all users in the organization
|
|
1300
|
+
|
|
1301
|
+
| Argument | Type | Description | Default |
|
|
1302
|
+
| --- | --- | --- | --- |
|
|
1303
|
+
| `--select` | `list[str]` | Comma-separated fields to select and show in output. Available: ['id', 'created', 'updated', 'name', 'org_id', 'org_name', 'person_id', 'email', 'avatar_url', 'locale', 'is_org_admin', 'is_org_developer', 'last_cluster_id', 'last_project_id', 'role'] | `['id', 'name', 'email', 'role']` |
|
|
1304
|
+
| `--role` | `str` | Filter by role | `None` |
|
|
1305
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1306
|
+
|
|
1307
|
+
#### `ellf users info`
|
|
1308
|
+
|
|
1309
|
+
Get detailed info for a user
|
|
1310
|
+
|
|
1311
|
+
| Argument | Type | Description | Default |
|
|
1312
|
+
| --- | --- | --- | --- |
|
|
1313
|
+
| `email_name_or_id` | `Union[str, UUID]` | Email, name, or ID of the user | |
|
|
1314
|
+
| `--select` | `list[str]` | Comma-separated fields to select and show in output. Available: ['id', 'created', 'updated', 'name', 'org_id', 'org_name', 'person_id', 'email', 'avatar_url', 'locale', 'is_org_admin', 'is_org_developer', 'last_cluster_id', 'last_project_id', 'groups', 'tags'] | `None` |
|
|
1315
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1316
|
+
|
|
1317
|
+
#### `ellf users whoami`
|
|
1318
|
+
|
|
1319
|
+
Show the currently authenticated user
|
|
1320
|
+
|
|
1321
|
+
| Argument | Type | Description | Default |
|
|
1322
|
+
| --- | --- | --- | --- |
|
|
1323
|
+
| `--select` | `list[str]` | Comma-separated fields to select and show in output. Available: ['id', 'created', 'updated', 'name', 'org_id', 'org_name', 'person_id', 'email', 'avatar_url', 'locale', 'is_org_admin', 'is_org_developer', 'last_cluster_id', 'last_project_id', 'groups', 'tags'] | `None` |
|
|
1324
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1325
|
+
|
|
1326
|
+
#### `ellf users update`
|
|
1327
|
+
|
|
1328
|
+
Update a user's role, name, email, locale, or tags
|
|
1329
|
+
|
|
1330
|
+
| Argument | Type | Description | Default |
|
|
1331
|
+
| --- | --- | --- | --- |
|
|
1332
|
+
| `email_name_or_id` | `Union[str, UUID]` | Email, name, or ID of the user | |
|
|
1333
|
+
| `--name` | `str` | New name of the user | `None` |
|
|
1334
|
+
| `--email` | `str` | Email of the user | `None` |
|
|
1335
|
+
| `--locale` | `str` | Locale of the user (e.g. 'en') | `None` |
|
|
1336
|
+
| `--role` | `str` | Role of the user. One of: ['admin', 'developer', 'annotator'] | `None` |
|
|
1337
|
+
| `--tags` | `list[str]` | Comma-separated tags to set on the user (replaces existing tags) | `None` |
|
|
1338
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1339
|
+
|
|
1340
|
+
#### `ellf users delete`
|
|
1341
|
+
|
|
1342
|
+
Remove a user from the organization
|
|
1343
|
+
|
|
1344
|
+
| Argument | Type | Description | Default |
|
|
1345
|
+
| --- | --- | --- | --- |
|
|
1346
|
+
| `email_name_or_id` | `Union[str, UUID]` | Email, name, or ID of the user | |
|
|
1347
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1348
|
+
|
|
1349
|
+
#### `ellf users invite`
|
|
1350
|
+
|
|
1351
|
+
Invite a new user to the organization by email
|
|
1352
|
+
|
|
1353
|
+
| Argument | Type | Description | Default |
|
|
1354
|
+
| --- | --- | --- | --- |
|
|
1355
|
+
| `email` | `str` | Email address to invite to the organization | |
|
|
1356
|
+
| `--role` | `str` | Role of the user. One of: ['admin', 'developer', 'annotator'] | `'annotator'` |
|
|
1357
|
+
| `--no-email` | `bool` | Create the invitation without sending the invitation email | `False` |
|
|
1358
|
+
| `--resend` | `bool` | Re-send the invitation email for an existing unclaimed invitation | `False` |
|
|
1359
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1360
|
+
|
|
1361
|
+
#### `ellf users invitations`
|
|
1362
|
+
|
|
1363
|
+
List pending invitations
|
|
1364
|
+
|
|
1365
|
+
| Argument | Type | Description | Default |
|
|
1366
|
+
| --- | --- | --- | --- |
|
|
1367
|
+
| `--select` | `list[str]` | Comma-separated fields to select and show in output. Available: ['id', 'created', 'updated', 'org_id', 'email', 'claimed', 'vars', 'role'] | `['id', 'email', 'role', 'claimed']` |
|
|
1368
|
+
| `--all` | `bool` | Also include invitations that have already been claimed | `False` |
|
|
1369
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1370
|
+
|
|
1371
|
+
#### `ellf users uninvite`
|
|
1372
|
+
|
|
1373
|
+
Revoke a pending invitation
|
|
1374
|
+
|
|
1375
|
+
| Argument | Type | Description | Default |
|
|
1376
|
+
| --- | --- | --- | --- |
|
|
1377
|
+
| `email_or_id` | `Union[str, UUID]` | Email or ID of the invitation | |
|
|
1378
|
+
| `--json` | `bool` | Output the result as JSON | `False` |
|
|
1379
|
+
|
|
1293
1380
|
### `ellf publish`
|
|
1294
1381
|
|
|
1295
1382
|
Publish
|
|
@@ -1315,7 +1402,7 @@ Transfer data to the cluster, and advertise it to Ellf. These steps can also be
|
|
|
1315
1402
|
| `dest` | `str` | Destination path to copy the data to | `None` |
|
|
1316
1403
|
| `--name` | `str` | Name of the asset | `None` |
|
|
1317
1404
|
| `--version` | `str` | Version of the asset | `None` |
|
|
1318
|
-
| `--kind` | `str` | Kind of the asset. Generally one of: ['
|
|
1405
|
+
| `--kind` | `str` | Kind of the asset. Generally one of: ['input', 'model', 'patterns'] | |
|
|
1319
1406
|
| `--loader` | `str` | Loader to convert data for Prodigy | `None` |
|
|
1320
1407
|
| `--meta` | `str` | Asset meta, formatted as a JSON string | `'{}'` |
|
|
1321
1408
|
| `--exists-ok` | `bool` | Don't raise an error if it already exists | `False` |
|
|
@@ -123,6 +123,7 @@ cli.placeholder("secrets", description=Messages.doc_secrets)
|
|
|
123
123
|
cli.placeholder("services", description=Messages.doc_services)
|
|
124
124
|
cli.placeholder("support", description=Messages.doc_support)
|
|
125
125
|
cli.placeholder("tasks", description=Messages.doc_tasks)
|
|
126
|
+
cli.placeholder("users", description=Messages.doc_users)
|
|
126
127
|
cli.placeholder("publish", description="Publish")
|
|
127
128
|
|
|
128
129
|
|
|
@@ -194,8 +194,8 @@ def info(
|
|
|
194
194
|
try:
|
|
195
195
|
status = auth.broker_client.jobs.get_status(res.id)
|
|
196
196
|
msg.text(f"State: {status.state}", icon="info")
|
|
197
|
-
except (BrokerError, *HTTPXErrors):
|
|
198
|
-
|
|
197
|
+
except (BrokerError, *HTTPXErrors) as e:
|
|
198
|
+
msg.warn(f"Could not fetch live status from the cluster: {e}")
|
|
199
199
|
print_info_table(res, exclude=["plan"], as_json=as_json, select=select)
|
|
200
200
|
return res
|
|
201
201
|
|
|
@@ -227,8 +227,8 @@ def info(
|
|
|
227
227
|
try:
|
|
228
228
|
status = auth.broker_client.jobs.get_status(res.id)
|
|
229
229
|
msg.text(f"State: {status.state.value}", icon="info")
|
|
230
|
-
except (BrokerError, *HTTPXErrors):
|
|
231
|
-
|
|
230
|
+
except (BrokerError, *HTTPXErrors) as e:
|
|
231
|
+
msg.warn(f"Could not fetch live status from the cluster: {e}")
|
|
232
232
|
print_info_table(res, exclude=["plan"], as_json=as_json, select=select)
|
|
233
233
|
return res
|
|
234
234
|
|
|
@@ -195,8 +195,8 @@ def info(
|
|
|
195
195
|
try:
|
|
196
196
|
status = auth.broker_client.jobs.get_status(res.id)
|
|
197
197
|
msg.text(f"State: {status.state.value}", icon="info")
|
|
198
|
-
except (BrokerError, *HTTPXErrors):
|
|
199
|
-
|
|
198
|
+
except (BrokerError, *HTTPXErrors) as e:
|
|
199
|
+
msg.warn(f"Could not fetch live status from the cluster: {e}")
|
|
200
200
|
print_info_table(res, exclude=["plan"], as_json=as_json, select=select)
|
|
201
201
|
return res
|
|
202
202
|
|
|
@@ -200,8 +200,8 @@ def info(
|
|
|
200
200
|
try:
|
|
201
201
|
status = auth.broker_client.jobs.get_status(res.id)
|
|
202
202
|
msg.text(f"State: {status.state.value}", icon="info")
|
|
203
|
-
except (BrokerError, *HTTPXErrors):
|
|
204
|
-
|
|
203
|
+
except (BrokerError, *HTTPXErrors) as e:
|
|
204
|
+
msg.warn(f"Could not fetch live status from the cluster: {e}")
|
|
205
205
|
print_info_table(res, exclude=["plan"], as_json=as_json, select=select)
|
|
206
206
|
return res
|
|
207
207
|
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Literal, Optional, Union
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from radicli import Arg
|
|
8
|
+
from wasabi import msg
|
|
9
|
+
|
|
10
|
+
from ellf_pam_sdk import Client as PamClient
|
|
11
|
+
from ellf_pam_sdk.errors import EllfError
|
|
12
|
+
from ellf_pam_sdk.models import (
|
|
13
|
+
InvitationDetail,
|
|
14
|
+
InvitationReading,
|
|
15
|
+
InvitationSummary,
|
|
16
|
+
InvitationUpdating,
|
|
17
|
+
UserDetail,
|
|
18
|
+
UserReading,
|
|
19
|
+
UserSummary,
|
|
20
|
+
UserUpdating,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from ..cli import cli
|
|
24
|
+
from ..errors import CLIError, EllfErrors, HTTPXErrors
|
|
25
|
+
from ..messages import Messages
|
|
26
|
+
from ..query import _try_uuid
|
|
27
|
+
from ..ui import print_info_table, print_mutation_result, print_table_with_select
|
|
28
|
+
from ._state import get_auth_state
|
|
29
|
+
|
|
30
|
+
# Mirrors the role model of the web app's members page: a user is an
|
|
31
|
+
# admin if is_org_admin is set, a developer if only is_org_developer is
|
|
32
|
+
# set, and an annotator if neither is. Agent users are system users and
|
|
33
|
+
# never listed by the API, so they don't appear here.
|
|
34
|
+
UserRoleName = Literal["admin", "developer", "annotator"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _role_of(user: Union[UserSummary, UserDetail]) -> str:
|
|
38
|
+
if user.is_org_admin:
|
|
39
|
+
return "admin"
|
|
40
|
+
if user.is_org_developer:
|
|
41
|
+
return "developer"
|
|
42
|
+
return "annotator"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _role_flags(role: UserRoleName) -> dict[str, bool]:
|
|
46
|
+
"""Exclusive flag assignment for updates, as the web app does it."""
|
|
47
|
+
return {
|
|
48
|
+
"is_org_admin": role == "admin",
|
|
49
|
+
"is_org_developer": role == "developer",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _role_query(role: UserRoleName) -> UserReading:
|
|
54
|
+
"""Server-side filter matching how _role_of classifies users."""
|
|
55
|
+
if role == "admin":
|
|
56
|
+
return UserReading(is_org_admin=True)
|
|
57
|
+
if role == "developer":
|
|
58
|
+
return UserReading(is_org_admin=False, is_org_developer=True)
|
|
59
|
+
return UserReading(is_org_admin=False, is_org_developer=False)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _resolve_user(email_name_or_id: Union[str, UUID]) -> UserDetail:
|
|
63
|
+
"""Resolve a user by UUID, email, or display name.
|
|
64
|
+
|
|
65
|
+
Users have no slug and their Reading schema only filters on IDs and
|
|
66
|
+
role flags, so the generic slug/name resolver in query.py can't be
|
|
67
|
+
used here (an unknown field would fall through to an empty query,
|
|
68
|
+
which the API answers with the *current* user). Emails are unique
|
|
69
|
+
per org; names aren't, so ambiguous name matches are an error.
|
|
70
|
+
"""
|
|
71
|
+
auth = get_auth_state()
|
|
72
|
+
record_id = _try_uuid(email_name_or_id)
|
|
73
|
+
if record_id is not None:
|
|
74
|
+
try:
|
|
75
|
+
return auth.client.user.read(id=record_id)
|
|
76
|
+
except EllfErrors.UserNotFound:
|
|
77
|
+
raise CLIError(
|
|
78
|
+
Messages.E038.format(noun="user", name_or_id=email_name_or_id)
|
|
79
|
+
)
|
|
80
|
+
value = str(email_name_or_id)
|
|
81
|
+
users = builtins.list(auth.client.user.all())
|
|
82
|
+
matches = [u for u in users if u.email.lower() == value.lower()]
|
|
83
|
+
if not matches:
|
|
84
|
+
matches = [u for u in users if u.name == value]
|
|
85
|
+
if len(matches) > 1:
|
|
86
|
+
ids = ", ".join(str(u.id) for u in matches)
|
|
87
|
+
raise CLIError(Messages.E061.format(name_or_id=value, ids=ids))
|
|
88
|
+
if not matches:
|
|
89
|
+
raise CLIError(Messages.E038.format(noun="user", name_or_id=value))
|
|
90
|
+
return auth.client.user.read(id=matches[0].id)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@cli.subcommand(
|
|
94
|
+
"users",
|
|
95
|
+
"list",
|
|
96
|
+
# fmt: off
|
|
97
|
+
select=Arg(
|
|
98
|
+
"--select",
|
|
99
|
+
help=Messages.select.format(
|
|
100
|
+
opts=builtins.list(UserSummary.model_fields) + ["role"]
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
role=Arg("--role", help=Messages.filter_by.format(filter="role")),
|
|
104
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
105
|
+
# fmt: on
|
|
106
|
+
)
|
|
107
|
+
def list(
|
|
108
|
+
select: builtins.list[str] = ["id", "name", "email", "role"],
|
|
109
|
+
role: Optional[UserRoleName] = None,
|
|
110
|
+
as_json: bool = False,
|
|
111
|
+
) -> Sequence[UserSummary]:
|
|
112
|
+
"""List all users in the organization"""
|
|
113
|
+
client = get_auth_state().client
|
|
114
|
+
query = _role_query(role) if role is not None else None
|
|
115
|
+
items = builtins.list(client.user.all(query))
|
|
116
|
+
rows = [{**u.model_dump(), "role": _role_of(u)} for u in items]
|
|
117
|
+
print_table_with_select(rows, select=select, as_json=as_json)
|
|
118
|
+
return items
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@cli.subcommand(
|
|
122
|
+
"users",
|
|
123
|
+
"info",
|
|
124
|
+
email_name_or_id=Arg(help=Messages.user_email_name_or_id),
|
|
125
|
+
select=Arg(
|
|
126
|
+
"--select",
|
|
127
|
+
help=Messages.select.format(opts=builtins.list(UserDetail.model_fields)),
|
|
128
|
+
),
|
|
129
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
130
|
+
)
|
|
131
|
+
def info(
|
|
132
|
+
email_name_or_id: Union[str, UUID],
|
|
133
|
+
select: Optional[builtins.list[str]] = None,
|
|
134
|
+
as_json: bool = False,
|
|
135
|
+
) -> UserDetail:
|
|
136
|
+
"""Get detailed info for a user"""
|
|
137
|
+
res = _resolve_user(email_name_or_id)
|
|
138
|
+
print_info_table(res, as_json=as_json, select=select)
|
|
139
|
+
return res
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@cli.subcommand(
|
|
143
|
+
"users",
|
|
144
|
+
"whoami",
|
|
145
|
+
select=Arg(
|
|
146
|
+
"--select",
|
|
147
|
+
help=Messages.select.format(opts=builtins.list(UserDetail.model_fields)),
|
|
148
|
+
),
|
|
149
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
150
|
+
)
|
|
151
|
+
def whoami(
|
|
152
|
+
select: Optional[builtins.list[str]] = None,
|
|
153
|
+
as_json: bool = False,
|
|
154
|
+
) -> UserDetail:
|
|
155
|
+
"""Show the currently authenticated user"""
|
|
156
|
+
auth = get_auth_state()
|
|
157
|
+
res = auth.client.user.read(id=auth.user_id)
|
|
158
|
+
print_info_table(res, as_json=as_json, select=select)
|
|
159
|
+
return res
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@cli.subcommand(
|
|
163
|
+
"users",
|
|
164
|
+
"update",
|
|
165
|
+
email_name_or_id=Arg(help=Messages.user_email_name_or_id),
|
|
166
|
+
name=Arg("--name", help=Messages.new_name.format(noun="user")),
|
|
167
|
+
email=Arg("--email", help=Messages.email.format(noun="user")),
|
|
168
|
+
locale=Arg("--locale", help=Messages.user_locale),
|
|
169
|
+
role=Arg("--role", help=Messages.user_role),
|
|
170
|
+
tags=Arg("--tags", help=Messages.user_tags),
|
|
171
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
172
|
+
)
|
|
173
|
+
def update(
|
|
174
|
+
email_name_or_id: Union[str, UUID],
|
|
175
|
+
name: Optional[str] = None,
|
|
176
|
+
email: Optional[str] = None,
|
|
177
|
+
locale: Optional[str] = None,
|
|
178
|
+
role: Optional[UserRoleName] = None,
|
|
179
|
+
tags: Optional[builtins.list[str]] = None,
|
|
180
|
+
as_json: bool = False,
|
|
181
|
+
) -> UserDetail:
|
|
182
|
+
"""Update a user's role, name, email, locale, or tags"""
|
|
183
|
+
auth = get_auth_state()
|
|
184
|
+
user = _resolve_user(email_name_or_id)
|
|
185
|
+
if role is not None and user.id == auth.user_id:
|
|
186
|
+
raise CLIError(Messages.E062.format(name=user.email))
|
|
187
|
+
extra: dict = _role_flags(role) if role is not None else {}
|
|
188
|
+
if tags is not None:
|
|
189
|
+
# Only pass tags when given: the Updating schema's default ([])
|
|
190
|
+
# is dropped server-side, but an explicit None would null the
|
|
191
|
+
# column.
|
|
192
|
+
extra["tags"] = tags
|
|
193
|
+
body = UserUpdating(
|
|
194
|
+
id=user.id,
|
|
195
|
+
name=name,
|
|
196
|
+
email=email,
|
|
197
|
+
locale=locale,
|
|
198
|
+
**extra,
|
|
199
|
+
)
|
|
200
|
+
res = auth.client.user.update(body)
|
|
201
|
+
print_mutation_result(
|
|
202
|
+
{
|
|
203
|
+
"id": str(res.id),
|
|
204
|
+
"name": res.name,
|
|
205
|
+
"email": res.email,
|
|
206
|
+
"role": _role_of(res),
|
|
207
|
+
},
|
|
208
|
+
Messages.T051.format(noun="user", name=res.email),
|
|
209
|
+
as_json=as_json,
|
|
210
|
+
)
|
|
211
|
+
if not as_json:
|
|
212
|
+
print_info_table(res)
|
|
213
|
+
return res
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@cli.subcommand(
|
|
217
|
+
"users",
|
|
218
|
+
"delete",
|
|
219
|
+
email_name_or_id=Arg(help=Messages.user_email_name_or_id),
|
|
220
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
221
|
+
)
|
|
222
|
+
def delete(email_name_or_id: Union[str, UUID], as_json: bool = False) -> UUID:
|
|
223
|
+
"""Remove a user from the organization"""
|
|
224
|
+
auth = get_auth_state()
|
|
225
|
+
user = _resolve_user(email_name_or_id)
|
|
226
|
+
try:
|
|
227
|
+
auth.client.user.delete(id=user.id)
|
|
228
|
+
except (EllfErrors.UserForbiddenDelete, EllfErrors.UserNotFound):
|
|
229
|
+
raise CLIError(Messages.E006.format(noun="user", name=email_name_or_id))
|
|
230
|
+
print_mutation_result(
|
|
231
|
+
{"id": str(user.id), "email": user.email, "deleted": True},
|
|
232
|
+
Messages.T003.format(noun="user", name=user.email),
|
|
233
|
+
as_json=as_json,
|
|
234
|
+
)
|
|
235
|
+
return user.id
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@cli.subcommand(
|
|
239
|
+
"users",
|
|
240
|
+
"invite",
|
|
241
|
+
email=Arg(help=Messages.invite_email),
|
|
242
|
+
role=Arg("--role", help=Messages.user_role),
|
|
243
|
+
no_email=Arg("--no-email", help=Messages.invite_no_email),
|
|
244
|
+
resend=Arg("--resend", help=Messages.invite_resend),
|
|
245
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
246
|
+
)
|
|
247
|
+
def invite(
|
|
248
|
+
email: str,
|
|
249
|
+
role: UserRoleName = "annotator",
|
|
250
|
+
no_email: bool = False,
|
|
251
|
+
resend: bool = False,
|
|
252
|
+
as_json: bool = False,
|
|
253
|
+
) -> InvitationDetail:
|
|
254
|
+
"""Invite a new user to the organization by email"""
|
|
255
|
+
client = get_auth_state().client
|
|
256
|
+
if resend:
|
|
257
|
+
invitation = client.invitation.exists(email=email, claimed=False)
|
|
258
|
+
if invitation is None:
|
|
259
|
+
raise CLIError(Messages.E038.format(noun="invitation", name_or_id=email))
|
|
260
|
+
else:
|
|
261
|
+
try:
|
|
262
|
+
invitation = client.invitation.create(email=email, vars={"role": role})
|
|
263
|
+
except EllfErrors.InvitationExists:
|
|
264
|
+
raise CLIError(Messages.E002.format(noun="invitation", name=email))
|
|
265
|
+
except EllfErrors.InvitationInvalid:
|
|
266
|
+
raise CLIError(Messages.E004.format(noun="invitation", name=email))
|
|
267
|
+
if not no_email:
|
|
268
|
+
_send_invitation_email(client, invitation)
|
|
269
|
+
msg.info(Messages.T052.format(email=email))
|
|
270
|
+
print_mutation_result(
|
|
271
|
+
{
|
|
272
|
+
"id": str(invitation.id),
|
|
273
|
+
"email": invitation.email,
|
|
274
|
+
"role": invitation.vars.get("role", role),
|
|
275
|
+
},
|
|
276
|
+
Messages.T002.format(noun="invitation", name=email),
|
|
277
|
+
as_json=as_json,
|
|
278
|
+
)
|
|
279
|
+
return invitation
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _send_invitation_email(client: PamClient, invitation: InvitationDetail) -> None:
|
|
283
|
+
"""Send the invitation email, mirroring the web app's bookkeeping:
|
|
284
|
+
stamp ``email_sent`` in the invitation vars on success and
|
|
285
|
+
``email_failed`` on failure, so both surfaces report send status the
|
|
286
|
+
same way.
|
|
287
|
+
"""
|
|
288
|
+
vars = dict(invitation.vars)
|
|
289
|
+
vars.pop("email_failed", None)
|
|
290
|
+
error: CLIError | None = None
|
|
291
|
+
try:
|
|
292
|
+
results = client.invitation.send(id=invitation.id)
|
|
293
|
+
except (EllfError, *HTTPXErrors) as e:
|
|
294
|
+
vars["email_failed"] = str(e)
|
|
295
|
+
error = CLIError(Messages.E063.format(email=invitation.email), str(e))
|
|
296
|
+
else:
|
|
297
|
+
result = results[0] if results else None
|
|
298
|
+
if result is None or result.status in ("rejected", "invalid"):
|
|
299
|
+
reason = (
|
|
300
|
+
result.reject_reason
|
|
301
|
+
if result is not None
|
|
302
|
+
else "Email service returned an empty response"
|
|
303
|
+
)
|
|
304
|
+
vars["email_failed"] = reason or "Failed sending invitation email"
|
|
305
|
+
error = CLIError(Messages.E063.format(email=invitation.email), reason)
|
|
306
|
+
else:
|
|
307
|
+
vars["email_sent"] = datetime.now(timezone.utc).isoformat()
|
|
308
|
+
client.invitation.update(InvitationUpdating(id=invitation.id, vars=vars))
|
|
309
|
+
if error is not None:
|
|
310
|
+
raise error
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@cli.subcommand(
|
|
314
|
+
"users",
|
|
315
|
+
"invitations",
|
|
316
|
+
# fmt: off
|
|
317
|
+
select=Arg(
|
|
318
|
+
"--select",
|
|
319
|
+
help=Messages.select.format(
|
|
320
|
+
opts=builtins.list(InvitationSummary.model_fields) + ["role"]
|
|
321
|
+
),
|
|
322
|
+
),
|
|
323
|
+
all_=Arg("--all", help=Messages.invitations_all),
|
|
324
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
325
|
+
# fmt: on
|
|
326
|
+
)
|
|
327
|
+
def invitations(
|
|
328
|
+
select: builtins.list[str] = ["id", "email", "role", "claimed"],
|
|
329
|
+
all_: bool = False,
|
|
330
|
+
as_json: bool = False,
|
|
331
|
+
) -> Sequence[InvitationSummary]:
|
|
332
|
+
"""List pending invitations"""
|
|
333
|
+
client = get_auth_state().client
|
|
334
|
+
query = None if all_ else InvitationReading(claimed=False)
|
|
335
|
+
items = builtins.list(client.invitation.all(query))
|
|
336
|
+
rows = [
|
|
337
|
+
{**inv.model_dump(), "role": inv.vars.get("role", "annotator")} for inv in items
|
|
338
|
+
]
|
|
339
|
+
print_table_with_select(rows, select=select, as_json=as_json)
|
|
340
|
+
return items
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@cli.subcommand(
|
|
344
|
+
"users",
|
|
345
|
+
"uninvite",
|
|
346
|
+
email_or_id=Arg(help=Messages.invitation_email_or_id),
|
|
347
|
+
as_json=Arg("--json", help=Messages.as_json),
|
|
348
|
+
)
|
|
349
|
+
def uninvite(email_or_id: Union[str, UUID], as_json: bool = False) -> UUID:
|
|
350
|
+
"""Revoke a pending invitation"""
|
|
351
|
+
client = get_auth_state().client
|
|
352
|
+
record_id = _try_uuid(email_or_id)
|
|
353
|
+
if record_id is None:
|
|
354
|
+
invitation = client.invitation.exists(email=str(email_or_id))
|
|
355
|
+
if invitation is None:
|
|
356
|
+
raise CLIError(
|
|
357
|
+
Messages.E038.format(noun="invitation", name_or_id=email_or_id)
|
|
358
|
+
)
|
|
359
|
+
record_id = invitation.id
|
|
360
|
+
try:
|
|
361
|
+
client.invitation.delete(id=record_id)
|
|
362
|
+
except (EllfErrors.InvitationNotFound, EllfErrors.InvitationForbiddenDelete):
|
|
363
|
+
raise CLIError(Messages.E006.format(noun="invitation", name=email_or_id))
|
|
364
|
+
print_mutation_result(
|
|
365
|
+
{"id": str(record_id), "deleted": True},
|
|
366
|
+
Messages.T003.format(noun="invitation", name=email_or_id),
|
|
367
|
+
as_json=as_json,
|
|
368
|
+
)
|
|
369
|
+
return record_id
|
|
@@ -5,8 +5,10 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Literal, overload
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
|
|
8
|
-
from pydantic import BaseModel
|
|
8
|
+
from pydantic import BaseModel, ValidationError
|
|
9
9
|
|
|
10
|
+
from .errors import CLIError
|
|
11
|
+
from .messages import Messages
|
|
10
12
|
from .ty import JSONableDict
|
|
11
13
|
from .util import APP_NAME, get_app_dir
|
|
12
14
|
|
|
@@ -68,6 +70,8 @@ class SavedSettings(BaseModel):
|
|
|
68
70
|
return cls.blank()
|
|
69
71
|
else:
|
|
70
72
|
raise
|
|
73
|
+
except ValidationError as e:
|
|
74
|
+
raise CLIError(Messages.E187.format(path=path), e)
|
|
71
75
|
|
|
72
76
|
def reset_defaults(self) -> None:
|
|
73
77
|
"""Reset defaut project/task/action/agent/service, e.g. on host changes."""
|