mammoth-cli 2.2.47__tar.gz → 2.2.48__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 (267) hide show
  1. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/PKG-INFO +2 -2
  2. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/__init__.py +1 -1
  3. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/SKILL.md +1 -1
  4. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/auth.md +5 -2
  5. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/view.md +1 -1
  6. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/auth-scope.md +3 -2
  7. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recovery.md +3 -2
  8. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/auth.py +51 -23
  9. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/job.py +3 -1
  10. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/project.py +2 -1
  11. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/schema.py +1 -0
  12. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/view.py +43 -1
  13. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/context/oauth.py +122 -1
  14. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/errors/envelope.py +3 -1
  15. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/dataset_health.py +2 -1
  16. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/job_outcome.py +23 -0
  17. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/mapping.py +22 -1
  18. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/positionals.py +3 -0
  19. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/pyproject.toml +2 -2
  20. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/view.yaml +3 -3
  21. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/.gitignore +0 -0
  22. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/LICENSE +0 -0
  23. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/README.md +0 -0
  24. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/__main__.py +0 -0
  25. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/app.py +0 -0
  26. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/about-mammoth.md +0 -0
  27. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/capabilities-misc.md +0 -0
  28. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/capabilities.md +0 -0
  29. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/command-index.md +0 -0
  30. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/activity.md +0 -0
  31. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/addon.md +0 -0
  32. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/agent.md +0 -0
  33. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/ai.md +0 -0
  34. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/annotation.md +0 -0
  35. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/auth.md +0 -0
  36. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/automation.md +0 -0
  37. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/batch.md +0 -0
  38. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/billing.md +0 -0
  39. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/browse.md +0 -0
  40. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/calc.md +0 -0
  41. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/capability.md +0 -0
  42. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/client-app.md +0 -0
  43. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/completion.md +0 -0
  44. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/config.md +0 -0
  45. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/connector.md +0 -0
  46. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/context.md +0 -0
  47. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard-qa.md +0 -0
  48. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard-style.md +0 -0
  49. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard-template.md +0 -0
  50. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard.md +0 -0
  51. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/data-app.md +0 -0
  52. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dataset.md +0 -0
  53. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/doctor.md +0 -0
  54. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/external-key.md +0 -0
  55. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/file.md +0 -0
  56. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/folder.md +0 -0
  57. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/job.md +0 -0
  58. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/link.md +0 -0
  59. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/log.md +0 -0
  60. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/notification.md +0 -0
  61. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/parameter.md +0 -0
  62. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/project.md +0 -0
  63. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/report.md +0 -0
  64. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/resolve.md +0 -0
  65. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/schedule.md +0 -0
  66. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/schema.md +0 -0
  67. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/skill.md +0 -0
  68. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/snippet.md +0 -0
  69. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/support.md +0 -0
  70. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/template.md +0 -0
  71. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/token.md +0 -0
  72. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/trash.md +0 -0
  73. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/upgrade.md +0 -0
  74. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/user.md +0 -0
  75. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/version.md +0 -0
  76. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/view-transform.md +0 -0
  77. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/webhook.md +0 -0
  78. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/workflow.md +0 -0
  79. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/workspace.md +0 -0
  80. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/handoff.md +0 -0
  81. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/input.md +0 -0
  82. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/jobs-drafts.md +0 -0
  83. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/machine-output.md +0 -0
  84. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/operations.md +0 -0
  85. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/cleanup.md +0 -0
  86. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/dashboards.md +0 -0
  87. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/database-import.md +0 -0
  88. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/end-to-end.md +0 -0
  89. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/exports.md +0 -0
  90. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/index.md +0 -0
  91. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/need-action.md +0 -0
  92. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/resources.md +0 -0
  93. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/scheduling.md +0 -0
  94. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/transforms.md +0 -0
  95. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/typed-etl-discovery.md +0 -0
  96. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/report-checklist.md +0 -0
  97. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/retention.md +0 -0
  98. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/safety.md +0 -0
  99. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/bundled_skill/mammoth-cli/references/task-start.md +0 -0
  100. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/__init__.py +0 -0
  101. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/activity.py +0 -0
  102. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/addon.py +0 -0
  103. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/agent.py +0 -0
  104. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/ai.py +0 -0
  105. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/annotation.py +0 -0
  106. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/automation.py +0 -0
  107. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/batch.py +0 -0
  108. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/billing.py +0 -0
  109. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/browse.py +0 -0
  110. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/calc.py +0 -0
  111. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/capability.py +0 -0
  112. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/client_app.py +0 -0
  113. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/completion.py +0 -0
  114. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/config.py +0 -0
  115. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/connector.py +0 -0
  116. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/context.py +0 -0
  117. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/dashboard.py +0 -0
  118. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/data_app.py +0 -0
  119. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/dataset.py +0 -0
  120. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/doctor.py +0 -0
  121. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/external_key.py +0 -0
  122. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/file.py +0 -0
  123. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/folder.py +0 -0
  124. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/link.py +0 -0
  125. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/notification.py +0 -0
  126. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/parameter.py +0 -0
  127. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/registry.py +0 -0
  128. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/report.py +0 -0
  129. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/resolve.py +0 -0
  130. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/schedule.py +0 -0
  131. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/skill.py +0 -0
  132. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/snippet.py +0 -0
  133. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/support.py +0 -0
  134. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/template.py +0 -0
  135. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/trash.py +0 -0
  136. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/upgrade.py +0 -0
  137. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/user.py +0 -0
  138. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/view_ops.py +0 -0
  139. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/view_profile.py +0 -0
  140. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/webhook.py +0 -0
  141. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/workflow.py +0 -0
  142. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/commands/workspace.py +0 -0
  143. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/context/__init__.py +0 -0
  144. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/context/credentials.py +0 -0
  145. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/context/endpoint.py +0 -0
  146. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/context/profiles.py +0 -0
  147. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/context/resolver.py +0 -0
  148. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/contracts/__init__.py +0 -0
  149. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/contracts/auth.py +0 -0
  150. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/embed.py +0 -0
  151. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/errors/__init__.py +0 -0
  152. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/manifest/__init__.py +0 -0
  153. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/manifest/loader.py +0 -0
  154. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/messages/__init__.py +0 -0
  155. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/output/__init__.py +0 -0
  156. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/output/envelope.py +0 -0
  157. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/output/normalize.py +0 -0
  158. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/output/policy.py +0 -0
  159. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/output/progress.py +0 -0
  160. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/output/render.py +0 -0
  161. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/py.typed +0 -0
  162. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/__init__.py +0 -0
  163. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/confirm.py +0 -0
  164. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/dryrun.py +0 -0
  165. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/dryrun_targets.py +0 -0
  166. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/embedded.py +0 -0
  167. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/executor.py +0 -0
  168. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/input_loader.py +0 -0
  169. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/intent_only.py +0 -0
  170. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/invocation.py +0 -0
  171. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/locked_files.py +0 -0
  172. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/new_data.py +0 -0
  173. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/options.py +0 -0
  174. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/parents.py +0 -0
  175. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/runlog.py +0 -0
  176. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/session.py +0 -0
  177. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/state.py +0 -0
  178. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/strict.py +0 -0
  179. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/updates.py +0 -0
  180. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/validate.py +0 -0
  181. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/runtime/verify.py +0 -0
  182. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/__init__.py +0 -0
  183. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/append_blank_columns.py +0 -0
  184. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/argspec.py +0 -0
  185. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/board_values.py +0 -0
  186. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/coerce.py +0 -0
  187. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/command_contract.py +0 -0
  188. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/conditional_format.py +0 -0
  189. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/conditions.py +0 -0
  190. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/dashboard_filters.py +0 -0
  191. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/dashboard_pages.py +0 -0
  192. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/dashboard_review.py +0 -0
  193. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/data_profile.py +0 -0
  194. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/data_quality.py +0 -0
  195. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/dispatch.py +0 -0
  196. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/factory.py +0 -0
  197. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/input_fields.py +0 -0
  198. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/listing.py +0 -0
  199. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/local_time.py +0 -0
  200. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/openapi_types.py +0 -0
  201. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/protocol.py +0 -0
  202. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/read_queries.py +0 -0
  203. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/sdk_service.py +0 -0
  204. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/sql_check.py +0 -0
  205. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/stored_stats.py +0 -0
  206. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/testing.py +0 -0
  207. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/text_dates.py +0 -0
  208. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/type_system.py +0 -0
  209. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/view_analysis.py +0 -0
  210. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/services/write_impact.py +0 -0
  211. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/skills/__init__.py +0 -0
  212. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/skills/installer.py +0 -0
  213. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/skills/steering.py +0 -0
  214. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/mammoth_cli/testing.py +0 -0
  215. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/activity.yaml +0 -0
  216. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/addon.yaml +0 -0
  217. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/agent.yaml +0 -0
  218. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/ai.yaml +0 -0
  219. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/annotation.yaml +0 -0
  220. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/auth.yaml +0 -0
  221. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/automation.yaml +0 -0
  222. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/batch.yaml +0 -0
  223. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/billing.yaml +0 -0
  224. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/browse.yaml +0 -0
  225. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/calc.yaml +0 -0
  226. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/capability.yaml +0 -0
  227. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/client-app.yaml +0 -0
  228. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/completion.yaml +0 -0
  229. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/config.yaml +0 -0
  230. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/connector.yaml +0 -0
  231. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/context.yaml +0 -0
  232. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/dashboard.yaml +0 -0
  233. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/data-app.yaml +0 -0
  234. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/dataset.yaml +0 -0
  235. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/doctor.yaml +0 -0
  236. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/external-key.yaml +0 -0
  237. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/file.yaml +0 -0
  238. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/folder.yaml +0 -0
  239. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/job.yaml +0 -0
  240. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/link.yaml +0 -0
  241. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/log.yaml +0 -0
  242. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/notification.yaml +0 -0
  243. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/parameter.yaml +0 -0
  244. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/project.yaml +0 -0
  245. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/report.yaml +0 -0
  246. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/resolve.yaml +0 -0
  247. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/schedule.yaml +0 -0
  248. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/schema.yaml +0 -0
  249. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/skill.yaml +0 -0
  250. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/snippet.yaml +0 -0
  251. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/support.yaml +0 -0
  252. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/template.yaml +0 -0
  253. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/token.yaml +0 -0
  254. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/trash.yaml +0 -0
  255. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/upgrade.yaml +0 -0
  256. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/user.yaml +0 -0
  257. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/version.yaml +0 -0
  258. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/webhook.yaml +0 -0
  259. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/workflow.yaml +0 -0
  260. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/commands/workspace.yaml +0 -0
  261. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/edits-target.source.yaml +0 -0
  262. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/openapi-operations.yaml +0 -0
  263. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/schema-v1.json +0 -0
  264. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/sdk-catalog.source.yaml +0 -0
  265. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/manifests/sdk-methods.yaml +0 -0
  266. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/openapi/metadata.json +0 -0
  267. {mammoth_cli-2.2.47 → mammoth_cli-2.2.48}/spec/openapi/openapi.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mammoth-cli
