ellf-cli 5.0.18__tar.gz → 6.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. {ellf_cli-5.0.18/ellf_cli.egg-info → ellf_cli-6.0.0}/PKG-INFO +1 -1
  2. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/README.md +11 -0
  3. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/about.json +1 -1
  4. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/auth.py +45 -1
  5. ellf_cli-6.0.0/ellf_cli/commands/_org_select.py +86 -0
  6. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/general.py +72 -0
  7. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/config.py +5 -2
  8. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf.json +62 -1
  9. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/messages.py +7 -0
  10. {ellf_cli-5.0.18 → ellf_cli-6.0.0/ellf_cli.egg-info}/PKG-INFO +1 -1
  11. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli.egg-info/SOURCES.txt +3 -0
  12. ellf_cli-6.0.0/tests/test_logout.py +126 -0
  13. ellf_cli-6.0.0/tests/test_org_select.py +120 -0
  14. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/LICENSE +0 -0
  15. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/MANIFEST.in +0 -0
  16. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/__init__.py +0 -0
  17. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/__main__.py +0 -0
  18. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/about.py +0 -0
  19. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/appdirs.py +0 -0
  20. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/cli.py +0 -0
  21. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/cloud/__init__.py +0 -0
  22. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/cloud/gcp.py +0 -0
  23. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/cluster_config.py +0 -0
  24. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/__init__.py +0 -0
  25. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/_cluster_select.py +0 -0
  26. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/_recipe_file.py +0 -0
  27. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/_recipe_subcommand.py +0 -0
  28. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/_state.py +0 -0
  29. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/actions.py +0 -0
  30. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/agents.py +0 -0
  31. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/assets.py +0 -0
  32. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/auth.py +0 -0
  33. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/clusters.py +0 -0
  34. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/config.py +0 -0
  35. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/datasets.py +0 -0
  36. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/files/__init__.py +0 -0
  37. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/files/cp.py +0 -0
  38. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/files/ls.py +0 -0
  39. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/files/rm.py +0 -0
  40. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/files/rsync.py +0 -0
  41. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/files/stats.py +0 -0
  42. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/import_export.py +0 -0
  43. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/infra/__init__.py +0 -0
  44. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/infra/_helpers.py +0 -0
  45. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/infra/deploy.py +0 -0
  46. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/infra/init_values.py +0 -0
  47. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/infra/provision.py +0 -0
  48. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/infra/register.py +0 -0
  49. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/infra/setup.py +0 -0
  50. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/infra/start.py +0 -0
  51. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/infra/terraform.py +0 -0
  52. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/infra/tls.py +0 -0
  53. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/jobs.py +0 -0
  54. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/packages.py +0 -0
  55. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/paths.py +0 -0
  56. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/plans.py +0 -0
  57. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/projects.py +0 -0
  58. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/publish_code.py +0 -0
  59. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/publish_data.py +0 -0
  60. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/recipes.py +0 -0
  61. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/secrets.py +0 -0
  62. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/support.py +0 -0
  63. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/tasks.py +0 -0
  64. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/commands/todos.py +0 -0
  65. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/.claude-plugin/plugin.json +0 -0
  66. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/.gitignore +0 -0
  67. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skill_variants.json +0 -0
  68. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/SKILL.md +0 -0
  69. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/references/annotation_audit.md +0 -0
  70. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-annotate.assistant/references/builtin_ellf_annotation_recipes.md +0 -0
  71. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/SKILL.md +0 -0
  72. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/annotation_audit.md +0 -0
  73. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/builtin_ellf_annotation_recipes.md +0 -0
  74. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-annotate.coding/references/builtin_prodigy_recipes.md +0 -0
  75. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-ask/SKILL.md +0 -0
  76. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-handoff/SKILL.md +0 -0
  77. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/SKILL.md +0 -0
  78. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/references/annotation_metrics.md +0 -0
  79. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-monitor.assistant/references/training_monitoring.md +0 -0
  80. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/SKILL.md +0 -0
  81. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/references/annotation_metrics.md +0 -0
  82. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/references/training_monitoring.md +0 -0
  83. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-monitor.coding/scripts/check_training.py +0 -0
  84. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-ops.assistant/SKILL.md +0 -0
  85. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-ops.coding/SKILL.md +0 -0
  86. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-ops.coding/references/data_infra_cli.md +0 -0
  87. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-ops.coding/scripts/run_job.py +0 -0
  88. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-patterns/SKILL.md +0 -0
  89. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-patterns/references/pattern_strategies.md +0 -0
  90. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/SKILL.md +0 -0
  91. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_action_recipe.py +0 -0
  92. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_agent_recipe.py +0 -0
  93. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_blocks_ui.py +0 -0
  94. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_correct.py +0 -0
  95. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_custom_ui.py +0 -0
  96. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_manual.py +0 -0
  97. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_pages_ui.py +0 -0
  98. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_routing.py +0 -0
  99. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_task_recipe.py +0 -0
  100. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/assets/templates/template_teach.py +0 -0
  101. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/builtin_recipes.md +0 -0
  102. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/ellf_recipe_sdk.md +0 -0
  103. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/lint_recipe.py +0 -0
  104. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/prodigy_recipe_api.md +0 -0
  105. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-prodigy/references/template_index.md +0 -0
  106. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-project.assistant/SKILL.md +0 -0
  107. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/consulting_patterns.md +0 -0
  108. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/explosion_strategy.md +0 -0
  109. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-project.assistant/references/prodigy_llm_bot.md +0 -0
  110. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-project.coding/SKILL.md +0 -0
  111. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/consulting_patterns.md +0 -0
  112. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/explosion_strategy.md +0 -0
  113. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-project.coding/references/prodigy_llm_bot.md +0 -0
  114. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-support.assistant/SKILL.md +0 -0
  115. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-support.coding/SKILL.md +0 -0
  116. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-todo/SKILL.md +0 -0
  117. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.assistant/SKILL.md +0 -0
  118. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/diagnostics.md +0 -0
  119. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/evaluation_guide.md +0 -0
  120. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/model_selection.md +0 -0
  121. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/training_paradigms.md +0 -0
  122. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.assistant/references/workflow.md +0 -0
  123. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.coding/SKILL.md +0 -0
  124. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_advanced.md +0 -0
  125. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_architectures.md +0 -0
  126. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/config_training.md +0 -0
  127. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/diagnostics.md +0 -0
  128. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/evaluation_guide.md +0 -0
  129. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/experiment_patterns.md +0 -0
  130. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/model_selection.md +0 -0
  131. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/training_paradigms.md +0 -0
  132. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/training_troubleshooting.md +0 -0
  133. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.coding/references/workflow.md +0 -0
  134. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ellf_skills/skills/ellf-train.coding/scripts/ellf_logger.py +0 -0
  135. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/errors.py +0 -0
  136. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/helm.py +0 -0
  137. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/key_pair.py +0 -0
  138. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/main.py +0 -0
  139. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/query.py +0 -0
  140. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/recipes_cookiecutter/cookiecutter.json +0 -0
  141. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/.gitignore +0 -0
  142. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/README.md.tmpl +0 -0
  143. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/requirements-dev.in +0 -0
  144. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/requirements.in +0 -0
  145. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/setup.py.tmpl +0 -0
  146. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/__init__.py +0 -0
  147. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/about.py +0 -0
  148. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/recipes/__init__.py +0 -0
  149. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/recipes_cookiecutter/{{cookiecutter.package_dir}}/{{cookiecutter.package_name}}/recipes/example_task.py +0 -0
  150. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/testing/__init__.py +0 -0
  151. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ty.py +0 -0
  152. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/ui.py +0 -0
  153. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/url.py +0 -0
  154. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli/util.py +0 -0
  155. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli.egg-info/dependency_links.txt +0 -0
  156. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli.egg-info/entry_points.txt +0 -0
  157. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli.egg-info/not-zip-safe +0 -0
  158. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli.egg-info/requires.txt +0 -0
  159. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/ellf_cli.egg-info/top_level.txt +0 -0
  160. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/pyproject.toml +0 -0
  161. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/setup.cfg +0 -0
  162. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/setup.py +0 -0
  163. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_appdirs.py +0 -0
  164. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_auth.py +0 -0
  165. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_config.py +0 -0
  166. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_errors.py +0 -0
  167. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_files_cp.py +0 -0
  168. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_files_cp_helpers.py +0 -0
  169. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_info.py +0 -0
  170. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_invalid_secrets.py +0 -0
  171. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_key_pair.py +0 -0
  172. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_login.py +0 -0
  173. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_main.py +0 -0
  174. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_plans.py +0 -0
  175. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_projects.py +0 -0
  176. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_query.py +0 -0
  177. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_recipe_file.py +0 -0
  178. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_recipes.py +0 -0
  179. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_state.py +0 -0
  180. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_support.py +0 -0
  181. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_ty.py +0 -0
  182. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_ui.py +0 -0
  183. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_ui_extras.py +0 -0
  184. {ellf_cli-5.0.18 → ellf_cli-6.0.0}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ellf-cli
