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.
Files changed (186) hide show
  1. {ellf_cli-14.0.0/ellf_cli.egg-info → ellf_cli-17.0.4}/PKG-INFO +1 -1
  2. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/README.md +89 -2
  3. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/about.json +1 -1
  4. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/cli.py +1 -0
  5. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/__init__.py +2 -0
  6. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/actions.py +2 -2
  7. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/agents.py +2 -2
  8. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/services.py +2 -2
  9. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/tasks.py +2 -2
  10. ellf_cli-17.0.4/ellf_cli/commands/users.py +369 -0
  11. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/config.py +5 -1
  12. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf.json +453 -7
  13. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/.gitignore +1 -0
  14. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.assistant/SKILL.md +29 -1
  15. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.coding/SKILL.md +29 -1
  16. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/messages.py +15 -0
  17. {ellf_cli-14.0.0 → ellf_cli-17.0.4/ellf_cli.egg-info}/PKG-INFO +1 -1
  18. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli.egg-info/SOURCES.txt +1 -0
  19. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_config.py +13 -0
  20. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/LICENSE +0 -0
  21. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/MANIFEST.in +0 -0
  22. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/__init__.py +0 -0
  23. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/__main__.py +0 -0
  24. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/about.py +0 -0
  25. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/appdirs.py +0 -0
  26. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/auth.py +0 -0
  27. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/cloud/__init__.py +0 -0
  28. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/cloud/gcp.py +0 -0
  29. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/cluster_config.py +0 -0
  30. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/_cluster_select.py +0 -0
  31. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/_org_select.py +0 -0
  32. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/_recipe_file.py +0 -0
  33. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/_recipe_subcommand.py +0 -0
  34. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/_state.py +0 -0
  35. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/assets.py +0 -0
  36. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/auth.py +0 -0
  37. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/clusters.py +0 -0
  38. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/config.py +0 -0
  39. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/datasets.py +0 -0
  40. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/files/__init__.py +0 -0
  41. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/files/cp.py +0 -0
  42. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/files/ls.py +0 -0
  43. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/files/rm.py +0 -0
  44. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/files/rsync.py +0 -0
  45. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/files/stats.py +0 -0
  46. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/general.py +0 -0
  47. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/import_export.py +0 -0
  48. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/__init__.py +0 -0
  49. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/_helpers.py +0 -0
  50. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/deploy.py +0 -0
  51. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/init_values.py +0 -0
  52. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/provision.py +0 -0
  53. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/register.py +0 -0
  54. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/setup.py +0 -0
  55. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/start.py +0 -0
  56. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/terraform.py +0 -0
  57. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/infra/tls.py +0 -0
  58. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/jobs.py +0 -0
  59. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/packages.py +0 -0
  60. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/paths.py +0 -0
  61. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/plans.py +0 -0
  62. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/projects.py +0 -0
  63. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/publish_code.py +0 -0
  64. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/publish_data.py +0 -0
  65. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/recipes.py +0 -0
  66. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/secrets.py +0 -0
  67. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/support.py +0 -0
  68. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/commands/todos.py +0 -0
  69. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/.claude-plugin/plugin.json +0 -0
  70. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skill_variants.json +0 -0
  71. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/SKILL.md +0 -0
  72. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/references/annotation_audit.md +0 -0
  73. {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
  74. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/SKILL.md +0 -0
  75. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/annotation_audit.md +0 -0
  76. {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
  77. {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
  78. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-ask/SKILL.md +0 -0
  79. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-handoff/SKILL.md +0 -0
  80. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/SKILL.md +0 -0
  81. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/references/annotation_metrics.md +0 -0
  82. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/references/training_monitoring.md +0 -0
  83. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/SKILL.md +0 -0
  84. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/references/annotation_metrics.md +0 -0
  85. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/references/training_monitoring.md +0 -0
  86. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/scripts/check_training.py +0 -0
  87. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-ops.assistant/SKILL.md +0 -0
  88. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-ops.coding/SKILL.md +0 -0
  89. {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
  90. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-ops.coding/scripts/run_job.py +0 -0
  91. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-patterns/SKILL.md +0 -0
  92. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-patterns/references/pattern_strategies.md +0 -0
  93. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/SKILL.md +0 -0
  94. {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
  95. {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
  96. {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
  97. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_correct.py +0 -0
  98. {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
  99. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_manual.py +0 -0
  100. {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
  101. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_routing.py +0 -0
  102. {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
  103. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_teach.py +0 -0
  104. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/builtin_recipes.md +0 -0
  105. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/ellf_recipe_sdk.md +0 -0
  106. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/lint_recipe.py +0 -0
  107. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/prodigy_recipe_api.md +0 -0
  108. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/template_index.md +0 -0
  109. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/consulting_patterns.md +0 -0
  110. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/explosion_strategy.md +0 -0
  111. {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
  112. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/consulting_patterns.md +0 -0
  113. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/explosion_strategy.md +0 -0
  114. {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
  115. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-support.assistant/SKILL.md +0 -0
  116. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-support.coding/SKILL.md +0 -0
  117. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-todo/SKILL.md +0 -0
  118. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/SKILL.md +0 -0
  119. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/diagnostics.md +0 -0
  120. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/evaluation_guide.md +0 -0
  121. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/model_selection.md +0 -0
  122. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/training_paradigms.md +0 -0
  123. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/workflow.md +0 -0
  124. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/SKILL.md +0 -0
  125. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_advanced.md +0 -0
  126. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_architectures.md +0 -0
  127. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_training.md +0 -0
  128. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/diagnostics.md +0 -0
  129. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/evaluation_guide.md +0 -0
  130. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/experiment_patterns.md +0 -0
  131. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/model_selection.md +0 -0
  132. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/training_paradigms.md +0 -0
  133. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/training_troubleshooting.md +0 -0
  134. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/workflow.md +0 -0
  135. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ellf_skills/skills/ellf-train.coding/scripts/ellf_logger.py +0 -0
  136. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/errors.py +0 -0
  137. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/helm.py +0 -0
  138. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/key_pair.py +0 -0
  139. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/main.py +0 -0
  140. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/query.py +0 -0
  141. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/cookiecutter.json +0 -0
  142. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/.gitignore +0 -0
  143. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/README.md.tmpl +0 -0
  144. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/requirements-dev.in +0 -0
  145. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/requirements.in +0 -0
  146. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/setup.py.tmpl +0 -0
  147. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/__init__.py +0 -0
  148. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/about.py +0 -0
  149. {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
  150. {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
  151. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/testing/__init__.py +0 -0
  152. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ty.py +0 -0
  153. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/ui.py +0 -0
  154. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/url.py +0 -0
  155. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli/util.py +0 -0
  156. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli.egg-info/dependency_links.txt +0 -0
  157. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli.egg-info/entry_points.txt +0 -0
  158. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli.egg-info/not-zip-safe +0 -0
  159. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli.egg-info/requires.txt +0 -0
  160. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/ellf_cli.egg-info/top_level.txt +0 -0
  161. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/pyproject.toml +0 -0
  162. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/setup.cfg +0 -0
  163. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/setup.py +0 -0
  164. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_appdirs.py +0 -0
  165. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_auth.py +0 -0
  166. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_errors.py +0 -0
  167. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_files_cp.py +0 -0
  168. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_files_cp_helpers.py +0 -0
  169. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_info.py +0 -0
  170. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_invalid_secrets.py +0 -0
  171. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_key_pair.py +0 -0
  172. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_login.py +0 -0
  173. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_logout.py +0 -0
  174. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_main.py +0 -0
  175. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_org_select.py +0 -0
  176. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_plans.py +0 -0
  177. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_projects.py +0 -0
  178. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_query.py +0 -0
  179. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_recipe_file.py +0 -0
  180. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_recipes.py +0 -0
  181. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_state.py +0 -0
  182. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_support.py +0 -0
  183. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_ty.py +0 -0
  184. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_ui.py +0 -0
  185. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_ui_extras.py +0 -0
  186. {ellf_cli-14.0.0 → ellf_cli-17.0.4}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ellf-cli
3
- Version: 14.0.0
3
+ Version: 17.0.4
4
4
  Summary: Ellf Command Line Interface
5
5
  Home-page: https://prodi.gy
6
6
  Author: ExplosionAI GmbH
@@ -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: ['Input', 'Model', 'Patterns] | |
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: ['Input', 'Model', 'Patterns] | |
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` |
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "title": "Ellf CLI",
3
3
  "name": "ellf-cli",
4
- "version": "14.0.0",
4
+ "version": "17.0.4",
5
5
  "summary": "Ellf Command Line Interface",
6
6
  "uri": "https://prodi.gy",
7
7
  "prog": "ellf",
@@ -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
 
@@ -23,6 +23,7 @@ from . import (
23
23
  support,
24
24
  tasks,
25
25
  todos,
26
+ users,
26
27
  )
27
28
  from ._state import (
28
29
  cli_context,
@@ -54,6 +55,7 @@ __all__ = [
54
55
  "services",
55
56
  "secrets",
56
57
  "support",
58
+ "users",
57
59
  "general",
58
60
  "import_export",
59
61
  "plans",
@@ -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
- pass
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
- pass
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
- pass
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
- pass
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."""