3
- Version: 2.2.47
3
+ Version: 2.2.48
4
4
  Summary: Command-line interface for the Mammoth Analytics platform
5
5
  Project-URL: homepage, https://mammoth.io
6
6
  Project-URL: repository, https://github.com/EdgeMetric/mammothsdk
@@ -17,7 +17,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
17
  Requires-Python: <3.15,>=3.12
18
18
  Requires-Dist: jsonschema>=4.20
19
19
  Requires-Dist: keyring<26,>=25.7
20
- Requires-Dist: mammoth-io<0.9,>=0.8.25
20
+ Requires-Dist: mammoth-io<0.9,>=0.8.26
21
21
  Requires-Dist: platformdirs<5,>=4.10
22
22
  Requires-Dist: pydantic<3,>=2.12.5
23
23
  Requires-Dist: pyyaml<7,>=6
@@ -6,7 +6,7 @@ not implement a second HTTP client and does not call private SDK members.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- __version__ = "2.2.47"
9
+ __version__ = "2.2.48"
10
10
 
11
11
  # The versioned machine output envelope contract.
12
12
  SCHEMA_VERSION = 1
@@ -49,7 +49,7 @@ MAMMOTH_OUTPUT=json MAMMOTH_NO_INPUT=1` once.
49
49
  Use the project you are given; never `project ensure`. No `doctor`, auth,
50
50
  profiles, `config set`, `upgrade`, `skill install`, env vars, local files,
51
51
  stdin, `--input FILE` or `output_path` (they fail `not_available_embedded`);
52
- downloads return `download_url`. On an auth error, ask the user to reload.
52
+ downloads return `download_url`. Ask for a reload on `authentication_failed` only; on a 403 check ids and scope.
53
53
 
54
54
  ## Defaults you do not repeat
55
55
 
@@ -17,8 +17,11 @@ created without a project.
17
17
  `mammoth auth login --method oauth` signs the operator in through the browser (PKCE, loopback
18
18
  `127.0.0.1`). The CLI keeps a 1 h `mm_` token and refreshes it under a file lock. `auth status`
19
19
  reports `credential: oauth` and `expires_at`. A refresh that fails exits 4 with `login_expired`:
20
- tell the operator to run `mammoth auth login` again. `--no-browser` prints the URL. Browser sign-in
21
- is for the operator's own terminal; an agent never runs it. `auth logout` also revokes the
20
+ tell the operator to run `mammoth auth login` again. `--no-browser` prints the URL. `--device` is the sign-in for a
21
+ machine with no browser: it prints a short code and a URL for the operator to approve on another
22
+ device, then polls. Browser and device sign-in are for the operator's own terminal; an agent never
23
+ runs them. A key made with `token create` expires after 30 days: `cli_key_expired` means run
24
+ `mammoth auth login` again. `auth logout` also revokes the
22
25
  connection on the server, and warns (still clearing local state) if it cannot.
23
26
 
24
27
  `mammoth token create NAME`, `token list`, `token revoke KEY` are aliases of `client-app create`,
@@ -738,7 +738,7 @@ Status on release: ran once on CLI 2.0.21 — ergonomics sweep 2026-09-19: exit
738
738
 
739
739
  Run: `mammoth view optimize`. Exact input fields: `mammoth schema get view.optimize`.
740
740
 
741
- Example: `mammoth view optimize 123 123`. Illustrative only: append `--yes` after observing an owned target.
741
+ Example: `mammoth view optimize 123`. Illustrative only: append `--yes` after observing an owned target.
742
742
 
743
743
  Result: `ViewOptimizeResult`; mutation `reversible_pipeline`, confirmation `prompt_or_yes`, wait policy `start_or_wait`.
744
744
 
@@ -19,5 +19,6 @@ condition, not permission to retry mutations. The credential rule lives in
19
19
  Exit 4 is auth/authorization; preserve the structured error and stop safely.
20
20
 
21
21
  In the Mammoth app none of the above applies: there is no `auth status`,
22
- `doctor` or profile, and the app passes the login and project. On exit 4 ask
23
- the user to reload the page.
22
+ `doctor` or profile, and the app passes the login and project. On `authentication_failed` or
23
+ `not_authenticated` ask the user to reload the page; on `authorization_required` (403)
24
+ check the ids and scope first, since a reload does not fix them.
@@ -7,7 +7,8 @@ these commands are not blanket permission to replay a mutation.
7
7
 
8
8
  | exit | error.code (examples) | next step |
9
9
  |---|---|---|
10
- | 4 | not_authenticated, authentication_failed | `mammoth auth login`; in the Mammoth app, ask the user to reload the page |
10
+ | 4 | not_authenticated, authentication_failed | `mammoth auth login`; in the Mammoth app, ask the user to reload the page (an expired or invalid sign-in only) |
11
+ | 4 | authorization_required (403) | check the view, dataset and project ids and the scope first; a reload does not fix a wrong id |
11
12
  | 2 | keyring_unavailable, keyring_unresponsive | operator runs `mammoth auth login --storage file` (or unlocks the OS keychain); not seen in the app |
12
13
  | 5 | resource_not_found | re-list to find the correct id |
13
14
  | 2 | project_required | `mammoth context project use ID` or `--project` (in the app: `--project` only) |
@@ -48,7 +49,7 @@ the `run_id` when you report a backend fault.
48
49
 
49
50
  Do not run the next mutation on any of these; report the state (with
50
51
  `log_ref.run_id`) and either recover per the tables above or hand off:
51
- - `auth status` is missing or its `endpoint` does not match the intended environment (in the app there is no `auth status` or `doctor`: an auth error means ask the user to reload the page)
52
+ - `auth status` is missing or its `endpoint` does not match the intended environment (in the app there is no `auth status` or `doctor`: a sign-in error (`authentication_failed`, `not_authenticated`) means ask the user to reload the page; `authorization_required` means check the ids and scope)
52
53
  - `mammoth doctor` fails (CLI only)
53
54
  - a dataset is still `need_action` after the settings update
54
55
  - a read-back after a value-changing step shows a uniform column (all 0 / all one value / all null) or a changed row the condition should not have touched
@@ -270,13 +270,13 @@ def _verify_login(invocation: Invocation, *, base_url: str, api_token: str) -> i
270
270
  return workspace_id
271
271
 
272
272
 
273
- _LOGIN_METHODS = ("oauth", "token")
273
+ _LOGIN_METHODS = ("oauth", "device", "token")
274
274
 
275
275
 
276
276
  def _choose_method(
277
277
  method: str | None, invocation: Invocation, *, server_prefix: str | None, blockers: list[str]
278
278
  ) -> str:
279
- """Pick ``oauth`` or ``token``: explicit, implied by ``--input``, or asked.
279
+ """Pick ``oauth``, ``device`` or ``token``: explicit, implied by ``--input``, or asked.
280
280
 
281
281
  A server with no CLI OAuth client, a non-interactive run, and ``--input``
282
282
  all keep the token login that existed before browser sign-in.
@@ -289,10 +289,10 @@ def _choose_method(
289
289
  exit_status=EXIT_USAGE,
290
290
  hint=f"Use one of: {', '.join(_LOGIN_METHODS)}.",
291
291
  )
292
- if method == "oauth" and invocation.input_file is not None:
292
+ if method != "token" and invocation.input_file is not None:
293
293
  raise CliError(
294
294
  code="invalid_argument_combination",
295
- message="--input is for token logins; it cannot be used with --method oauth.",
295
+ message=f"--input is for token logins; it cannot be used with --method {method}.",
296
296
  exit_status=EXIT_USAGE,
297
297
  )
298
298
  return method
@@ -302,16 +302,18 @@ def _choose_method(
302
302
  return "token"
303
303
  typer.echo("How do you want to sign in?", err=True)
304
304
  typer.echo(" 1) Browser (OAuth) recommended", err=True)
305
+ typer.echo(" 2) Device code no browser on this machine", err=True)
305
306
  typer.echo(" 3) Paste an API token for CI and scripts", err=True)
306
307
  choice = typer.prompt("Choose", default="1", show_default=False).strip()
307
- if choice not in ("1", "3"):
308
+ methods = {"1": "oauth", "2": "device", "3": "token"}
309
+ if choice not in methods:
308
310
  raise CliError(
309
311
  code="invalid_login_method",
310
312
  message=f"'{choice}' is not one of the choices.",
311
313
  exit_status=EXIT_USAGE,
312
- hint="Enter 1 for the browser or 3 to paste an API token.",
314
+ hint="Enter 1 for the browser, 2 for a device code or 3 to paste an API token.",
313
315
  )
314
- return "oauth" if choice == "1" else "token"
316
+ return methods[choice]
315
317
 
316
318
 
317
319
  def _iso(epoch: int) -> str:
@@ -325,16 +327,18 @@ def _run_oauth_login(
325
327
  storage: str,
326
328
  open_browser: bool,
327
329
  interactive: bool,
330
+ device: bool = False,
328
331
  ) -> tuple[dict[str, Any], dict[str, Any]]:
329
- """Sign in through the browser and persist the refreshable session."""
332
+ """Sign in through the browser (or a device code) and persist the refreshable session."""
330
333
  base_url = resolve_base_url(server_prefix)
331
334
  client_id = oauth.client_id_for(server_prefix)
332
- session = oauth.browser_login(
333
- base_url,
334
- client_id=client_id,
335
- open_browser=open_browser,
336
- say=lambda line: typer.echo(line, err=True),
337
- )
335
+ say = lambda line: typer.echo(line, err=True) # noqa: E731
336
+ if device:
337
+ session = oauth.device_login(base_url, client_id=client_id, say=say)
338
+ else:
339
+ session = oauth.browser_login(
340
+ base_url, client_id=client_id, open_browser=open_browser, say=say
341
+ )
338
342
  workspace_id = _verify_login(invocation, base_url=base_url, api_token=session.access)
339
343
 
340
344
  profile_name = invocation.profile or profiles.DEFAULT_PROFILE_NAME
@@ -378,8 +382,18 @@ def _run_login(
378
382
  storage: str,
379
383
  method: str | None = None,
380
384
  no_browser: bool = False,
385
+ device: bool = False,
381
386
  ) -> tuple[dict[str, Any], dict[str, Any]]:
382
387
  """Validate, connection-check, and persist one `auth login` invocation."""
388
+ if device:
389
+ if method not in (None, "device"):
390
+ raise CliError(
391
+ code="invalid_argument_combination",
392
+ message=f"--device cannot be used with --method {method}.",
393
+ exit_status=EXIT_USAGE,
394
+ hint="Use --device alone, or --method device.",
395
+ )
396
+ method = "device"
383
397
  if storage not in _STORAGE_MODES:
384
398
  raise CliError(
385
399
  code="invalid_storage_mode",
@@ -390,16 +404,15 @@ def _run_login(
390
404
 
391
405
  blockers = _prompt_blockers(invocation)
392
406
 
393
- if (
394
- _choose_method(method, invocation, server_prefix=server_prefix, blockers=blockers)
395
- == "oauth"
396
- ):
407
+ chosen = _choose_method(method, invocation, server_prefix=server_prefix, blockers=blockers)
408
+ if chosen in ("oauth", "device"):
397
409
  return _run_oauth_login(
398
410
  invocation,
399
411
  server_prefix=server_prefix,
400
412
  storage=storage,
401
413
  open_browser=not no_browser,
402
414
  interactive=not blockers,
415
+ device=chosen == "device",
403
416
  )
404
417
 
405
418
  if invocation.input_file is not None:
@@ -488,19 +501,33 @@ def auth_login(
488
501
  method: str | None = typer.Option(
489
502
  None,
490
503
  "--method",
491
- help="How to sign in: a browser (oauth) or a pasted API token (token).",
492
- metavar="oauth|token",
504
+ help=(
505
+ "How to sign in: a browser (oauth), a code typed on another device (device) "
506
+ "or a pasted API token (token)."
507
+ ),
508
+ metavar="oauth|device|token",
493
509
  ),
494
510
  no_browser: bool = typer.Option(
495
511
  False, "--no-browser", help="Print the sign-in URL instead of opening a browser."
496
512
  ),
513
+ device: bool = typer.Option(
514
+ False,
515
+ "--device",
516
+ help=(
517
+ "Sign in with a device code, for a machine with no browser: the CLI prints "
518
+ "a short code and a URL, you approve on any other device, and the CLI "
519
+ "polls until you do. Same as --method device."
520
+ ),
521
+ ),
497
522
  ) -> None:
498
523
  """Log in and store one profile's credentials.
499
524
 
500
525
  In a terminal, choose a browser sign-in (OAuth; the CLI keeps a token that
501
- refreshes itself) or paste an API token (mm_..., from Workspace settings ->
502
- API Tokens). The token names its workspace. For non-interactive use
503
- (agents, CI), pass ``--input FILE`` with {"api_token": ...} instead.
526
+ refreshes itself), a device code (``--device``: for a machine with no
527
+ browser, you type a short code on any other device) or paste an API token
528
+ (mm_..., from Workspace settings -> API Tokens). The token names its
529
+ workspace. For non-interactive use (agents, CI), pass ``--input FILE`` with
530
+ {"api_token": ...} instead.
504
531
  Performs a lightweight connection check before saving anything; a failed
505
532
  check leaves existing profile state unchanged.
506
533
  """
@@ -531,6 +558,7 @@ def auth_login(
531
558
  storage=storage,
532
559
  method=method,
533
560
  no_browser=no_browser,
561
+ device=device,
534
562
  )
535
563
 
536
564
  executor.run(
@@ -199,7 +199,9 @@ def _with_outcome(service: Any, data: dict[str, Any]) -> dict[str, Any]:
199
199
  gets its copied counts and per-view runs (no read). The job succeeded either way, so
200
200
  a view that cannot be read is an ``outcome_error``, never a failed wait.
201
201
  """
202
- copied = job_outcome.copy_outcome(data.get("result"))
202
+ result = job_outcome.with_derived_datasets(data.get("result"))
203
+ data = {**data, "result": result} if result is not data.get("result") else data
204
+ copied = job_outcome.copy_outcome(result)
203
205
  if copied is not None and data.get("operation") == job_outcome.COPY_PROJECT_OPERATION:
204
206
  return {**data, "outcome": copied}
205
207
  target = job_outcome.view_target(data)
@@ -36,6 +36,7 @@ from mammoth_cli.runtime.confirm import (
36
36
  )
37
37
  from mammoth_cli.runtime.invocation import Invocation
38
38
  from mammoth_cli.runtime.session import open_service, resolved_project
39
+ from mammoth_cli.services import job_outcome
39
40
 
40
41
  HandlerResult = tuple[Any, dict[str, Any]]
41
42
 
@@ -299,7 +300,7 @@ def project_copy(invocation: Invocation) -> HandlerResult:
299
300
  _forward_optional(document, kwargs, _COPY_OPTIONAL)
300
301
  with open_service(invocation) as (service, auth):
301
302
  queued = service.call(_symbol(invocation), **kwargs)
302
- data = service.wait_if_job(queued)
303
+ data = job_outcome.with_derived_datasets(service.wait_if_job(queued))
303
304
  return data, _meta(invocation, auth.workspace_id, source_id)
304
305
 
305
306
 
@@ -45,6 +45,7 @@ _PROTECTED_INPUT_PATH = "/private/path/request.json"
45
45
  # Commands whose useful example the SDK signature cannot express: a distinct
46
46
  # second id, an optional-but-central input field, or two views to compare.
47
47
  _FIXED_EXAMPLES: dict[str, tuple[tuple[str, ...], dict[str, Any]]] = {
48
+ "view.optimize": (("123",), {}),
48
49
  "project.check": (("123", "456"), {}),
49
50
  "view.data.profile": (("123",), {"target": "Churn"}),
50
51
  "workflow.canvas": (
@@ -213,6 +213,48 @@ def _resolve_dataset_id(
213
213
  return dataset_id
214
214
 
215
215
 
216
+ def _check_parent_matches(view_id: int, given: int, real: int) -> None:
217
+ """Refuse a DATASET_ID that is not the view's parent, before any write."""
218
+ if given == real:
219
+ return
220
+ raise CliError(
221
+ code=CODE_INVALID_ARGUMENT,
222
+ message=f"view {view_id} belongs to dataset {real}, not {given}",
223
+ exit_status=EXIT_USAGE,
224
+ hint=f"Run it again with dataset {real}, or leave DATASET_ID out: mammoth looks it up.",
225
+ details={"view_id": view_id, "given_dataset_id": given, "dataset_id": real},
226
+ )
227
+
228
+
229
+ def _view_parent_dataset_id(service: Any, invocation: Invocation, view_id: int) -> int:
230
+ """The view's own dataset: the remembered parent, else one read of the view's row."""
231
+ profile_name = _profile_name(invocation)
232
+ workspace_id = getattr(service, "_workspace_id", None)
233
+ remembered = parents.lookup(profile_name, workspace_id, view_id)
234
+ if remembered is not None:
235
+ return remembered
236
+ dataset_id = int(service.call(_FIND_DATASET_SYMBOL, dataview_id=view_id))
237
+ parents.remember(profile_name, workspace_id, {view_id: dataset_id})
238
+ return dataset_id
239
+
240
+
241
+ def _verified_dataset_id(
242
+ service: Any, invocation: Invocation, view_id: int, document: dict[str, Any]
243
+ ) -> int:
244
+ """The view's dataset for a write, so the caller never has to supply it.
245
+
246
+ A DATASET_ID (positional or ``dataset_id`` input field) is checked against the
247
+ view's real parent and refused when it differs; with none, the parent is looked up.
248
+ """
249
+ explicit = _int_positional_at(invocation, 1, "dataset id")
250
+ if explicit is None and document.get(_DATASET_ID_FIELD) is not None:
251
+ explicit = _int_field(document[_DATASET_ID_FIELD], _DATASET_ID_FIELD)
252
+ real = _view_parent_dataset_id(service, invocation, view_id)
253
+ if explicit is not None:
254
+ _check_parent_matches(view_id, explicit, real)
255
+ return real
256
+
257
+
216
258
  def _resolve_dataset_id_for_settle(
217
259
  service: Any,
218
260
  invocation: Invocation,
@@ -1030,7 +1072,7 @@ def view_optimize(invocation: Invocation) -> HandlerResult:
1030
1072
  kwargs: dict[str, Any] = {"dataview_id": dataview_id, "project_id": project_id}
1031
1073
  _forward_optional(document, kwargs, ("apply_rules",))
1032
1074
  with open_service(invocation) as (service, auth):
1033
- kwargs["dataset_id"] = _resolve_dataset_id(service, invocation, dataview_id, document)
1075
+ kwargs["dataset_id"] = _verified_dataset_id(service, invocation, dataview_id, document)
1034
1076
  data = service.call(_symbol(invocation), **kwargs)
1035
1077
  if isinstance(data, dict) and data.get("job_id") is not None:
1036
1078
  settled = service.wait_if_job({"job_id": data["job_id"]})
@@ -1,9 +1,14 @@
1
- """Browser sign-in (OAuth 2.0 authorization code + PKCE) and token refresh.
1
+ """Browser sign-in (OAuth 2.0 authorization code + PKCE), device-code sign-in
2
+ (RFC 8628) and token refresh.
2
3
 
3
4
  ``mammoth auth login`` opens the Mammoth consent page, receives the code on a
4
5
  loopback listener (RFC 8252), and exchanges it for a 1 h ``mm_`` access token
5
6
  plus a rotating refresh token. The CLI is a public client: it holds no secret.
6
7
 
8
+ ``mammoth auth login --device`` is for a machine with no browser: it prints a
9
+ short code and a URL to open on any other device, then polls the token endpoint
10
+ until the code is approved, denied or expired.
11
+
7
12
  The refresh token rotates on every use, and replaying an already-used one
8
13
  revokes the whole connection on the server. Two CLI processes must therefore
9
14
  never refresh at once: :func:`refresh_session` takes a file lock, re-reads the
@@ -52,6 +57,14 @@ _LOCK_WAIT_SECONDS = 60.0
52
57
  CODE_LOGIN_EXPIRED = "login_expired"
53
58
  CODE_OAUTH_UNAVAILABLE = "oauth_unavailable"
54
59
  CODE_OAUTH_LOGIN_FAILED = "oauth_login_failed"
60
+ CODE_DEVICE_LOGIN_EXPIRED = "device_login_expired"
61
+
62
+ #: RFC 8628 §3.4: the grant type the device sign-in polls with.
63
+ DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
64
+ #: RFC 8628 §3.5: a slow_down answer adds this many seconds to the interval.
65
+ SLOW_DOWN_STEP_SECONDS = 5
66
+ #: Added to every wait so a poll never lands a hair before the server's interval.
67
+ POLL_MARGIN_SECONDS = 1.0
55
68
 
56
69
 
57
70
  def login_expired_error() -> CliError:
@@ -324,6 +337,114 @@ def browser_login(
324
337
  return session_from_grant(grant, client_id)
325
338
 
326
339
 
340
+ # --- device code (RFC 8628) -----------------------------------------------------
341
+
342
+
343
+ @dataclass(frozen=True)
344
+ class DeviceChallenge:
345
+ """What the server issues to start a device sign-in."""
346
+
347
+ device_code: str
348
+ user_code: str
349
+ verification_uri: str
350
+ verification_uri_complete: str
351
+ expires_in: int
352
+ interval: int
353
+
354
+
355
+ def parse_device_challenge(body: dict[str, object]) -> DeviceChallenge:
356
+ """Read the device-authorization answer; an unreadable one is a failed login."""
357
+ try:
358
+ return DeviceChallenge(
359
+ device_code=str(body["device_code"]),
360
+ user_code=str(body["user_code"]),
361
+ verification_uri=str(body["verification_uri"]),
362
+ verification_uri_complete=str(body.get("verification_uri_complete") or ""),
363
+ expires_in=int(str(body["expires_in"])),
364
+ interval=int(str(body["interval"])),
365
+ )
366
+ except (ValueError, KeyError, TypeError) as exc:
367
+ raise _login_failed("Mammoth's sign-in service returned an unreadable answer.") from exc
368
+
369
+
370
+ def start_device_login(base_url: str, *, client_id: str) -> DeviceChallenge:
371
+ """Ask Mammoth for a device code and the code the person will type."""
372
+ try:
373
+ status, body = sdk_oauth.device_authorization_request(base_url, {"client_id": client_id})
374
+ except sdk_oauth.OAuthTransportError as exc:
375
+ raise CliError(
376
+ code="network_error",
377
+ message="Could not reach Mammoth's sign-in service.",
378
+ exit_status=EXIT_AUTH,
379
+ hint="Check your connection and try again.",
380
+ ) from exc
381
+ if status != 200 or not isinstance(body, dict):
382
+ raise _login_failed(f"Mammoth refused to start a device sign-in (HTTP {status}).")
383
+ return parse_device_challenge(body)
384
+
385
+
386
+ def read_poll(status: int, body: dict[str, object], interval: int) -> TokenGrant | int:
387
+ """Interpret one poll answer.
388
+
389
+ Returns the tokens once approved, or the seconds to wait before the next
390
+ poll (longer after ``slow_down``).
391
+
392
+ Raises:
393
+ CliError: ``oauth_login_failed`` when the person denied the sign-in or
394
+ the answer is not one RFC 8628 lets the client retry on;
395
+ ``device_login_expired`` when the code ran out.
396
+ """
397
+ if status == 200:
398
+ return _parse_grant(body)
399
+ error = str(body.get("error", ""))
400
+ if error == "authorization_pending":
401
+ return interval
402
+ if error == "slow_down":
403
+ return interval + SLOW_DOWN_STEP_SECONDS
404
+ if error == "access_denied":
405
+ raise _login_failed("The sign-in was denied in the browser.")
406
+ if error == "expired_token":
407
+ raise device_login_expired_error()
408
+ raise _login_failed(f"Mammoth refused the sign-in ({error or f'HTTP {status}'}).")
409
+
410
+
411
+ def device_login_expired_error() -> CliError:
412
+ return CliError(
413
+ code=CODE_DEVICE_LOGIN_EXPIRED,
414
+ message="The sign-in code expired before it was approved.",
415
+ exit_status=EXIT_AUTH,
416
+ hint="Run `mammoth auth login --device` again and approve the new code.",
417
+ recovery_commands=["mammoth auth login --device"],
418
+ )
419
+
420
+
421
+ def device_login(base_url: str, *, client_id: str, say: Callable[[str], None]) -> OAuthSession:
422
+ """Run one device sign-in: print the code, poll until it is approved."""
423
+ challenge = start_device_login(base_url, client_id=client_id)
424
+ say(f"To sign in, open {challenge.verification_uri} on any device and enter:")
425
+ say(f" {challenge.user_code}")
426
+ if challenge.verification_uri_complete:
427
+ say(f"Or open this link directly:\n {challenge.verification_uri_complete}")
428
+ say("Waiting for approval ...")
429
+ deadline = time.monotonic() + challenge.expires_in
430
+ interval = challenge.interval
431
+ while time.monotonic() < deadline:
432
+ time.sleep(interval + POLL_MARGIN_SECONDS)
433
+ status, body = _post_token(
434
+ base_url,
435
+ {
436
+ "grant_type": DEVICE_GRANT_TYPE,
437
+ "device_code": challenge.device_code,
438
+ "client_id": client_id,
439
+ },
440
+ )
441
+ step = read_poll(status, body, interval)
442
+ if isinstance(step, TokenGrant):
443
+ return session_from_grant(step, client_id)
444
+ interval = step
445
+ raise device_login_expired_error()
446
+
447
+
327
448
  # --- refresh under a file lock ----------------------------------------------------
328
449
 
329
450
 
@@ -89,6 +89,7 @@ ERROR_SUMMARIES: dict[str, str] = {
89
89
  "authentication_failed": "Mammoth didn't accept the sign-in for this step.",
90
90
  "authorization_required": "You don't have permission to do this.",
91
91
  "capability_not_found": "The assistant tried something Mammoth doesn't support.",
92
+ "cli_key_expired": "The saved API key has expired, so the assistant needs to sign in again.",
92
93
  "cli_keys_need_browser_sign_in": "Managing API keys needs a browser sign-in, not an API token.",
93
94
  "cli_keys_not_allowed": "This server doesn't let a browser sign-in create API keys.",
94
95
  "confirmation_declined": "This step was not confirmed, so nothing was changed.",
@@ -97,6 +98,7 @@ ERROR_SUMMARIES: dict[str, str] = {
97
98
  "The confirmation was for a different item, so nothing was changed."
98
99
  ),
99
100
  "conflict": "This conflicts with the current state of your data, so nothing was changed.",
101
+ "device_login_expired": "The sign-in code expired before it was approved.",
100
102
  "dry_run_targets_unresolvable": (
101
103
  "The assistant couldn't tell exactly which items this would change, so nothing was changed."
102
104
  ),
@@ -284,7 +286,7 @@ def no_saved_project_error() -> CliError:
284
286
  )
285
287
 
286
288
 
287
- EMBEDDED_AUTH_HINT = "Your session expired or is not authorised; ask the user to reload the page."
289
+ EMBEDDED_AUTH_HINT = "Your session expired or is invalid; ask the user to reload the page."
288
290
  EMBEDDED_NO_PROFILE_HINT = "This environment keeps no profiles; the login comes from the host."
289
291
 
290
292
 
@@ -10,6 +10,7 @@ makes a request.
10
10
  from __future__ import annotations
11
11
 
12
12
  import json
13
+ import shlex
13
14
  from collections.abc import Iterator
14
15
  from typing import Any
15
16
 
@@ -113,7 +114,7 @@ def _entry(record: dict[str, Any]) -> dict[str, Any]:
113
114
  entry["stored_suggestions"] = suggestions
114
115
  entry["fix"] = (
115
116
  f"mammoth dataset interpretation preview {dataset_id} --input "
116
- + json.dumps({"user_instruction": suggestions[0]})
117
+ + shlex.quote(json.dumps({"user_instruction": suggestions[0]}))
117
118
  + "; then mammoth dataset interpretation confirm "
118
119
  + f"{dataset_id} with the same input"
119
120
  )
@@ -91,6 +91,26 @@ def view_outcome(record: Any, target: ViewTarget) -> dict[str, Any]:
91
91
  }
92
92
 
93
93
 
94
+ def with_derived_datasets(result: Any) -> Any:
95
+ """Add ``derived_datasets`` beside a copy's ``derived``, whose direction misleads.
96
+
97
+ The backend records ``derived`` as ``{"5214": [5224]}``: dataset 5214 is derived,
98
+ and view 5224 exports into it. Read as "view 5224 of dataset 5214", it sent an agent
99
+ to the wrong parent. ``derived_datasets`` says the same thing in words:
100
+ ``[{"dataset_id": 5214, "from_view_ids": [5224]}]``. ``derived`` stays as it was.
101
+ """
102
+ derived = result.get("derived") if isinstance(result, dict) else None
103
+ if not isinstance(derived, dict) or not derived:
104
+ return result
105
+ return {
106
+ **result,
107
+ "derived_datasets": [
108
+ {"dataset_id": _int_or_none(dataset) or dataset, "from_view_ids": list(views or [])}
109
+ for dataset, views in sorted(derived.items(), key=lambda item: str(item[0]))
110
+ ],
111
+ }
112
+
113
+
94
114
  def copy_outcome(result: Any) -> dict[str, Any] | None:
95
115
  """A project copy's outcome: what was copied, what was skipped, and how each run ended.
96
116
 
@@ -107,6 +127,9 @@ def copy_outcome(result: Any) -> dict[str, Any] | None:
107
127
  "views": len(result.get("view_map") or {}),
108
128
  "skipped": len(result.get("skipped") or []),
109
129
  }
130
+ derived = with_derived_datasets(result).get("derived_datasets")
131
+ if derived:
132
+ outcome["derived_datasets"] = derived
110
133
  runs = result.get("runs")
111
134
  if isinstance(runs, dict):
112
135
  outcome["runs"] = _run_summary(runs, result.get("run_views"))
@@ -258,6 +258,8 @@ def _not_allowed_reason(body: Any, status: int | None) -> str | None:
258
258
 
259
259
  CODE_CLI_KEYS_NOT_ALLOWED = "cli_keys_not_allowed"
260
260
  CODE_CLI_KEYS_NEED_BROWSER_SIGN_IN = "cli_keys_need_browser_sign_in"
261
+ CODE_CLI_KEY_EXPIRED = "cli_key_expired"
262
+ _TOKEN_EXPIRED_NAME = "API_TOKEN_EXPIRED" # noqa: S105 - an error name
261
263
  _TOKEN_REFUSED_NAME = "INVALID_TOKEN_FOR_CLIENT_APPS" # noqa: S105 - an error name
262
264
 
263
265
 
@@ -267,6 +269,12 @@ def _is_cli_keys_refused(exc: BaseException) -> bool:
267
269
  return isinstance(body, dict) and body.get("name") == _TOKEN_REFUSED_NAME
268
270
 
269
271
 
272
+ def _is_token_expired(exc: BaseException) -> bool:
273
+ """True for the server's 401 naming an API key that is past its expiry."""
274
+ body = getattr(exc, "response_body", None)
275
+ return isinstance(body, dict) and body.get("name") == _TOKEN_EXPIRED_NAME
276
+
277
+
270
278
  def _stored_credential_kind(profile: str | None) -> str | None:
271
279
  """The stored credential kind of ``profile`` (the selected one when None)."""
272
280
  credential = credentials.load_credential(profile or profiles.get_selected())
@@ -303,6 +311,17 @@ def map_sdk_exception(
303
311
  details=getattr(exc, "details", None),
304
312
  )
305
313
 
314
+ if isinstance(exc, MammothAuthError) and _is_token_expired(exc):
315
+ return CliError(
316
+ code=CODE_CLI_KEY_EXPIRED,
317
+ message="This API key has expired; run `mammoth auth login` to sign in again.",
318
+ exit_status=EXIT_AUTH,
319
+ hint="Sign in again to get a new key.",
320
+ details=_metadata(exc),
321
+ request_id=exc.request_id,
322
+ recovery_commands=["mammoth auth login"],
323
+ )
324
+
306
325
  if isinstance(exc, MammothAuthError):
307
326
  mismatch = _workspace_mismatch_error(exc, workspace_id)
308
327
  if mismatch is not None:
@@ -535,7 +554,9 @@ def map_sdk_exception(
535
554
  message="Mammoth denied access to the requested resource or operation.",
536
555
  exit_status=EXIT_AUTH,
537
556
  hint=(
538
- "Check the target scope or ask an administrator for permission. "
557
+ "Check the ids and scope first: a view id given with another dataset's id, "
558
+ "or an id from a different project, is refused the same way. Then ask an "
559
+ "administrator for permission. "
539
560
  "An API token limited to one project gets this in every other "
540
561
  "project, including one it created: pass that project's --project ID, "
541
562
  "or log in with a token created without a project."
@@ -1231,6 +1231,9 @@ EXACT_PARENT_HELP = (
1231
1231
  # instead and hit a not-found on the (unrelated) source view's own parent
1232
1232
  # (WPP evidence c38/c39).
1233
1233
  _EXACT_PARENT_HELP_OVERRIDES: dict[str, str] = {
1234
+ "view.optimize": (
1235
+ "Optional parent dataset ID. Leave it out and the CLI looks up the view's dataset. A value that is not the view's dataset is refused before the write."
1236
+ ),
1234
1237
  "view.export.dataset": (
1235
1238
  "Exact parent dataset ID of the SOURCE view being exported -- not the "
1236
1239
  "destination. Required for this command: pass it here or as the "