3
- Version: 5.0.18
3
+ Version: 6.0.0
4
4
  Summary: Ellf Command Line Interface
5
5
  Home-page: https://prodi.gy
6
6
  Author: ExplosionAI GmbH
@@ -1069,11 +1069,22 @@ Log in to your Ellf account. You normally don't need to call this manually. It w
1069
1069
  | Argument | Type | Description | Default |
1070
1070
  | --- | --- | --- | --- |
1071
1071
  | `--no-cluster` | `bool` | Don't use a cluster | `False` |
1072
+ | `--no-org` | `bool` | Skip the organization selection step (stay in the org that the device-flow login lands in) | `False` |
1072
1073
  | `--no-browser` | `bool` | Don't open a browser, just print the login URL | `False` |
1074
+ | `--org` | `str` | Name or ID of the organization to log into. If omitted: auto-select when only one is available, or prompt interactively | `None` |
1073
1075
  | `--cluster` | `str` | Name or ID of the cluster to log into. If omitted: auto-select when only one is available, or prompt interactively | `None` |
1074
1076
  | `--json` | `bool` | Output the result as JSON | `False` |
1075
1077
  | `--claude` | `bool` | Install Ellf Claude Code skills and transcript hook into ~/.claude/ | `False` |
1076
1078
 
1079
+ ### `ellf logout`
1080
+
1081
+ Log out by deleting locally-stored auth tokens. PAM bearer tokens can't be revoked server-side, so this is a local operation: subsequent commands will trigger a fresh device-flow login. The cached tokens would have expired on their own within roughly an hour. Use ``--all`` to also reset the saved defaults (active org, cluster, project, etc.), e.g. when handing the machine to a different user.
1082
+
1083
+ | Argument | Type | Description | Default |
1084
+ | --- | --- | --- | --- |
1085
+ | `--all` | `bool` | Also clear saved defaults (broker host, active org/cluster/project/task/agent), not just the auth tokens | `False` |
1086
+ | `--json` | `bool` | Output the result as JSON | `False` |
1087
+
1077
1088
  ### `ellf info`
