mammoth-cli 2.0.38__tar.gz → 2.0.39__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 (222) hide show
  1. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/PKG-INFO +1 -1
  2. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/__init__.py +1 -1
  3. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/SKILL.md +20 -8
  4. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/end-to-end.md +5 -2
  5. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/transforms.md +35 -0
  6. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/task-start.md +4 -2
  7. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/view.py +175 -14
  8. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/view_ops.py +30 -3
  9. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/strict.py +14 -1
  10. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/command_contract.py +4 -1
  11. mammoth_cli-2.0.39/mammoth_cli/services/data_quality.py +136 -0
  12. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/pyproject.toml +1 -1
  13. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/.gitignore +0 -0
  14. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/LICENSE +0 -0
  15. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/README.md +0 -0
  16. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/__main__.py +0 -0
  17. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/app.py +0 -0
  18. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/about-mammoth.md +0 -0
  19. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/auth.md +0 -0
  20. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/capabilities-misc.md +0 -0
  21. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/capabilities.md +0 -0
  22. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/command-index.md +0 -0
  23. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/activity.md +0 -0
  24. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/addon.md +0 -0
  25. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/agent.md +0 -0
  26. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/ai.md +0 -0
  27. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/annotation.md +0 -0
  28. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/auth.md +0 -0
  29. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/automation.md +0 -0
  30. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/batch.md +0 -0
  31. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/billing.md +0 -0
  32. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/browse.md +0 -0
  33. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/capability.md +0 -0
  34. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/client-app.md +0 -0
  35. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/completion.md +0 -0
  36. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/config.md +0 -0
  37. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/connector.md +0 -0
  38. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/context.md +0 -0
  39. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard.md +0 -0
  40. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/data-app.md +0 -0
  41. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dataset.md +0 -0
  42. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/doctor.md +0 -0
  43. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/external-key.md +0 -0
  44. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/file.md +0 -0
  45. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/folder.md +0 -0
  46. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/job.md +0 -0
  47. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/log.md +0 -0
  48. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/notification.md +0 -0
  49. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/parameter.md +0 -0
  50. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/project.md +0 -0
  51. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/report.md +0 -0
  52. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/schedule.md +0 -0
  53. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/schema.md +0 -0
  54. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/skill.md +0 -0
  55. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/snippet.md +0 -0
  56. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/support.md +0 -0
  57. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/template.md +0 -0
  58. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/trash.md +0 -0
  59. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/upgrade.md +0 -0
  60. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/user.md +0 -0
  61. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/version.md +0 -0
  62. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/view.md +0 -0
  63. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/webhook.md +0 -0
  64. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/workflow.md +0 -0
  65. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/workspace.md +0 -0
  66. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/handoff.md +0 -0
  67. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/input.md +0 -0
  68. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/jobs-drafts.md +0 -0
  69. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/machine-output.md +0 -0
  70. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/operations.md +0 -0
  71. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/auth-scope.md +0 -0
  72. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/cleanup.md +0 -0
  73. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/dashboards.md +0 -0
  74. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/exports.md +0 -0
  75. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/index.md +0 -0
  76. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/need-action.md +0 -0
  77. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/resources.md +0 -0
  78. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/typed-etl-discovery.md +0 -0
  79. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/recovery.md +0 -0
  80. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/report-checklist.md +0 -0
  81. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/retention.md +0 -0
  82. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/bundled_skill/mammoth-cli/references/safety.md +0 -0
  83. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/__init__.py +0 -0
  84. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/activity.py +0 -0
  85. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/addon.py +0 -0
  86. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/agent.py +0 -0
  87. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/ai.py +0 -0
  88. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/annotation.py +0 -0
  89. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/auth.py +0 -0
  90. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/automation.py +0 -0
  91. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/batch.py +0 -0
  92. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/billing.py +0 -0
  93. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/browse.py +0 -0
  94. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/capability.py +0 -0
  95. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/client_app.py +0 -0
  96. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/completion.py +0 -0
  97. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/config.py +0 -0
  98. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/connector.py +0 -0
  99. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/context.py +0 -0
  100. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/dashboard.py +0 -0
  101. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/data_app.py +0 -0
  102. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/dataset.py +0 -0
  103. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/doctor.py +0 -0
  104. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/external_key.py +0 -0
  105. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/file.py +0 -0
  106. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/folder.py +0 -0
  107. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/job.py +0 -0
  108. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/notification.py +0 -0
  109. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/parameter.py +0 -0
  110. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/project.py +0 -0
  111. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/registry.py +0 -0
  112. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/report.py +0 -0
  113. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/schedule.py +0 -0
  114. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/schema.py +0 -0
  115. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/skill.py +0 -0
  116. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/snippet.py +0 -0
  117. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/support.py +0 -0
  118. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/template.py +0 -0
  119. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/trash.py +0 -0
  120. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/upgrade.py +0 -0
  121. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/user.py +0 -0
  122. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/webhook.py +0 -0
  123. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/workflow.py +0 -0
  124. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/commands/workspace.py +0 -0
  125. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/context/__init__.py +0 -0
  126. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/context/credentials.py +0 -0
  127. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/context/endpoint.py +0 -0
  128. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/context/profiles.py +0 -0
  129. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/context/resolver.py +0 -0
  130. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/contracts/__init__.py +0 -0
  131. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/contracts/auth.py +0 -0
  132. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/errors/__init__.py +0 -0
  133. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/errors/envelope.py +0 -0
  134. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/manifest/__init__.py +0 -0
  135. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/manifest/loader.py +0 -0
  136. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/messages/__init__.py +0 -0
  137. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/output/__init__.py +0 -0
  138. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/output/envelope.py +0 -0
  139. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/output/normalize.py +0 -0
  140. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/output/policy.py +0 -0
  141. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/output/progress.py +0 -0
  142. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/output/render.py +0 -0
  143. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/py.typed +0 -0
  144. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/__init__.py +0 -0
  145. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/confirm.py +0 -0
  146. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/dryrun.py +0 -0
  147. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/executor.py +0 -0
  148. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/input_loader.py +0 -0
  149. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/invocation.py +0 -0
  150. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/options.py +0 -0
  151. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/parents.py +0 -0
  152. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/runlog.py +0 -0
  153. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/session.py +0 -0
  154. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/updates.py +0 -0
  155. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/runtime/validate.py +0 -0
  156. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/__init__.py +0 -0
  157. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/argspec.py +0 -0
  158. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/coerce.py +0 -0
  159. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/conditions.py +0 -0
  160. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/dispatch.py +0 -0
  161. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/factory.py +0 -0
  162. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/input_fields.py +0 -0
  163. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/mapping.py +0 -0
  164. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/openapi_types.py +0 -0
  165. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/positionals.py +0 -0
  166. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/protocol.py +0 -0
  167. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/sdk_service.py +0 -0
  168. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/testing.py +0 -0
  169. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/services/type_system.py +0 -0
  170. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/skills/__init__.py +0 -0
  171. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/skills/installer.py +0 -0
  172. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/skills/steering.py +0 -0
  173. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/mammoth_cli/testing.py +0 -0
  174. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/activity.yaml +0 -0
  175. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/addon.yaml +0 -0
  176. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/agent.yaml +0 -0
  177. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/ai.yaml +0 -0
  178. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/annotation.yaml +0 -0
  179. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/auth.yaml +0 -0
  180. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/automation.yaml +0 -0
  181. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/batch.yaml +0 -0
  182. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/billing.yaml +0 -0
  183. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/browse.yaml +0 -0
  184. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/capability.yaml +0 -0
  185. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/client-app.yaml +0 -0
  186. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/completion.yaml +0 -0
  187. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/config.yaml +0 -0
  188. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/connector.yaml +0 -0
  189. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/context.yaml +0 -0
  190. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/dashboard.yaml +0 -0
  191. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/data-app.yaml +0 -0
  192. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/dataset.yaml +0 -0
  193. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/doctor.yaml +0 -0
  194. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/external-key.yaml +0 -0
  195. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/file.yaml +0 -0
  196. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/folder.yaml +0 -0
  197. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/job.yaml +0 -0
  198. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/log.yaml +0 -0
  199. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/notification.yaml +0 -0
  200. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/parameter.yaml +0 -0
  201. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/project.yaml +0 -0
  202. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/report.yaml +0 -0
  203. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/schedule.yaml +0 -0
  204. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/schema.yaml +0 -0
  205. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/skill.yaml +0 -0
  206. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/snippet.yaml +0 -0
  207. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/support.yaml +0 -0
  208. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/template.yaml +0 -0
  209. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/trash.yaml +0 -0
  210. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/upgrade.yaml +0 -0
  211. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/user.yaml +0 -0
  212. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/version.yaml +0 -0
  213. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/view.yaml +0 -0
  214. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/webhook.yaml +0 -0
  215. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/workflow.yaml +0 -0
  216. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/commands/workspace.yaml +0 -0
  217. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/openapi-operations.yaml +0 -0
  218. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/schema-v1.json +0 -0
  219. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/sdk-catalog.source.yaml +0 -0
  220. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/manifests/sdk-methods.yaml +0 -0
  221. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/openapi/metadata.json +0 -0
  222. {mammoth_cli-2.0.38 → mammoth_cli-2.0.39}/spec/openapi/openapi.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mammoth-cli