1078
1089
 
1079
1090
  Print information about the CLI
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "title": "Ellf CLI",
3
3
  "name": "ellf-cli",
4
- "version": "5.0.18",
4
+ "version": "6.0.0",
5
5
  "summary": "Ellf Command Line Interface",
6
6
  "uri": "https://prodi.gy",
7
7
  "prog": "ellf",
@@ -169,6 +169,8 @@ class AuthState(Protocol):
169
169
 
170
170
  def set_active_cluster(self, cluster_id: UUID, broker_url: URL) -> None: ...
171
171
 
172
+ def set_active_org(self, org_id: UUID) -> AccessTokenCredential: ...
173
+
172
174
  def get_id_token(self, force_refresh: bool = False) -> str: ...
173
175
 
174
176
  def get_api_token(self, force_refresh: bool = False) -> AccessTokenCredential: ...
@@ -208,7 +210,11 @@ class AuthStateImpl:
208
210
  # PAM lookup-by-address every time the user runs an ellf command.
209
211
  # `auth.cluster_id` falls back to the address lookup only if this is None.
210
212
  self._cluster_id = settings.cluster_id
211
- self._org_id = None
213
+ # Seed org_id from saved settings too: the user's api_token already
214
+ # carries the org_id in its `oid` claim, but we don't decode the JWT
215
+ # here. The saved setting reflects whatever was chosen at the last
216
+ # `ellf login` and stays in sync via set_active_org.
217
+ self._org_id = settings.org_id
212
218
  self._secrets_path = ctx.secrets_path
213
219
  self._saved_settings_path = ctx.saved_settings_path
214
220
  self._client = None
@@ -379,6 +385,44 @@ class AuthStateImpl:
379
385
  self._cluster_id = cluster_id
380
386
  self._cluster_client = None
381
387
 
388
+ def set_active_org(self, org_id: UUID) -> AccessTokenCredential:
389
+ """Exchange the current api_token for one scoped to ``org_id``.
390
+
391
+ PAM tokens carry an org_id claim (each user-org pair is a separate
392
+ ``models.User`` record), so "switching org" is a token-exchange,
393
+ not a local-only change. We POST to ``/v1/token/switch-org`` with
394
+ the current api_token and persist the response in secrets. Any
395
+ cached cluster-scoped broker tokens are dropped because they were
396
+ derived from the old api_token's org.
397
+
398
+ Persisting ``org_id`` to ``SavedSettings`` is the caller's
399
+ responsibility.
400
+ """
401
+ api_token = self.get_api_token().access_token
402
+ res = httpx.post(
403
+ str(self.pam_url / "api/v1/token/switch-org"),
404
+ headers={"authorization": f"Bearer {api_token}"},
405
+ json={"org_id": str(org_id)},
406
+ timeout=AUTH_REQUEST_TIMEOUT_SECONDS,
407
+ )
408
+ if res.status_code != 200:
409
+ err = _format_http_error(res)
410
+ if res.status_code == 401:
411
+ raise EllfIDTokenError(err)
412
+ raise EllfError(Messages.E137.format(org_id=org_id) + f": {err}")
413
+ data = res.json()
414
+ new_token = AccessTokenCredential(access_token=data["access_token"])
415
+ self.secrets.api_token = new_token
416
+ # Broker tokens were minted for the previous org; clear them so
417
+ # the next get_cluster_token re-issues against the new org.
418
+ self.secrets.broker_tokens = {}
419
+ if self._secrets_path is not None:
420
+ self.secrets.save(self._secrets_path)
421
+ self._org_id = org_id
422
+ self._client = None
423
+ self._cluster_client = None
424
+ return new_token
425
+
382
426
  @staticmethod
383
427
  def _check_token_expired(
384
428
  token: AccessTokenCredential | BrokerAccessTokenCredential | str,
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import builtins
4
+ import sys
5
+ from typing import Union
6
+ from uuid import UUID
7
+
8
+ from wasabi import msg
9
+
10
+ from ellf_pam_sdk import Client
11
+ from ellf_pam_sdk.models import OrgMembership
12
+
13
+ from ..errors import CLIError
14
+ from ..messages import Messages
15
+ from ..ui import isatty
16
+
17
+
18
+ def list_person_orgs(client: Client) -> builtins.list[OrgMembership]:
19
+ """Return all org memberships for the authenticated person.
20
+
21
+ The PAM ``org.all()`` listing is org-scoped by the api_token, so it
22
+ only returns the *current* org. To enumerate everywhere this person
23
+ can sign in we hit ``/api/v1/person/orgs`` directly — the same
24
+ endpoint the web app's org switcher uses.
25
+ """
26
+ response = client._sync_client.post("/api/v1/person/orgs")
27
+ response.raise_for_status()
28
+ return [OrgMembership.model_validate(row) for row in response.json()]
29
+
30
+
31
+ def _match_org(
32
+ orgs: builtins.list[OrgMembership],
33
+ name_or_id: Union[str, UUID],
34
+ ) -> OrgMembership | None:
35
+ if isinstance(name_or_id, UUID):
36
+ for o in orgs:
37
+ if o.org_id == name_or_id:
38
+ return o
39
+ return None
40
+ needle = str(name_or_id)
41
+ for o in orgs:
42
+ if str(o.org_id) == needle or o.org_name == needle:
43
+ return o
44
+ return None
45
+
46
+
47
+ def _prompt_for_org(orgs: builtins.list[OrgMembership]) -> OrgMembership:
48
+ if not isatty(sys.stdin) or not isatty(sys.stdout):
49
+ raise CLIError(Messages.E136)
50
+ msg.info("Multiple organizations are available:")
51
+ for idx, org in enumerate(orgs, start=1):
52
+ print(f" [{idx}] {org.org_name} ({org.org_id})")
53
+ while True:
54
+ raw = input(f"Pick an organization [1-{len(orgs)}]: ").strip()
55
+ if raw.isdigit():
56
+ choice = int(raw)
57
+ if 1 <= choice <= len(orgs):
58
+ return orgs[choice - 1]
59
+ matched = _match_org(orgs, raw)
60
+ if matched is not None:
61
+ return matched
62
+ msg.warn(f"Invalid selection: {raw!r}")
63
+
64
+
65
+ def select_org(
66
+ client: Client,
67
+ name_or_id: Union[str, UUID, None] = None,
68
+ ) -> OrgMembership:
69
+ """Pick an org the current person belongs to.
70
+
71
+ When ``name_or_id`` is provided, looks it up exactly. Otherwise:
72
+ auto-selects the only org, or prompts in a TTY when there are
73
+ multiple. Raises ``CLIError`` if the person has no orgs or the
74
+ caller is non-interactive with multiple orgs.
75
+ """
76
+ orgs = list_person_orgs(client)
77
+ if not orgs:
78
+ raise CLIError(Messages.E135)
79
+ if name_or_id is not None:
80
+ matched = _match_org(orgs, name_or_id)
81
+ if matched is None:
82
+ raise CLIError(Messages.E038.format(noun="org", name_or_id=name_or_id))
83
+ return matched
84
+ if len(orgs) == 1:
85
+ return orgs[0]
86
+ return _prompt_for_org(orgs)
@@ -17,6 +17,7 @@ from ..messages import Messages
17
17
  from ..ui import print_as_json, print_mutation_result
18
18
  from ..util import URL
19
19
  from ._cluster_select import select_cluster
20
+ from ._org_select import select_org
20
21
  from ._state import get_auth_state, get_root_cfg, get_saved_settings
21
22
 
22
23
 
@@ -62,14 +63,18 @@ def _install_claude_skills() -> None:
62
63
  @cli.command(
63
64
  "login",
64
65
  no_cluster=Arg("--no-cluster", help=Messages.no_cluster),
66
+ no_org=Arg("--no-org", help=Messages.no_org),
65
67
  no_browser=Arg("--no-browser", help=Messages.no_browser),
68
+ org=Arg("--org", help=Messages.login_org),
66
69
  cluster=Arg("--cluster", help=Messages.login_cluster),
67
70
  as_json=Arg("--json", help=Messages.as_json),
68
71
  claude=Arg("--claude", help=Messages.claude),
69
72
  )
70
73
  def login(
71
74
  no_cluster: bool = False,
75
+ no_org: bool = False,
72
76
  no_browser: bool = False,
77
+ org: Optional[Union[str, UUID]] = None,
73
78
  cluster: Optional[Union[str, UUID]] = None,
74
79
  as_json: bool = False,
75
80
  claude: bool = False,
@@ -82,6 +87,11 @@ def login(
82
87
  auth.no_browser = no_browser
83
88
  auth._ensure_readable_secrets()
84
89
  auth.get_api_token(force_refresh=True)
90
+ if not no_org:
91
+ # Org selection happens before cluster selection so cluster lookups
92
+ # below run against the chosen org's api_token, not whatever org
93
+ # the device-flow login happened to land in.
94
+ _select_and_persist_org(auth, org)
85
95
  if not no_cluster:
86
96
  try:
87
97
  _select_and_persist_cluster(auth, cluster)
@@ -98,6 +108,68 @@ def login(
98
108
  _install_claude_skills()
99
109
 
100
110
 
111
+ @cli.command(
112
+ "logout",
113
+ all_=Arg("--all", help=Messages.logout_all),
114
+ as_json=Arg("--json", help=Messages.as_json),
115
+ )
116
+ def logout(all_: bool = False, as_json: bool = False) -> None:
117
+ """
118
+ Log out by deleting locally-stored auth tokens.
119
+
120
+ PAM bearer tokens can't be revoked server-side, so this is a local
121
+ operation: subsequent commands will trigger a fresh device-flow
122
+ login. The cached tokens would have expired on their own within
123
+ roughly an hour. Use ``--all`` to also reset the saved defaults
124
+ (active org, cluster, project, etc.), e.g. when handing the
125
+ machine to a different user.
126
+ """
127
+ from ..auth import FileSecrets
128
+
129
+ ctx = get_root_cfg()
130
+ secrets_existed = ctx.secrets_path.exists()
131
+ FileSecrets.clean(ctx.secrets_path)
132
+ settings_reset = False
133
+ if all_:
134
+ settings_path = ctx.saved_settings_path
135
+ if settings_path.exists():
136
+ SavedSettings.blank().save(settings_path)
137
+ settings_reset = True
138
+ print_mutation_result(
139
+ {
140
+ "status": "ok",
141
+ "secrets_removed": secrets_existed,
142
+ "settings_reset": settings_reset,
143
+ },
144
+ Messages.T013,
145
+ as_json=as_json,
146
+ )
147
+
148
+
149
+ def _select_and_persist_org(
150
+ auth: AuthState,
151
+ name_or_id: Optional[Union[str, UUID]] = None,
152
+ ) -> UUID:
153
+ """Pick an org via pam and write it into SavedSettings.
154
+
155
+ If the chosen org differs from the current api_token's org, calls
156
+ ``/v1/token/switch-org`` to re-issue the token for the target org.
157
+ The first time a person logs in here we always have *some* api_token
158
+ (from the device-flow login), but we don't know which org it landed
159
+ in without a round-trip, so the switch is unconditional when an org
160
+ is explicitly selected — that's cheap and avoids a JWT decode here.
161
+ """
162
+ chosen = select_org(auth.client, name_or_id)
163
+ settings = get_saved_settings()
164
+ previous_org_id = settings.org_id
165
+ if previous_org_id != chosen.org_id:
166
+ auth.set_active_org(chosen.org_id)
167
+ settings.update("org_id", chosen.org_id)
168
+ settings.save(get_root_cfg().saved_settings_path)
169
+ msg.good(f"Logged in to organization '{chosen.org_name}'")
170
+ return chosen.org_id
171
+
172
+
101
173
  def _select_and_persist_cluster(
102
174
  auth: AuthState,
103
175
  name_or_id: Optional[Union[str, UUID]] = None,
@@ -40,6 +40,7 @@ class SavedSettings(BaseModel):
40
40
  agent: UUID | None = None
41
41
  pam_host: str | None = DEFAULT_PAM_HOST
42
42
  cluster_id: UUID | None = None
43
+ org_id: UUID | None = None
43
44
  recipes_file: str | None = None
44
45
 
45
46
  @classmethod
@@ -52,6 +53,7 @@ class SavedSettings(BaseModel):
52
53
  agent=None,
53
54
  pam_host=DEFAULT_PAM_HOST,
54
55
  cluster_id=None,
56
+ org_id=None,
55
57
  recipes_file=None,
56
58
  )
57
59
 
@@ -96,7 +98,7 @@ class SavedSettings(BaseModel):
96
98
  @overload
97
99
  def update(
98
100
  self,
99
- field: Literal["project", "task", "action", "agent", "cluster_id"],
101
+ field: Literal["project", "task", "action", "agent", "cluster_id", "org_id"],
100
102
  value: UUID | None = None,
101
103
  ) -> UUID | None: ...
102
104
 
@@ -110,12 +112,13 @@ class SavedSettings(BaseModel):
110
112
  "action",
111
113
  "agent",
112
114
  "cluster_id",
115
+ "org_id",
113
116
  "recipes_file",
114
117
  ],
115
118
  value: str | UUID | None = None,
116
119
  ) -> str | UUID | None:
117
120
  old_value = getattr(self, field)
118
121
  setattr(self, field, value)
119
- if old_value != value and field in ["broker_host", "pam_host"]:
122
+ if old_value != value and field in ["broker_host", "pam_host", "org_id"]:
120
123
  self.reset_defaults()
121
124
  return getattr(self, field)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "prog": "ellf",
3
3
  "help": "Ellf Command Line Interface.",
4
- "version": "5.0.17",
4
+ "version": "5.0.18",
5
5
  "extra_key": "_extra",
6
6
  "commands": {
7
7
  "actions": {
@@ -164,6 +164,19 @@
164
164
  "type": null,
165
165
  "orig_type": "bool"
166
166
  },
167
+ {
168
+ "id": "no_org",
169
+ "option": "--no-org",
170
+ "short": null,
171
+ "orig_help": "Skip the organization selection step (stay in the org that the device-flow login lands in)",
172
+ "default": false,
173
+ "help": "Skip the organization selection step (stay in the org that the device-flow login lands in) (bool)",
174
+ "action": "store_true",
175
+ "choices": null,
176
+ "has_converter": false,
177
+ "type": null,
178
+ "orig_type": "bool"
179
+ },
167
180
  {
168
181
  "id": "no_browser",
169
182
  "option": "--no-browser",
@@ -177,6 +190,19 @@
177
190
  "type": null,
178
191
  "orig_type": "bool"
179
192
  },
193
+ {
194
+ "id": "org",
195
+ "option": "--org",
196
+ "short": null,
197
+ "orig_help": "Name or ID of the organization to log into. If omitted: auto-select when only one is available, or prompt interactively",
198
+ "default": null,
199
+ "help": "Name or ID of the organization to log into. If omitted: auto-select when only one is available, or prompt interactively (str)",
200
+ "action": null,
201
+ "choices": null,
202
+ "has_converter": false,
203
+ "type": "str",
204
+ "orig_type": "str"
205
+ },
180
206
  {
181
207
  "id": "cluster",
182
208
  "option": "--cluster",
@@ -222,6 +248,41 @@
222
248
  "parent": null,
223
249
  "is_placeholder": false
224
250
  },
251
+ "logout": {
252
+ "name": "logout",
253
+ "args": [
254
+ {
255
+ "id": "all_",
256
+ "option": "--all",
257
+ "short": null,
258
+ "orig_help": "Also clear saved defaults (broker host, active org/cluster/project/task/agent), not just the auth tokens",
259
+ "default": false,
260
+ "help": "Also clear saved defaults (broker host, active org/cluster/project/task/agent), not just the auth tokens (bool)",
261
+ "action": "store_true",
262
+ "choices": null,
263
+ "has_converter": false,
264
+ "type": null,
265
+ "orig_type": "bool"
266
+ },
267
+ {
268
+ "id": "as_json",
269
+ "option": "--json",
270
+ "short": null,
271
+ "orig_help": "Output the result as JSON",
272
+ "default": false,
273
+ "help": "Output the result as JSON (bool)",
274
+ "action": "store_true",
275
+ "choices": null,
276
+ "has_converter": false,
277
+ "type": null,
278
+ "orig_type": "bool"
279
+ }
280
+ ],
281
+ "description": "\nLog out by deleting locally-stored auth tokens.\n\nPAM bearer tokens can't be revoked server-side, so this is a local\noperation: subsequent commands will trigger a fresh device-flow\nlogin. The cached tokens would have expired on their own within\nroughly an hour. Use ``--all`` to also reset the saved defaults\n(active org, cluster, project, etc.), e.g. when handing the\nmachine to a different user.\n",
282
+ "allow_extra": false,
283
+ "parent": null,
284
+ "is_placeholder": false
285
+ },
225
286
  "info": {
226
287
  "name": "info",
227
288
  "args": [
@@ -75,6 +75,7 @@ ellf secret create my-credentials OPENAI_API_KEY="sk-..." CUSTOM_SECRET=-
75
75
  T009 = "Successfully deregistered {noun} {name}"
76
76
  T011 = "Successfully copied files"
77
77
  T012 = "Login successful"
78
+ T013 = "Logout successful"
78
79
  T014 = "Opening browser to authenticate..."
79
80
  T015 = "If a browser doesn't open automatically after some seconds, you can copy and open this link:"
80
81
  T016 = "Alternatively, you can manually open:"
@@ -170,8 +171,11 @@ ellf secret create my-credentials OPENAI_API_KEY="sk-..." CUSTOM_SECRET=-
170
171
  output_dir = "Output directory for the {noun}"
171
172
  pam_host_config = "Host or URL of the Prodigy Annotation Manager (PAM) app"
172
173
  no_cluster = "Don't use a cluster"
174
+ no_org = "Skip the organization selection step (stay in the org that the device-flow login lands in)"
173
175
  no_browser = "Don't open a browser, just print the login URL"
176
+ logout_all = "Also clear saved defaults (broker host, active org/cluster/project/task/agent), not just the auth tokens"
174
177
  login_cluster = "Name or ID of the cluster to log into. If omitted: auto-select when only one is available, or prompt interactively"
178
+ login_org = "Name or ID of the organization to log into. If omitted: auto-select when only one is available, or prompt interactively"
175
179
  claude = "Install Ellf Claude Code skills and transcript hook into ~/.claude/"
176
180
  use_active_venv = "Use the currently active virtualenv, instead of making a temporary one"
177
181
  filter_by = "Filter by {filter}"
@@ -217,6 +221,9 @@ ellf secret create my-credentials OPENAI_API_KEY="sk-..." CUSTOM_SECRET=-
217
221
  E118 = "Error requesting recipe schemas"
218
222
  E121 = "Invalid JSON for meta"
219
223
  E128 = "Cannot parse path: {path}"
224
+ E135 = "No organizations are accessible for this account"
225
+ E136 = "Multiple organizations available. Run `ellf login --org <name>` to pick one"
226
+ E137 = "Could not switch to organization {org_id}"
220
227
  E144 = "Not a valid Python package: name should end in .tar.gz or .whl"
221
228
  E145 = "No valid meta.json found in package"
222
229
  E146 = "Invalid meta.json key ellf_recipes"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ellf-cli
3
- Version: 5.0.18
3
+ Version: 6.0.0
4
4
  Summary: Ellf Command Line Interface
5
5
  Home-page: https://prodi.gy
6
6
  Author: ExplosionAI GmbH
@@ -35,6 +35,7 @@ ellf_cli/cloud/__init__.py
35
35
  ellf_cli/cloud/gcp.py
36
36
  ellf_cli/commands/__init__.py
37
37
  ellf_cli/commands/_cluster_select.py
38
+ ellf_cli/commands/_org_select.py
38
39
  ellf_cli/commands/_recipe_file.py
39
40
  ellf_cli/commands/_recipe_subcommand.py
40
41
  ellf_cli/commands/_state.py
@@ -166,7 +167,9 @@ tests/test_info.py
166
167
  tests/test_invalid_secrets.py
167
168
  tests/test_key_pair.py
168
169
  tests/test_login.py
170
+ tests/test_logout.py
169
171
  tests/test_main.py
172
+ tests/test_org_select.py
170
173
  tests/test_plans.py
171
174
  tests/test_projects.py
172
175
  tests/test_query.py
@@ -0,0 +1,126 @@
1
+ from uuid import uuid4
2
+
3
+ from ellf_cli.auth import FileSecrets
4
+ from ellf_cli.commands._state import get_root_cfg, get_saved_settings
5
+ from ellf_cli.commands.general import logout
6
+ from ellf_cli.config import DEFAULT_PAM_HOST, SavedSettings
7
+
8
+
9
+ def _seed_secrets() -> None:
10
+ """Write a secrets.json with a sentinel id_token at the active config path."""
11
+ ctx = get_root_cfg()
12
+ ctx.secrets_path.parent.mkdir(parents=True, exist_ok=True)
13
+ FileSecrets(id_token="sentinel-id-token").save(ctx.secrets_path)
14
+
15
+
16
+ def _seed_settings() -> None:
17
+ """Write saved-defaults.json populated with workflow defaults."""
18
+ ctx = get_root_cfg()
19
+ settings = SavedSettings(
20
+ broker_host="https://cluster.example.com",
21
+ project=uuid4(),
22
+ task=uuid4(),
23
+ action=uuid4(),
24
+ agent=uuid4(),
25
+ pam_host="https://pam.example.com",
26
+ cluster_id=uuid4(),
27
+ org_id=uuid4(),
28
+ recipes_file="/tmp/recipes.json",
29
+ )
30
+ settings.save(ctx.saved_settings_path)
31
+
32
+
33
+ def test_logout_removes_secrets_file():
34
+ _seed_secrets()
35
+ ctx = get_root_cfg()
36
+ assert ctx.secrets_path.exists()
37
+ logout()
38
+ assert not ctx.secrets_path.exists()
39
+
40
+
41
+ def test_logout_is_idempotent_when_no_secrets():
42
+ ctx = get_root_cfg()
43
+ assert not ctx.secrets_path.exists()
44
+ # Should not raise even when nothing to clean up.
45
+ logout()
46
+ assert not ctx.secrets_path.exists()
47
+
48
+
49
+ def test_logout_default_keeps_saved_settings():
50
+ _seed_secrets()
51
+ _seed_settings()
52
+ ctx = get_root_cfg()
53
+ logout()
54
+ # secrets gone, saved-defaults untouched
55
+ assert not ctx.secrets_path.exists()
56
+ persisted = SavedSettings.from_file(ctx.saved_settings_path)
57
+ assert persisted.org_id is not None
58
+ assert persisted.cluster_id is not None
59
+ assert persisted.broker_host == "https://cluster.example.com"
60
+
61
+
62
+ def test_logout_all_resets_saved_settings():
63
+ _seed_secrets()
64
+ _seed_settings()
65
+ ctx = get_root_cfg()
66
+ logout(all_=True)
67
+ assert not ctx.secrets_path.exists()
68
+ persisted = SavedSettings.from_file(ctx.saved_settings_path)
69
+ # `blank()` zeroes everything except pam_host, which defaults back
70
+ # to DEFAULT_PAM_HOST so the next `ellf login` still knows where
71
+ # to talk.
72
+ assert persisted.org_id is None
73
+ assert persisted.cluster_id is None
74
+ assert persisted.broker_host is None
75
+ assert persisted.project is None
76
+ assert persisted.task is None
77
+ assert persisted.action is None
78
+ assert persisted.agent is None
79
+ assert persisted.recipes_file is None
80
+ assert persisted.pam_host == DEFAULT_PAM_HOST
81
+
82
+
83
+ def test_logout_all_without_existing_settings_file():
84
+ """--all on a machine that never wrote saved-defaults.json shouldn't
85
+ create one. Otherwise we'd leave a sticky artifact on what should be
86
+ a "clean slate" command.
87
+ """
88
+ _seed_secrets()
89
+ ctx = get_root_cfg()
90
+ # Defensive: confirm settings file isn't there
91
+ if ctx.saved_settings_path.exists():
92
+ ctx.saved_settings_path.unlink()
93
+ logout(all_=True)
94
+ assert not ctx.secrets_path.exists()
95
+ assert not ctx.saved_settings_path.exists()
96
+
97
+
98
+ def test_logout_does_not_invoke_network(monkeypatch):
99
+ """logout is a local operation — no PAM round-trip required."""
100
+ import httpx
101
+
102
+ def _explode(*_args, **_kwargs):
103
+ raise AssertionError("logout must not touch the network")
104
+
105
+ monkeypatch.setattr(httpx, "post", _explode)
106
+ monkeypatch.setattr(httpx, "get", _explode)
107
+ _seed_secrets()
108
+ logout()
109
+ # If we got here without the assertion firing, no network call happened.
110
+
111
+
112
+ def test_logout_clears_get_saved_settings_cache_independence():
113
+ """The in-memory cached SavedSettings from a prior get_saved_settings()
114
+ call is not mutated by `logout` itself — it just rewrites the file.
115
+ The next process invocation will pick up the new on-disk state.
116
+ """
117
+ _seed_secrets()
118
+ _seed_settings()
119
+ cached = get_saved_settings()
120
+ assert cached.org_id is not None
121
+ logout(all_=True)
122
+ # Re-read from disk to verify the persisted state, since the cached
123
+ # object lives in a ContextVar and isn't refreshed in-process.
124
+ ctx = get_root_cfg()
125
+ on_disk = SavedSettings.from_file(ctx.saved_settings_path)
126
+ assert on_disk.org_id is None