3
- Version: 2.0.38
3
+ Version: 2.0.39
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
@@ -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.0.38"
9
+ __version__ = "2.0.39"
10
10
 
11
11
  # The versioned machine output envelope contract.
12
12
  SCHEMA_VERSION = 1
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mammoth-cli
3
- version: 2.0.38
3
+ version: 2.0.39
4
4
  description: "Use Mammoth Analytics from a terminal: install or authenticate the CLI, discover its live command contract, and safely manage projects, data, views, pipelines (join, merge, pivot, filter, clean), dashboards, exports, and handoffs."
5
5
  ---
6
6
 
@@ -93,21 +93,29 @@ report a real gap with the command you tried.
93
93
  Users often upload related files without saying how they relate ("make a
94
94
  dashboard from t_a and t_b"). Work it out from the data before you build:
95
95
 
96
- 1. Read every view first: `view get VIEW_ID` (columns, types, row count) and
97
- `view data get VIEW_ID --input '{"limit": 20}'`.
96
+ 1. Upload every file, then read every view in Mammoth (not the local
97
+ files): `view get VIEW_ID` (columns, types, row count) and `view data get
98
+ VIEW_ID`. The data read lists `column_warnings`: numbers or dates stored
99
+ as text, and blanks, each with the fix.
98
100
  2. Find the keys. A column in one view whose values appear in a column of
99
101
  the other (`customer_id` and `id`, `order_ref` and `order_no`) is a
100
102
  foreign key; the view with many rows per key is the main one.
101
103
  3. Make the keys match before the join: same type (`convert-type`), same
102
104
  case and padding (`text`), no blanks (`set-values`, `filter`).
103
- 4. Fix what the deliverable needs: numbers stored as text (`convert-type`
104
- to `NUMERIC`), dates stored as text (`convert-type` to `DATE`), blanks.
105
- 5. `view transform join` the lookup view into the main view (`LEFT`), then
106
- read back and check the match rate
107
- ([worked example](references/recipes/end-to-end.md), step 5).
105
+ 4. Act on every `column_warnings` entry for a column the deliverable uses:
106
+ run its `fix` (`convert-type` to `NUMERIC` or `DATE`; the values that are
107
+ not numbers become empty), then decide on blanks (fill, filter, or keep and
108
+ say so). A text `price` cannot be summed on a dashboard.
109
+ 5. `view transform join` the lookup view into the main view (`LEFT`). The
110
+ result's `join_check` gives `match_rate`, `unmatched_rows` and
111
+ `unmatched_keys`; put them in your report, and stop to compare the keys
112
+ if more than a few rows found no match.
108
113
  6. Build the dashboard from the joined view
109
114
  ([dashboards](references/recipes/dashboards.md)).
110
115
 
116
+ For the defaults that most pipelines use (new column or overwrite, `LEFT`
117
+ joins, date steps), see
118
+ [transforms](references/recipes/transforms.md#what-most-pipelines-look-like).
111
119
  Say what you inferred (which columns you joined on, what you converted) in
112
120
  your report. If no column links the files, ask before you combine them.
113
121
 
@@ -149,6 +157,10 @@ your report. If no column links the files, ask before you combine them.
149
157
  target column *before* the change so the after-read has a comparison.
150
158
  - A uniform result (every amount 0, every region "Unknown", most join rows
151
159
  unmatched) means the previous step went wrong; stop and re-inspect it.
160
+ - `column_warnings` on a data read and `join_check` on a join are findings,
161
+ not decoration: fix each one the deliverable depends on, or say in the
162
+ report why you kept it. `view data get` pages with `{"offset": 51,
163
+ "limit": 50}`.
152
164
  - A timeout, exit 7 or interruption does not prove failure; reconcile an
153
165
  `outcome_unknown` (`job get`, `view task list`) before replaying.
154
166
  - Run the [report checklist](references/report-checklist.md) before stating
@@ -22,6 +22,8 @@ mammoth view list CUSTOMERS_DS --project PROJECT_ID # -> CUSTOMERS_VIEW
22
22
  # 2. look before you change: column names, types, row_count, a sample
23
23
  mammoth view get ORDERS_VIEW ORDERS_DS --project PROJECT_ID
24
24
  mammoth view data get ORDERS_VIEW ORDERS_DS --project PROJECT_ID
25
+ # data.column_warnings: text columns of numbers/dates (with the convert-type fix) and blanks;
26
+ # plan step 3 from them
25
27
 
26
28
  # 3. clean orders (each call waits for its job; read back after each value change)
27
29
  mammoth view transform discard-duplicates ORDERS_VIEW --project PROJECT_ID \
@@ -43,9 +45,10 @@ mammoth view get ORDERS_VIEW ORDERS_DS --project PROJECT_ID # row_count before
43
45
  # 5. join, then check the match rate before building on it
44
46
  mammoth view transform join ORDERS_VIEW --project PROJECT_ID \
45
47
  --input '{"dataset_id": ORDERS_DS, "foreign_view": CUSTOMERS_VIEW, "foreign_dataset_id": CUSTOMERS_DS, "join_type": "LEFT", "on": [{"left": "customer_id", "right": "customer_id"}], "select": ["region"]}'
46
- mammoth view data get ORDERS_VIEW ORDERS_DS --project PROJECT_ID
47
- # if region is empty/"Unknown" on most rows, the key does not match: compare the two key columns' values
48
+ # data.join_check: rows_before/rows_after, match_rate, unmatched_rows, unmatched_keys; report them
49
+ # if many rows are unmatched, the key does not match: compare the two key columns' values
48
50
  # (type, padding, case) with view data get on both views; do not summarise unmatched data
51
+ # rows_after > rows_before: a key repeats in the other view (use lookup for one value per key)
49
52
 
50
53
  # 6. deliverable first: export the cleaned, joined rows before any step that replaces them
51
54
  mammoth view export csv ORDERS_VIEW --project PROJECT_ID \
@@ -73,6 +73,41 @@ mammoth schema get view.transform.join
73
73
  mammoth view transform join VIEW_ID --project PROJECT_ID --input INPUT_JSON
74
74
  ```
75
75
 
76
+ ## What most pipelines look like
77
+
78
+ These patterns come from how Mammoth pipelines are usually built. Use them
79
+ as defaults, and let the user's request override them:
80
+
81
+ - Keep it short. Most pipelines have one to three steps and touch a few
82
+ columns. Add only the steps the deliverable needs.
83
+ - New column or overwrite? `set-values`, `math` and `lookup` take
84
+ `new_column` (add a column) or `existing_column` (replace the values in
85
+ place), never both. Overwriting is as common as adding. If the user said
86
+ "fix", "clean" or "change" a column, use `existing_column`. If they said
87
+ "add", "calculate" or "flag", use `new_column`. If it is not clear, ask.
88
+ - Joins are nearly always `LEFT` on one key column. Use `INNER`, `RIGHT`,
89
+ `OUTER` or a key of two or more columns only when the user asks for it or
90
+ the data needs it, and say why in your report.
91
+ - Check the key before a join or lookup. Most keys go in without a prep
92
+ step, so a type, case or padding difference shows up as unmatched rows. Read
93
+ both key columns first (`view data get`) and act on `join_check`.
94
+ - Date steps (`increment-date`, `extract-date`, and `text` on date columns)
95
+ fail more often than other steps. Before a date step, confirm that the
96
+ column type is `DATE` (`view get`); if it is `TEXT`, run `convert-type`
97
+ first. Check the result on a few rows.
98
+ - Convert several columns to the same type in one `convert-type` call (one
99
+ `conversions` entry for each column), not one call for each column.
100
+ - Remove duplicates near the end, after the cleanup and joins, so the check
101
+ uses the final columns.
102
+ - `filter` also takes a plain-language `prompt` instead of a `condition`.
103
+ Use a `condition` when the rule is exact (a column, an operator, a value).
104
+ Use `prompt` only when the user described the rule in words and no exact
105
+ condition expresses it, then check the kept rows.
106
+ - "Top N" means the largest values first. Use the smallest values first only
107
+ when the user says "bottom", "lowest" or "smallest".
108
+ - The last step of a pipeline is not a signal that it is finished. Say that
109
+ the work is done only after you verify the deliverable.
110
+
76
111
  ## Aggregate or summarise
77
112
 
78
113
  For a grouped summary (totals per region, counts per status) use the typed
@@ -9,11 +9,13 @@ or secrets in an argument, prompt, transcript, or checkpoint.
9
9
  from PyPI into a fresh environment and verify it:
10
10
 
11
11
  ```bash
12
- python3 -m venv .mammoth && .mammoth/bin/python -m pip install --upgrade mammoth-cli
12
+ python3 -m venv .mammoth && .mammoth/bin/python -m pip install --no-cache-dir --upgrade mammoth-cli
13
13
  .mammoth/bin/mammoth --version # or: uv tool install mammoth-cli
14
14
  ```
15
15
 
16
- Pin `mammoth-cli==X.Y.Z` when the task names an approved release. Use the
16
+ Pin `mammoth-cli==X.Y.Z` when the task names an approved release, and
17
+ check that `mammoth --version` prints it (a pip cache can serve an older
18
+ release for a few minutes after a new one is published). Use the
17
19
  host installer script (`installers/mammoth-install.sh` in the repository)
18
20
  only when the operator names it; do not pipe a remote script to a shell on
19
21
  your own initiative. The SDK (`mammoth-io`) comes with the CLI. If a later
@@ -46,6 +46,7 @@ from mammoth_cli.runtime.confirm import (
46
46
  from mammoth_cli.runtime.invocation import Invocation
47
47
  from mammoth_cli.runtime.session import open_service, require_project
48
48
  from mammoth_cli.services.conditions import CONDITION_KWARG, compile_condition
49
+ from mammoth_cli.services.data_quality import column_warnings
49
50
 
50
51
  HandlerResult = tuple[Any, dict[str, Any]]
51
52
 
@@ -424,7 +425,7 @@ def _compile_query_filters(
424
425
  project_id: int | None,
425
426
  document: dict[str, Any],
426
427
  kwargs: dict[str, Any],
427
- ) -> dict[str, str] | None:
428
+ ) -> tuple[dict[str, str] | None, dict[str, str] | None]:
428
429
  """Translate a data query's ``condition`` and ``columns`` to the wire format.
429
430
 
430
431
  The data route takes the backend condition shape (``{internal: {OP: ...}}``,
@@ -433,11 +434,11 @@ def _compile_query_filters(
433
434
  only have one key". The spec is compiled through the shared condition
434
435
  service and built against the view's display -> internal name and type
435
436
  maps, and ``columns`` given as display names are mapped the same way.
436
- Returns the internal -> display map when metadata was fetched, so the row
437
- relabelling can reuse it.
437
+ Returns the internal -> display map and the display -> type map when
438
+ metadata was fetched, so the row relabelling and checks can reuse them.
438
439
  """
439
440
  if CONDITION_KWARG not in document and "columns" not in document:
440
- return None
441
+ return None, None
441
442
  metadata = _dataview_metadata(service, dataset_id, dataview_id, project_id)
442
443
  column_map: dict[str, str] = {}
443
444
  column_types: dict[str, str] = {}
@@ -454,7 +455,9 @@ def _compile_query_filters(
454
455
  columns = document.get("columns")
455
456
  if isinstance(columns, list):
456
457
  kwargs["columns"] = [column_map.get(c, c) if isinstance(c, str) else c for c in columns]
457
- return {internal: display for display, internal in column_map.items()} if metadata else None
458
+ if not metadata:
459
+ return None, None
460
+ return {internal: display for display, internal in column_map.items()}, column_types
458
461
 
459
462
 
460
463
  def _relabel_columns(
@@ -610,11 +613,14 @@ def view_data_get(invocation: Invocation) -> HandlerResult:
610
613
  """Fetch a dataview's data, waiting for the backing job to complete.
611
614
 
612
615
  The dataset is resolved from the view unless given as a trailing positional
613
- or a ``dataset_id`` --input field.
616
+ or a ``dataset_id`` --input field. ``offset`` (1-based) reads a later page
617
+ through the query route. The result carries ``column_warnings`` when the
618
+ rows show a text column of numbers or dates, or blanks.
614
619
  """
615
620
  project_id = require_project(invocation)
616
621
  view_id = _require_int_positional_at(invocation, 0, "view id")
617
622
  document = invocation.load_input() or {}
623
+ limit = document.get("limit", _DATA_GET_DEFAULT_LIMIT)
618
624
  with open_service(invocation) as (service, auth):
619
625
  dataset_id = _resolve_dataset_id(service, invocation, view_id, document)
620
626
  kwargs: dict[str, Any] = {
@@ -622,12 +628,165 @@ def view_data_get(invocation: Invocation) -> HandlerResult:
622
628
  "dataview_id": view_id,
623
629
  "project_id": project_id,
624
630
  }
625
- _forward_optional(document, kwargs, ("timeout", "poll_interval", "sequence"))
626
- data = service.call(_symbol(invocation), **kwargs)
627
- data = _relabel_columns(service, dataset_id, view_id, project_id, data)
628
- return _trim_rows(data, document.get("limit", _DATA_GET_DEFAULT_LIMIT)), _meta(
629
- invocation, auth.workspace_id, project_id
630
- )
631
+ if document.get("offset") is not None:
632
+ kwargs["offset"] = int(document["offset"])
633
+ kwargs["limit"] = int(limit) if int(limit) > 0 else 400
634
+ _forward_optional(document, kwargs, ("sequence",))
635
+ data = service.call(_QUERY_DATA_SYMBOL, **kwargs)
636
+ else:
637
+ _forward_optional(document, kwargs, ("timeout", "poll_interval", "sequence"))
638
+ data = service.call(_symbol(invocation), **kwargs)
639
+ data = _relabel_and_check(service, dataset_id, view_id, project_id, data)
640
+ return _trim_rows(data, limit), _meta(invocation, auth.workspace_id, project_id)
641
+
642
+
643
+ _QUERY_DATA_SYMBOL = "mammoth.api.dataviews.DataviewsAPI.query_data"
644
+
645
+ _DATA_PAGE_SYMBOL = "mammoth.api.dataviews.DataviewsAPI.get_data"
646
+ _MAX_UNMATCHED_KEYS = 5
647
+
648
+
649
+ def join_snapshot(
650
+ service: Any, dataset_id: int, dataview_id: int, project_id: int | None
651
+ ) -> dict[str, Any]:
652
+ """Row count, columns and the first data page of a view (best effort)."""
653
+ snapshot: dict[str, Any] = {"row_count": None, "columns": {}, "rows": []}
654
+ try:
655
+ info = apply_column_renames(
656
+ service.call(
657
+ _DATAVIEW_GET_SYMBOL,
658
+ dataset_id=dataset_id,
659
+ dataview_id=dataview_id,
660
+ project_id=project_id,
661
+ )
662
+ )
663
+ if isinstance(info, dict):
664
+ snapshot["row_count"] = info.get("row_count")
665
+ snapshot["columns"] = {
666
+ c.get(_INTERNAL_NAME_KEY): c.get(_DISPLAY_NAME_KEY)
667
+ for c in info.get(_METADATA_KEY) or []
668
+ if isinstance(c, dict)
669
+ }
670
+ page = service.call(
671
+ _DATA_PAGE_SYMBOL,
672
+ dataset_id=dataset_id,
673
+ dataview_id=dataview_id,
674
+ project_id=project_id,
675
+ )
676
+ page = _relabel_columns(
677
+ service, dataset_id, dataview_id, project_id, page, snapshot["columns"]
678
+ )
679
+ rows = page.get(_ROWS_KEY) if isinstance(page, dict) else None
680
+ snapshot["rows"] = [r for r in rows or [] if isinstance(r, dict)]
681
+ except Exception: # noqa: BLE001 -- the check is advice; the join already ran
682
+ return snapshot
683
+ return snapshot
684
+
685
+
686
+ def with_join_check(data: Any, before: Any, after: dict[str, Any], document: dict[str, Any]) -> Any:
687
+ """Add ``join_check`` (row counts, columns added, match rate) to a join result.
688
+
689
+ ``unmatched_rows`` counts rows where every added column is blank: for a
690
+ LEFT join these are rows whose key found no match in the other view.
691
+ """
692
+ if not isinstance(data, dict) or not isinstance(before, dict):
693
+ return data
694
+ added = [
695
+ name
696
+ for internal, name in after.get("columns", {}).items()
697
+ if internal not in before.get("columns", {}) and isinstance(name, str)
698
+ ]
699
+ rows = after.get("rows") or []
700
+ check: dict[str, Any] = {
701
+ "rows_before": before.get("row_count"),
702
+ "rows_after": after.get("row_count"),
703
+ "columns_added": added,
704
+ "rows_checked": len(rows),
705
+ }
706
+ notes: list[str] = []
707
+ on = document.get("on")
708
+ left_key = None
709
+ if isinstance(on, list) and on and isinstance(on[0], dict):
710
+ left_key = on[0].get("left")
711
+ if added and rows:
712
+ unmatched = [r for r in rows if all(r.get(c) in (None, "") for c in added)]
713
+ check["unmatched_rows"] = len(unmatched)
714
+ check["match_rate"] = round((len(rows) - len(unmatched)) / len(rows), 3)
715
+ if unmatched:
716
+ keys = sorted({str(r.get(left_key)) for r in unmatched if left_key in r})
717
+ check["unmatched_keys"] = keys[:_MAX_UNMATCHED_KEYS]
718
+ notes.append(
719
+ f"{len(unmatched)} of {len(rows)} rows found no match. If that is more "
720
+ "than a few, compare the key columns in both views (type, case, "
721
+ "padding) before you build on this; otherwise say so in your report."
722
+ )
723
+ before_n, after_n = check["rows_before"], check["rows_after"]
724
+ if isinstance(before_n, int) and isinstance(after_n, int):
725
+ if after_n > before_n:
726
+ notes.append(
727
+ f"The join added {after_n - before_n} rows: a key repeats in the other "
728
+ "view, so matching rows repeat. Totals over this view count them twice; "
729
+ "use view transform lookup for one value per key."
730
+ )
731
+ elif after_n < before_n:
732
+ notes.append(
733
+ f"{before_n - after_n} rows had no match and were dropped "
734
+ "(an INNER join keeps matched rows only)."
735
+ )
736
+ if isinstance(after_n, int) and len(rows) < after_n:
737
+ notes.append(f"Match rate is from the first {len(rows)} rows of {after_n}.")
738
+ if notes:
739
+ check["notes"] = notes
740
+ return {**data, "join_check": check}
741
+
742
+
743
+ def _column_profile(
744
+ service: Any, dataset_id: int, dataview_id: int, project_id: int | None
745
+ ) -> tuple[dict[str, str], dict[str, str]]:
746
+ """One metadata read: internal-to-display names, and display name to type."""
747
+ mapping: dict[str, str] = {}
748
+ types: dict[str, str] = {}
749
+ for column in _dataview_metadata(service, dataset_id, dataview_id, project_id):
750
+ internal = column.get(_INTERNAL_NAME_KEY)
751
+ display = column.get(_DISPLAY_NAME_KEY)
752
+ if isinstance(internal, str) and isinstance(display, str):
753
+ mapping[internal] = display
754
+ types[display] = str(column.get("type") or "")
755
+ return mapping, types
756
+
757
+
758
+ def _relabel_and_check(
759
+ service: Any,
760
+ dataset_id: int,
761
+ view_id: int,
762
+ project_id: int | None,
763
+ data: Any,
764
+ mapping: dict[str, str] | None = None,
765
+ types: dict[str, str] | None = None,
766
+ ) -> Any:
767
+ """Relabel a data page to display names and add ``column_warnings``.
768
+
769
+ The metadata read happens only when the page has rows and the caller has
770
+ not read it already (one read serves both the names and the types).
771
+ """
772
+ rows = data.get(_ROWS_KEY) if isinstance(data, dict) else None
773
+ if not isinstance(rows, list) or not rows:
774
+ return _relabel_columns(service, dataset_id, view_id, project_id, data, mapping)
775
+ if mapping is None or types is None:
776
+ mapping, types = _column_profile(service, dataset_id, view_id, project_id)
777
+ data = _relabel_columns(service, dataset_id, view_id, project_id, data, mapping)
778
+ return _with_column_warnings(data, types, view_id)
779
+
780
+
781
+ def _with_column_warnings(data: Any, types: dict[str, str], view_id: int) -> Any:
782
+ """Add ``column_warnings`` for the rows of a data page (never fatal)."""
783
+ if not isinstance(data, dict) or not isinstance(data.get(_ROWS_KEY), list) or not types:
784
+ return data
785
+ try:
786
+ warnings = column_warnings(data[_ROWS_KEY], types, view_id)
787
+ except Exception: # noqa: BLE001 -- a presentation aid must not fail the read
788
+ return data
789
+ return {**data, "column_warnings": warnings} if warnings else data
631
790
 
632
791
 
633
792
  #: Rows a plain ``view data get`` returns unless ``limit`` says otherwise.
@@ -677,9 +836,11 @@ def view_data_query(invocation: Invocation) -> HandlerResult:
677
836
  "project_id": project_id,
678
837
  }
679
838
  _forward_optional(document, kwargs, ("sequence", "offset", "limit", "sort"))
680
- mapping = _compile_query_filters(service, dataset_id, view_id, project_id, document, kwargs)
839
+ mapping, types = _compile_query_filters(
840
+ service, dataset_id, view_id, project_id, document, kwargs
841
+ )
681
842
  data = service.call(_symbol(invocation), **kwargs)
682
- data = _relabel_columns(service, dataset_id, view_id, project_id, data, mapping)
843
+ data = _relabel_and_check(service, dataset_id, view_id, project_id, data, mapping, types)
683
844
  return data, _meta(invocation, auth.workspace_id, project_id)
684
845
 
685
846
 
@@ -19,6 +19,7 @@ enum-typed fields are forwarded as the plain string given on ``--input``.
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
+ from collections.abc import Callable
22
23
  from typing import Any
23
24
 
24
25
  from mammoth_cli.commands.view import (
@@ -26,6 +27,8 @@ from mammoth_cli.commands.view import (
26
27
  _require_discovery_allowed,
27
28
  apply_column_renames,
28
29
  brief_view_record,
30
+ join_snapshot,
31
+ with_join_check,
29
32
  )
30
33
  from mammoth_cli.context import profiles
31
34
  from mammoth_cli.errors.envelope import (
@@ -232,9 +235,20 @@ def _meta(invocation: Invocation, workspace_id: int) -> dict[str, Any]:
232
235
 
233
236
 
234
237
  def _dispatch_view(
235
- invocation: Invocation, view_id: int, method: str, **kwargs: Any
238
+ invocation: Invocation,
239
+ view_id: int,
240
+ method: str,
241
+ *,
242
+ before: Callable[[Any, int], Any] | None = None,
243
+ after: Callable[[Any, int, Any, Any], Any] | None = None,
244
+ **kwargs: Any,
236
245
  ) -> HandlerResult:
237
- """Open the service, dispatch a View method call, and build the envelope."""
246
+ """Open the service, dispatch a View method call, and build the envelope.
247
+
248
+ ``before(service, dataset_id)`` runs ahead of the call and its value is
249
+ handed to ``after(service, dataset_id, state, data)``, which returns the
250
+ result to emit. Both need the exact parent; without one they are skipped.
251
+ """
238
252
  # ``dataset_id`` is invocation-local resource context. It is not a View
239
253
  # transform argument, but passing it through lets the service fetch the
240
254
  # exact parent endpoint and prevents the SDK's legacy bare-view resolver
@@ -254,6 +268,7 @@ def _dispatch_view(
254
268
  if dataset_id is None:
255
269
  dataset_id = parents.lookup(_profile_name(invocation), auth.workspace_id, view_id)
256
270
  require_expected_task_count(service, view_id, dataset_id, document)
271
+ state = before(service, int(dataset_id)) if before and dataset_id is not None else None
257
272
  try:
258
273
  if dataset_id is None:
259
274
  _require_discovery_allowed(invocation, view_id)
@@ -266,6 +281,8 @@ def _dispatch_view(
266
281
  reject_pipeline_reference_errors(service, view_id, dataset_id, response)
267
282
  raise
268
283
  reject_pipeline_reference_errors(service, view_id, dataset_id, data)
284
+ if after is not None and dataset_id is not None:
285
+ data = after(service, int(dataset_id), state, data)
269
286
  return data, _meta(invocation, auth.workspace_id)
270
287
 
271
288
 
@@ -782,7 +799,17 @@ def view_transform_join(invocation: Invocation) -> HandlerResult:
782
799
  _require_field(document, "select")
783
800
  assert document is not None
784
801
  kwargs = _bind_transform_inputs(invocation, document)
785
- return _dispatch_view(invocation, view_id, "join", **kwargs)
802
+ project_id = invocation.project
803
+
804
+ def before(service: Any, dataset_id: int) -> Any:
805
+ return join_snapshot(service, dataset_id, view_id, project_id)
806
+
807
+ def after(service: Any, dataset_id: int, state: Any, data: Any) -> Any:
808
+ return with_join_check(
809
+ data, state, join_snapshot(service, dataset_id, view_id, project_id), document
810
+ )
811
+
812
+ return _dispatch_view(invocation, view_id, "join", before=before, after=after, **kwargs)
786
813
 
787
814
 
788
815
  def view_transform_json_extract(invocation: Invocation) -> HandlerResult:
@@ -271,6 +271,17 @@ def _coerce_document_fields(
271
271
  ) from None
272
272
 
273
273
 
274
+ #: Commands whose request nests the payload: name the nesting on a wrong key.
275
+ _UNKNOWN_FIELD_HINTS: dict[str, str] = {
276
+ "dashboard.canvas.save": (
277
+ "Accepted field: body. Wrap the canvas: "
278
+ '{"body": {"params": {"canvas": CANVAS}}}, where CANVAS is data.canvas '
279
+ "from 'mammoth dashboard canvas get DASHBOARD_ID', edited. An --input file "
280
+ "takes the same wrapped document."
281
+ ),
282
+ }
283
+
284
+
274
285
  def validate_input_fields(command_id: str, document: dict[str, Any] | None) -> None:
275
286
  """Reject unknown ``--input`` keys, then coerce known keys to their type.
276
287
 
@@ -319,7 +330,9 @@ def validate_input_fields(command_id: str, document: dict[str, Any] | None) -> N
319
330
  f"{', '.join(unknown)}."
320
331
  ),
321
332
  exit_status=EXIT_USAGE,
322
- hint=f"Accepted fields: {', '.join(sorted(fields_by_name)) or '(none)' }.",
333
+ hint=_UNKNOWN_FIELD_HINTS.get(
334
+ command_id, f"Accepted fields: {', '.join(sorted(fields_by_name)) or '(none)' }."
335
+ ),
323
336
  details={"unknown": unknown, "accepted": sorted(fields_by_name)},
324
337
  )
325
338
  if is_closed_zero_input(command_id) and document == {}:
@@ -584,7 +584,10 @@ S7_COMMANDS = frozenset(
584
584
  _S7_ADDITIONAL_INPUT_FIELDS: dict[str, tuple[FieldSpec, ...]] = {
585
585
  # The GET data route has no server-side page size; the CLI trims the row
586
586
  # list so a read-back costs a screen of tokens, not the whole view.
587
- "view.data.get": (FieldSpec("limit", required=False, annotation=int, default=50),),
587
+ "view.data.get": (
588
+ FieldSpec("limit", required=False, annotation=int, default=50),
589
+ FieldSpec("offset", required=False, annotation=int, default=1),
590
+ ),
588
591
  # The dataview GET takes a server-side projection: "__min", "__standard",
589
592
  # "__full" or a comma-separated field list.
590
593
  "view.get": (FieldSpec("fields", required=False, annotation=str | None, default=None),),
@@ -0,0 +1,136 @@
1
+ """Column warnings computed from a page of view rows.
2
+
3
+ An agent that reads a view before building on it sees rows, not problems: a
4
+ ``price`` column of numbers with a few ``"N/A"`` values is TEXT, so a chart
5
+ cannot sum it, and blanks drop out of totals. These checks name the problem
6
+ and the command that fixes it, in the output the agent is already reading.
7
+ They run on the rows a command already fetched and never make a request.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ from collections.abc import Iterable, Mapping
15
+ from datetime import datetime
16
+ from typing import Any
17
+
18
+ #: A share of non-blank values at or above this reads as "meant to be" that type.
19
+ _MOSTLY = 0.8
20
+ #: Fewer non-blank values than this are too few to call a pattern.
21
+ _MIN_VALUES = 3
22
+ _MAX_EXAMPLES = 3
23
+
24
+ _NUMBER = re.compile(r"^[-+]?[$€£]?\s*\d[\d,]*(\.\d+)?\s*%?$|^[-+]?[$€£]?\s*\.\d+\s*%?$")
25
+ _DATE_FORMATS = (
26
+ "%Y-%m-%d",
27
+ "%Y/%m/%d",
28
+ "%m/%d/%Y",
29
+ "%d/%m/%Y",
30
+ "%d-%m-%Y",
31
+ "%m-%d-%Y",
32
+ "%Y-%m-%d %H:%M:%S",
33
+ "%Y-%m-%dT%H:%M:%S",
34
+ "%d %b %Y",
35
+ "%b %d, %Y",
36
+ )
37
+
38
+
39
+ def _blank(value: Any) -> bool:
40
+ return value is None or (isinstance(value, str) and not value.strip())
41
+
42
+
43
+ def _is_number(text: str) -> bool:
44
+ return bool(_NUMBER.match(text.strip()))
45
+
46
+
47
+ def _is_date(text: str) -> bool:
48
+ candidate = text.strip()
49
+ for fmt in _DATE_FORMATS:
50
+ try:
51
+ datetime.strptime(candidate, fmt)
52
+ except ValueError:
53
+ continue
54
+ return True
55
+ return False
56
+
57
+
58
+ def _convert_hint(view_id: int | None, column: str, to: str) -> str:
59
+ target = str(view_id) if view_id is not None else "VIEW_ID"
60
+ spec = json.dumps({"conversions": [{"column": column, "to": to}]})
61
+ return f"mammoth view transform convert-type {target} --input '{spec}'"
62
+
63
+
64
+ def column_warnings(
65
+ rows: Iterable[Mapping[str, Any]],
66
+ column_types: Mapping[str, str],
67
+ view_id: int | None = None,
68
+ ) -> list[dict[str, Any]]:
69
+ """Return warnings for text columns that hold numbers or dates, and for blanks.
70
+
71
+ Args:
72
+ rows: Row objects keyed by display name (a data page after relabeling).
73
+ column_types: Display name to Mammoth type (``TEXT``, ``NUMERIC``, ...).
74
+ view_id: The view the rows came from, used in the suggested command.
75
+
76
+ Returns:
77
+ One record per finding: ``column``, ``issue``
78
+ (``numbers_stored_as_text``, ``dates_stored_as_text`` or
79
+ ``blank_values``), ``detail`` and, where one command fixes it, ``fix``.
80
+ Counts are over the rows given (``rows_checked`` on each record).
81
+ """
82
+ materialised = [row for row in rows if isinstance(row, Mapping)]
83
+ if not materialised:
84
+ return []
85
+ checked = len(materialised)
86
+ warnings: list[dict[str, Any]] = []
87
+ for column, col_type in column_types.items():
88
+ values = [row.get(column) for row in materialised if column in row]
89
+ if not values:
90
+ continue
91
+ present = [v for v in values if not _blank(v)]
92
+ blanks = len(values) - len(present)
93
+ if str(col_type).upper() == "TEXT" and len(present) >= _MIN_VALUES:
94
+ texts = [str(v) for v in present]
95
+ numbers = [t for t in texts if _is_number(t)]
96
+ dates = [t for t in texts if _is_date(t)]
97
+ for matched, issue, to, kind in (
98
+ (numbers, "numbers_stored_as_text", "NUMERIC", "numbers"),
99
+ (dates, "dates_stored_as_text", "DATE", "dates"),
100
+ ):
101
+ if len(matched) / len(texts) < _MOSTLY:
102
+ continue
103
+ others = sorted({t for t in texts if t not in set(matched)})
104
+ detail = (
105
+ f"{len(matched)} of {len(texts)} non-blank values are {kind}, "
106
+ "but the column is TEXT: charts and totals cannot use it as a "
107
+ f"{'number' if to == 'NUMERIC' else 'date'}."
108
+ )
109
+ if others:
110
+ shown = ", ".join(repr(o) for o in others[:_MAX_EXAMPLES])
111
+ detail += f" Other values: {shown} (the conversion makes them empty)."
112
+ warnings.append(
113
+ {
114
+ "column": column,
115
+ "issue": issue,
116
+ "detail": detail,
117
+ "fix": _convert_hint(view_id, column, to),
118
+ "rows_checked": checked,
119
+ }
120
+ )
121
+ break
122
+ if blanks:
123
+ warnings.append(
124
+ {
125
+ "column": column,
126
+ "issue": "blank_values",
127
+ "detail": (
128
+ f"{blanks} of {len(values)} rows are blank. Decide before you "
129
+ "summarise: fill them (view transform set-values with "
130
+ "IS_EMPTY, or fill-missing), remove the rows (view transform "
131
+ "filter), or keep them and say so."
132
+ ),
133
+ "rows_checked": checked,
134
+ }
135
+ )
136
+ return warnings