mammoth-cli 2.2.30__tar.gz → 2.2.32__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 (258) hide show
  1. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/PKG-INFO +1 -1
  2. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/__init__.py +1 -1
  3. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/about-mammoth.md +7 -2
  4. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/exports.md +16 -2
  5. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/batch.py +26 -4
  6. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/view.py +243 -57
  7. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/view_ops.py +53 -1
  8. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/errors/envelope.py +7 -0
  9. mammoth_cli-2.2.32/mammoth_cli/services/append_blank_columns.py +57 -0
  10. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/command_contract.py +17 -0
  11. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/pyproject.toml +1 -1
  12. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/batch.yaml +1 -1
  13. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/view.yaml +18 -13
  14. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/.gitignore +0 -0
  15. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/LICENSE +0 -0
  16. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/README.md +0 -0
  17. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/__main__.py +0 -0
  18. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/app.py +0 -0
  19. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/SKILL.md +0 -0
  20. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/auth.md +0 -0
  21. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/capabilities-misc.md +0 -0
  22. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/capabilities.md +0 -0
  23. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/command-index.md +0 -0
  24. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/activity.md +0 -0
  25. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/addon.md +0 -0
  26. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/agent.md +0 -0
  27. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/ai.md +0 -0
  28. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/annotation.md +0 -0
  29. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/auth.md +0 -0
  30. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/automation.md +0 -0
  31. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/batch.md +0 -0
  32. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/billing.md +0 -0
  33. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/browse.md +0 -0
  34. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/calc.md +0 -0
  35. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/capability.md +0 -0
  36. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/client-app.md +0 -0
  37. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/completion.md +0 -0
  38. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/config.md +0 -0
  39. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/connector.md +0 -0
  40. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/context.md +0 -0
  41. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard-qa.md +0 -0
  42. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard-style.md +0 -0
  43. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard-template.md +0 -0
  44. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard.md +0 -0
  45. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/data-app.md +0 -0
  46. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dataset.md +0 -0
  47. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/doctor.md +0 -0
  48. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/external-key.md +0 -0
  49. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/file.md +0 -0
  50. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/folder.md +0 -0
  51. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/job.md +0 -0
  52. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/link.md +0 -0
  53. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/log.md +0 -0
  54. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/notification.md +0 -0
  55. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/parameter.md +0 -0
  56. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/project.md +0 -0
  57. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/report.md +0 -0
  58. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/schedule.md +0 -0
  59. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/schema.md +0 -0
  60. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/skill.md +0 -0
  61. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/snippet.md +0 -0
  62. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/support.md +0 -0
  63. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/template.md +0 -0
  64. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/trash.md +0 -0
  65. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/upgrade.md +0 -0
  66. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/user.md +0 -0
  67. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/version.md +0 -0
  68. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/view-transform.md +0 -0
  69. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/view.md +0 -0
  70. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/webhook.md +0 -0
  71. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/workflow.md +0 -0
  72. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/workspace.md +0 -0
  73. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/handoff.md +0 -0
  74. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/input.md +0 -0
  75. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/jobs-drafts.md +0 -0
  76. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/machine-output.md +0 -0
  77. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/operations.md +0 -0
  78. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/auth-scope.md +0 -0
  79. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/cleanup.md +0 -0
  80. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/dashboards.md +0 -0
  81. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/database-import.md +0 -0
  82. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/end-to-end.md +0 -0
  83. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/index.md +0 -0
  84. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/need-action.md +0 -0
  85. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/resources.md +0 -0
  86. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/scheduling.md +0 -0
  87. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/transforms.md +0 -0
  88. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/typed-etl-discovery.md +0 -0
  89. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recovery.md +0 -0
  90. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/report-checklist.md +0 -0
  91. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/retention.md +0 -0
  92. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/safety.md +0 -0
  93. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/task-start.md +0 -0
  94. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/__init__.py +0 -0
  95. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/activity.py +0 -0
  96. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/addon.py +0 -0
  97. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/agent.py +0 -0
  98. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/ai.py +0 -0
  99. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/annotation.py +0 -0
  100. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/auth.py +0 -0
  101. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/automation.py +0 -0
  102. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/billing.py +0 -0
  103. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/browse.py +0 -0
  104. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/calc.py +0 -0
  105. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/capability.py +0 -0
  106. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/client_app.py +0 -0
  107. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/completion.py +0 -0
  108. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/config.py +0 -0
  109. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/connector.py +0 -0
  110. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/context.py +0 -0
  111. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/dashboard.py +0 -0
  112. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/data_app.py +0 -0
  113. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/dataset.py +0 -0
  114. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/doctor.py +0 -0
  115. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/external_key.py +0 -0
  116. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/file.py +0 -0
  117. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/folder.py +0 -0
  118. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/job.py +0 -0
  119. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/link.py +0 -0
  120. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/notification.py +0 -0
  121. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/parameter.py +0 -0
  122. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/project.py +0 -0
  123. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/registry.py +0 -0
  124. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/report.py +0 -0
  125. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/schedule.py +0 -0
  126. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/schema.py +0 -0
  127. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/skill.py +0 -0
  128. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/snippet.py +0 -0
  129. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/support.py +0 -0
  130. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/template.py +0 -0
  131. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/trash.py +0 -0
  132. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/upgrade.py +0 -0
  133. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/user.py +0 -0
  134. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/view_profile.py +0 -0
  135. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/webhook.py +0 -0
  136. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/workflow.py +0 -0
  137. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/workspace.py +0 -0
  138. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/context/__init__.py +0 -0
  139. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/context/credentials.py +0 -0
  140. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/context/endpoint.py +0 -0
  141. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/context/profiles.py +0 -0
  142. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/context/resolver.py +0 -0
  143. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/contracts/__init__.py +0 -0
  144. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/contracts/auth.py +0 -0
  145. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/embed.py +0 -0
  146. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/errors/__init__.py +0 -0
  147. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/manifest/__init__.py +0 -0
  148. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/manifest/loader.py +0 -0
  149. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/messages/__init__.py +0 -0
  150. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/output/__init__.py +0 -0
  151. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/output/envelope.py +0 -0
  152. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/output/normalize.py +0 -0
  153. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/output/policy.py +0 -0
  154. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/output/progress.py +0 -0
  155. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/output/render.py +0 -0
  156. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/py.typed +0 -0
  157. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/__init__.py +0 -0
  158. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/confirm.py +0 -0
  159. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/dataset_health.py +0 -0
  160. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/dryrun.py +0 -0
  161. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/dryrun_targets.py +0 -0
  162. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/embedded.py +0 -0
  163. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/executor.py +0 -0
  164. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/input_loader.py +0 -0
  165. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/intent_only.py +0 -0
  166. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/invocation.py +0 -0
  167. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/new_data.py +0 -0
  168. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/options.py +0 -0
  169. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/parents.py +0 -0
  170. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/runlog.py +0 -0
  171. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/session.py +0 -0
  172. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/state.py +0 -0
  173. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/strict.py +0 -0
  174. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/updates.py +0 -0
  175. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/validate.py +0 -0
  176. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/verify.py +0 -0
  177. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/__init__.py +0 -0
  178. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/argspec.py +0 -0
  179. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/board_values.py +0 -0
  180. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/coerce.py +0 -0
  181. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/conditional_format.py +0 -0
  182. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/conditions.py +0 -0
  183. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/dashboard_filters.py +0 -0
  184. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/dashboard_pages.py +0 -0
  185. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/dashboard_review.py +0 -0
  186. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/data_profile.py +0 -0
  187. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/data_quality.py +0 -0
  188. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/dispatch.py +0 -0
  189. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/factory.py +0 -0
  190. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/input_fields.py +0 -0
  191. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/listing.py +0 -0
  192. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/local_time.py +0 -0
  193. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/mapping.py +0 -0
  194. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/openapi_types.py +0 -0
  195. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/positionals.py +0 -0
  196. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/protocol.py +0 -0
  197. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/read_queries.py +0 -0
  198. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/sdk_service.py +0 -0
  199. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/sql_check.py +0 -0
  200. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/stored_stats.py +0 -0
  201. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/testing.py +0 -0
  202. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/text_dates.py +0 -0
  203. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/type_system.py +0 -0
  204. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/write_impact.py +0 -0
  205. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/skills/__init__.py +0 -0
  206. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/skills/installer.py +0 -0
  207. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/skills/steering.py +0 -0
  208. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/testing.py +0 -0
  209. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/activity.yaml +0 -0
  210. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/addon.yaml +0 -0
  211. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/agent.yaml +0 -0
  212. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/ai.yaml +0 -0
  213. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/annotation.yaml +0 -0
  214. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/auth.yaml +0 -0
  215. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/automation.yaml +0 -0
  216. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/billing.yaml +0 -0
  217. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/browse.yaml +0 -0
  218. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/calc.yaml +0 -0
  219. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/capability.yaml +0 -0
  220. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/client-app.yaml +0 -0
  221. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/completion.yaml +0 -0
  222. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/config.yaml +0 -0
  223. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/connector.yaml +0 -0
  224. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/context.yaml +0 -0
  225. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/dashboard.yaml +0 -0
  226. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/data-app.yaml +0 -0
  227. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/dataset.yaml +0 -0
  228. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/doctor.yaml +0 -0
  229. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/external-key.yaml +0 -0
  230. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/file.yaml +0 -0
  231. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/folder.yaml +0 -0
  232. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/job.yaml +0 -0
  233. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/link.yaml +0 -0
  234. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/log.yaml +0 -0
  235. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/notification.yaml +0 -0
  236. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/parameter.yaml +0 -0
  237. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/project.yaml +0 -0
  238. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/report.yaml +0 -0
  239. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/schedule.yaml +0 -0
  240. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/schema.yaml +0 -0
  241. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/skill.yaml +0 -0
  242. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/snippet.yaml +0 -0
  243. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/support.yaml +0 -0
  244. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/template.yaml +0 -0
  245. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/trash.yaml +0 -0
  246. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/upgrade.yaml +0 -0
  247. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/user.yaml +0 -0
  248. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/version.yaml +0 -0
  249. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/webhook.yaml +0 -0
  250. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/workflow.yaml +0 -0
  251. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/workspace.yaml +0 -0
  252. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/edits-target.source.yaml +0 -0
  253. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/openapi-operations.yaml +0 -0
  254. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/schema-v1.json +0 -0
  255. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/sdk-catalog.source.yaml +0 -0
  256. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/sdk-methods.yaml +0 -0
  257. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/openapi/metadata.json +0 -0
  258. {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/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.30
3
+ Version: 2.2.32
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.2.30"
9
+ __version__ = "2.2.32"
10
10
 
11
11
  # The versioned machine output envelope contract.
12
12
  SCHEMA_VERSION = 1
@@ -106,7 +106,7 @@ Checkpoint Alert are the `view data-check` and `view checkpoint` families.
106
106
  | Add columns from another view by matching keys | Join | `join` (`INNER`, `LEFT`, `RIGHT`, `OUTER`) |
107
107
  | Bring one value per key from another view | Lookup | `lookup` |
108
108
  | Totals, counts, averages per group | Group & Pivot | `pivot` (replaces the view's columns) |
109
- | A rows-by-columns summary table | Group & Pivot (crosstab) | `crosstab` (writes a new dataset) |
109
+ | A rows-by-columns summary table | Group & Pivot (crosstab) | `crosstab` (writes a new dataset, and keeps rewriting it on every pipeline run of the view) |
110
110
  | Turn wide columns into label/value rows | Unpivot | `unnest` |
111
111
  | Read fields out of a JSON column | Extract JSON | `json-extract` |
112
112
  | Classify, tag or summarise rows with AI | Generative AI | `ai` |
@@ -146,7 +146,12 @@ No command:
146
146
  every source column to an existing target column. A partial mapping, or a
147
147
  destination the target lacks, is refused with `append_mapping_incomplete`
148
148
  before anything is written (the backend would append only the mapped columns
149
- and add a new column for the unknown name). An append is a standing link: it is stored
149
+ and add a new column for the unknown name). A target column the append
150
+ leaves unfilled would be NULL in every appended row: it is refused with
151
+ `append_leaves_columns_blank` unless the input names exactly those columns,
152
+ e.g. `"blank_columns": ["id","score"]` (a deliberate partial append, such as
153
+ mapping only `name`); never delete source columns or clone the source view to
154
+ get around it. An append is a standing link: it is stored
150
155
  as a pipeline step of the source view, so every later re-run of that view
151
156
  appends again into the target dataset, not just the first run. A column
152
157
  with the same name but a different type in the source view and the target
@@ -55,7 +55,11 @@ there is no separate Auto-Sync endpoint or field to find.
55
55
  `target_ds_id` writes into an existing dataset instead of creating one
56
56
  (`save_as_mode` `REPLACE_IN_DS` or `APPEND_TO_DS`). Read the delivery back in
57
57
  the target project (`view list N --project P`, `view data get`) before
58
- reporting it. Do not build this with `view export create`: the raw
58
+ reporting it. An append that leaves target columns unfilled (a partial
59
+ `column_mapping`, or a source with fewer columns) is refused with
60
+ `append_leaves_columns_blank` unless the input lists exactly those columns,
61
+ `"blank_columns": ["id","score"]`; confirm that with the user, and never
62
+ clone the source view or delete its columns to get around the refusal. Do not build this with `view export create`: the raw
59
63
  `internal_dataset` spec needs `USER_ID` and the `export_project` /
60
64
  `project_id` / `source_project_id` trio, and without them the backend answers
61
65
  `4GENR007 Validation error` with no detail; the typed command fills them.
@@ -84,7 +88,7 @@ the run log. The connector command is `external_effect` with
84
88
  mammoth schema get view.export.postgres # read secret_fields, required fields
85
89
  # operator writes /private/path/request.json (mode 0600):
86
90
  # {"host":"db.example","port":5432,"database":"analytics","table":"exports",
87
- # "username":"agent","password":"…","dataset_id":DATASET_ID}
91
+ # "username":"agent","password":"…","replace_table":true,"dataset_id":DATASET_ID}
88
92
  mammoth view export postgres VIEW_ID DATASET_ID --project PROJECT_ID \
89
93
  --input /private/path/request.json --yes
90
94
  mammoth view export list VIEW_ID DATASET_ID --project PROJECT_ID
@@ -93,6 +97,16 @@ mammoth view export list VIEW_ID DATASET_ID --project PROJECT_ID
93
97
  If the operator has not supplied such a file, stop and ask for it; do not
94
98
  compose the body yourself from values seen in chat.
95
99
 
100
+ A database export (`postgres`, `mysql`, `mssql`, `redshift`, `bigquery` with
101
+ `export_type` REPLACE, and `publish-db`) drops and recreates its destination
102
+ table now and on every later run of the view's pipeline, so any existing table
103
+ of that name is lost. It is refused (`replace_table_not_acknowledged`) unless
104
+ the input says `"replace_table": true`; only set that for a table the user said
105
+ may be overwritten. `postgres` and `mysql` also cut a table name to 60
106
+ characters and write a different table, so a longer name is refused
107
+ (`table_name_too_long`). The result carries `refreshes_on_pipeline_run` and a
108
+ `note` saying so.
109
+
96
110
  ## Generic export routes
97
111
 
98
112
  `view export csv` and the typed destination commands are the proven path. The
@@ -25,6 +25,10 @@ from mammoth_cli.manifest.loader import command_by_id
25
25
  from mammoth_cli.runtime.confirm import POLICY_PROMPT_OR_YES, enforce_confirmation
26
26
  from mammoth_cli.runtime.invocation import Invocation
27
27
  from mammoth_cli.runtime.session import open_service, require_project
28
+ from mammoth_cli.services.append_blank_columns import (
29
+ BLANK_COLUMNS_FIELD,
30
+ acknowledged_blank_columns,
31
+ )
28
32
 
29
33
  HandlerResult = tuple[Any, dict[str, Any]]
30
34
 
@@ -221,15 +225,32 @@ def _reject_type_change(item: dict[str, Any], dest_col: dict[str, Any], dataset_
221
225
  )
222
226
 
223
227
 
228
+ def _filled_destination_names(
229
+ items: list[dict[str, Any]], dest: list[dict[str, Any]], source: list[dict[str, Any]]
230
+ ) -> set[str]:
231
+ """Destination column names some mapping item fills (``add_column`` items included)."""
232
+ dest_names = {col["c_name"] for col in dest}
233
+ filled: set[str] = set()
234
+ for item in items:
235
+ _, dest_col = _resolve_item(item, source, dest)
236
+ if dest_col is not None:
237
+ filled.add(dest_col["c_name"])
238
+ elif item.get("action") == "add_column" and item.get("destination_c_name") in dest_names:
239
+ filled.add(item["destination_c_name"])
240
+ return filled
241
+
242
+
224
243
  def _checked_batch_mapping(
225
- service: Any, dataset_id: int, source_id: int, mapping: Any
244
+ service: Any, dataset_id: int, source_id: int, mapping: Any, blank_columns: Any = None
226
245
  ) -> tuple[list[dict[str, Any]], list[str]]:
227
246
  """Validate a batch mapping against both schemas; return typed items and warnings.
228
247
 
229
248
  Refuses unmapped source columns and unknown destinations (the backend appends
230
249
  only the mapped columns) and stamps every item with the destination's actual
231
250
  current type, so no destination column is silently re-typed. A mapped pair
232
- whose source and destination types differ is returned as a warning.
251
+ whose source and destination types differ is returned as a warning. A
252
+ destination column no item fills is refused unless ``blank_columns`` names
253
+ exactly those columns; the confirmed blanks are returned as a warning.
233
254
  """
234
255
  source = _dataset_columns(service, source_id, "source")
235
256
  dest = _dataset_columns(service, dataset_id, "target")
@@ -258,7 +279,8 @@ def _checked_batch_mapping(
258
279
  _reject_incomplete_batch_mapping(
259
280
  (dataset_id, source_id), {col["c_name"] for col in source}, mapped_sources, unknown
260
281
  )
261
- return typed, warnings
282
+ blank = sorted({col["c_name"] for col in dest} - _filled_destination_names(typed, dest, source))
283
+ return typed, acknowledged_blank_columns(dataset_id, blank, blank_columns) + warnings
262
284
 
263
285
 
264
286
  def batch_create(invocation: Invocation) -> HandlerResult:
@@ -297,7 +319,7 @@ def batch_create(invocation: Invocation) -> HandlerResult:
297
319
  )
298
320
  with open_service(invocation) as (service, auth):
299
321
  kwargs["mapping"], warnings = _checked_batch_mapping(
300
- service, dataset_id, source_id, mapping
322
+ service, dataset_id, source_id, mapping, document.get(BLANK_COLUMNS_FIELD)
301
323
  )
302
324
  data = service.call(_symbol(invocation), **kwargs)
303
325
  if warnings and isinstance(data, dict):
@@ -55,6 +55,10 @@ from mammoth_cli.runtime.invocation import Invocation
55
55
  from mammoth_cli.runtime.session import open_service, require_project
56
56
  from mammoth_cli.services import conditional_format as cf_rules
57
57
  from mammoth_cli.services import read_queries, text_dates
58
+ from mammoth_cli.services.append_blank_columns import (
59
+ BLANK_COLUMNS_FIELD,
60
+ acknowledged_blank_columns,
61
+ )
58
62
  from mammoth_cli.services.conditions import CONDITION_KWARG, compile_condition
59
63
  from mammoth_cli.services.dashboard_review import UPLOAD_NOTE, upload_hints
60
64
  from mammoth_cli.services.data_quality import column_warnings, duplicate_rows_fact
@@ -2963,13 +2967,53 @@ def view_pipeline_rerun(invocation: Invocation) -> HandlerResult:
2963
2967
  service, invocation, dataview_id, document, 1
2964
2968
  )
2965
2969
  before = _rows_before(service, dataset_id, dataview_id, invocation.project)
2970
+ fired = _end_of_pipeline_exports(service, dataview_id, dataset_id)
2966
2971
  data = service.call(_symbol(invocation), **kwargs)
2967
2972
  data = _settle_async_view_write(
2968
2973
  service, dataset_id, dataview_id, invocation.project, data, before
2969
2974
  )
2975
+ if fired and isinstance(data, dict):
2976
+ data = {**data, "exports_fired": fired}
2970
2977
  return data, _meta(invocation, auth.workspace_id, None)
2971
2978
 
2972
2979
 
2980
+ #: Non-secret ``target_properties`` keys that say where an export writes.
2981
+ _EXPORT_TARGET_HINT_KEYS = ("table", "host", "database", "TARGET_DS_ID", "DS_NAME", "emails")
2982
+
2983
+
2984
+ def _end_of_pipeline_exports(
2985
+ service: Any, dataview_id: int, dataset_id: int | None
2986
+ ) -> list[dict[str, Any]]:
2987
+ """The live end-of-pipeline exports a rerun of this view fires again.
2988
+
2989
+ A rerun re-sends every one of them to its destination (database, email, REST
2990
+ endpoint, another dataset), so the caller is told which, not just that the
2991
+ pipeline ran. Soft-deleted exports do not fire.
2992
+ """
2993
+ listing = service.call(
2994
+ _EXPORTS_LIST_SYMBOL,
2995
+ dataview_id=dataview_id,
2996
+ dataset_id=dataset_id,
2997
+ end_of_pipeline=True,
2998
+ )
2999
+ fired: list[dict[str, Any]] = []
3000
+ for export in getattr(listing, "exports", None) or []:
3001
+ if export.status is ExportStatus.DELETED:
3002
+ continue
3003
+ handler = export.handler_type
3004
+ properties = export.target_properties or {}
3005
+ fired.append(
3006
+ {
3007
+ "export_id": export.id,
3008
+ "handler_type": getattr(handler, "value", handler),
3009
+ "target": {
3010
+ key: properties[key] for key in _EXPORT_TARGET_HINT_KEYS if key in properties
3011
+ },
3012
+ }
3013
+ )
3014
+ return fired
3015
+
3016
+
2973
3017
  #: Seconds ``view pipeline wait`` waits when the caller names no ``timeout`` (the SDK's own
2974
3018
  #: default is an hour, one request every few seconds for as long as it runs).
2975
3019
  _PIPELINE_WAIT_TIMEOUT = 300.0
@@ -3261,20 +3305,24 @@ def view_export_publish_db(invocation: Invocation) -> HandlerResult:
3261
3305
  document = invocation.load_input()
3262
3306
  odbc_type = _require_field(document, "odbc_type")
3263
3307
  target_properties = _require_field(document, "target_properties")
3308
+ assert document is not None
3309
+ table = target_properties.get("table") if isinstance(target_properties, dict) else None
3264
3310
  enforce_confirmation(
3265
3311
  invocation, policy=POLICY_YES_ALWAYS, action=f"publish-db on view {dataview_id}"
3266
3312
  )
3313
+ table_note = _reject_unacknowledged_table_replace(invocation.command_id, document, table)
3267
3314
  kwargs: dict[str, Any] = {
3268
3315
  "dataview_id": dataview_id,
3269
3316
  "odbc_type": odbc_type,
3270
3317
  "target_properties": target_properties,
3271
3318
  "project_id": project_id,
3272
3319
  }
3273
- assert document is not None
3274
3320
  _forward_optional(document, kwargs, ("dataset_id",))
3275
3321
  with open_service(invocation) as (service, auth):
3276
3322
  _require_exact_parent(service, invocation, dataview_id, kwargs)
3277
3323
  data = service.call(_symbol(invocation), **kwargs)
3324
+ if table_note is not None:
3325
+ data = _with_recurrence_note(data, table_note)
3278
3326
  return data, _meta(invocation, auth.workspace_id, project_id)
3279
3327
 
3280
3328
 
@@ -3453,6 +3501,81 @@ def _reject_export_into_draft(service: Any, dataview_id: int, dataset_id: int) -
3453
3501
  )
3454
3502
 
3455
3503
 
3504
+ # Database exports rebuild their destination table: the distributary drops it
3505
+ # and recreates it from the view on the first write and on every later pipeline
3506
+ # run (BigQuery only for export_type REPLACE, its WRITE_TRUNCATE mode). Postgres
3507
+ # and MySQL also cut a table name to 60 characters, silently writing elsewhere.
3508
+ _REPLACE_TABLE_FIELD = "replace_table"
3509
+ _DB_REPLACE_ROUTES = frozenset(
3510
+ {
3511
+ "view.export.postgres",
3512
+ "view.export.mysql",
3513
+ "view.export.mssql",
3514
+ "view.export.redshift",
3515
+ "view.export.bigquery",
3516
+ }
3517
+ )
3518
+ _DB_TABLE_NAME_LIMIT_ROUTES = frozenset({"view.export.postgres", "view.export.mysql"})
3519
+ _DB_TABLE_NAME_LIMIT = 60
3520
+
3521
+
3522
+ def _replaces_table(command_id: str, document: dict[str, Any]) -> bool:
3523
+ """Whether this database export drops and recreates its destination table."""
3524
+ if command_id == "view.export.bigquery":
3525
+ return str(document.get("export_type") or "REPLACE").upper() == "REPLACE"
3526
+ return command_id in _DB_REPLACE_ROUTES or command_id == "view.export.publish-db"
3527
+
3528
+
3529
+ def _reject_unacknowledged_table_replace(
3530
+ command_id: str, document: dict[str, Any], table: Any
3531
+ ) -> str | None:
3532
+ """Refuse a database export that is not explicitly allowed to replace its table.
3533
+
3534
+ Returns the recurrence note for the result, or ``None`` when the export
3535
+ does not replace a table.
3536
+ """
3537
+ if not _replaces_table(command_id, document):
3538
+ return None
3539
+ name = str(table)
3540
+ if command_id in _DB_TABLE_NAME_LIMIT_ROUTES and len(name) > _DB_TABLE_NAME_LIMIT:
3541
+ raise CliError(
3542
+ code="table_name_too_long",
3543
+ message=(
3544
+ f"Table name {name!r} is {len(name)} characters; the database export cuts "
3545
+ f"table names to {_DB_TABLE_NAME_LIMIT} and would write a different table "
3546
+ f"({name[:_DB_TABLE_NAME_LIMIT]!r})."
3547
+ ),
3548
+ exit_status=EXIT_USAGE,
3549
+ hint=f"Use a table name of at most {_DB_TABLE_NAME_LIMIT} characters.",
3550
+ details={"table": name, "length": len(name), "limit": _DB_TABLE_NAME_LIMIT},
3551
+ )
3552
+ if document.get(_REPLACE_TABLE_FIELD) is not True:
3553
+ raise CliError(
3554
+ code="replace_table_not_acknowledged",
3555
+ message=(
3556
+ f"This export drops and recreates table {name!r} in the destination database "
3557
+ "now and on every pipeline run of the view; any existing table of that name "
3558
+ "and its rows are lost."
3559
+ ),
3560
+ exit_status=EXIT_USAGE,
3561
+ hint=(
3562
+ f'Pick a table that may be overwritten, then add "{_REPLACE_TABLE_FIELD}": true '
3563
+ "to the input to confirm."
3564
+ ),
3565
+ details={"table": name, "field": _REPLACE_TABLE_FIELD},
3566
+ )
3567
+ return f"drops and recreates {name} now and on every pipeline run"
3568
+
3569
+
3570
+ def _with_recurrence_note(data: Any, note: str) -> dict[str, Any]:
3571
+ """The export result as a mapping carrying ``refreshes_on_pipeline_run`` and ``note``."""
3572
+ result = data.model_dump(mode="json") if hasattr(data, "model_dump") else data
3573
+ record = dict(result) if isinstance(result, dict) else {"result": result}
3574
+ record["refreshes_on_pipeline_run"] = True
3575
+ record["note"] = note
3576
+ return record
3577
+
3578
+
3456
3579
  def view_export_specialized(invocation: Invocation) -> HandlerResult:
3457
3580
  """Run one of the SDK's typed ``View.export`` destination helpers."""
3458
3581
  route = _SPECIAL_EXPORTS.get(invocation.command_id)
@@ -3493,6 +3616,10 @@ def view_export_specialized(invocation: Invocation) -> HandlerResult:
3493
3616
  )
3494
3617
  common_fields = _SPECIAL_EXPORT_COMMON_FIELDS if accepts_var_keyword else frozenset()
3495
3618
  allowed = explicit_fields | common_fields | {_DATASET_ID_FIELD}
3619
+ if invocation.command_id in _DB_REPLACE_ROUTES:
3620
+ allowed = allowed | {_REPLACE_TABLE_FIELD}
3621
+ if invocation.command_id == "view.export.dataset":
3622
+ allowed = allowed | {BLANK_COLUMNS_FIELD}
3496
3623
  unknown = sorted(set(document) - allowed)
3497
3624
  if unknown:
3498
3625
  raise CliError(
@@ -3520,8 +3647,13 @@ def view_export_specialized(invocation: Invocation) -> HandlerResult:
3520
3647
  policy=str(export_record.get("confirmation") or POLICY_YES_ALWAYS),
3521
3648
  action=f"export view {dataview_id} via {invocation.command_id.rsplit('.', 1)[-1]}",
3522
3649
  )
3650
+ table_note = _reject_unacknowledged_table_replace(
3651
+ invocation.command_id, document, document.get("table")
3652
+ )
3523
3653
  kwargs = dict(document)
3524
3654
  kwargs.pop(_DATASET_ID_FIELD, None)
3655
+ kwargs.pop(_REPLACE_TABLE_FIELD, None)
3656
+ blank_columns = kwargs.pop(BLANK_COLUMNS_FIELD, None)
3525
3657
  is_dataset_route = invocation.command_id == "view.export.dataset"
3526
3658
  target_ds_id = kwargs.get("target_ds_id") if is_dataset_route else None
3527
3659
  save_as_mode = kwargs.get("save_as_mode") if is_dataset_route else None
@@ -3530,62 +3662,39 @@ def view_export_specialized(invocation: Invocation) -> HandlerResult:
3530
3662
  with open_service(invocation) as (service, auth):
3531
3663
  if dataset_id is None:
3532
3664
  dataset_id = _resolve_dataset_id(service, invocation, dataview_id, document)
3533
- if target_ds_id is not None and int(target_ds_id) == dataset_id:
3534
- raise CliError(
3535
- code=CODE_INVALID_ARGUMENTS,
3536
- message="target_ds_id equals the view's own dataset.",
3537
- exit_status=EXIT_USAGE,
3538
- hint=(
3539
- "A view cannot write into its own dataset. Export to a new "
3540
- "dataset name (omit target_ds_id), or target a different dataset."
3541
- ),
3542
- details={"dataset_id": dataset_id, "target_ds_id": int(target_ds_id)},
3543
- )
3544
3665
  if is_dataset_route:
3545
- _reject_export_into_draft(service, dataview_id, dataset_id)
3666
+ reject_dataset_export_conflicts(
3667
+ service,
3668
+ dataview_id,
3669
+ dataset_id,
3670
+ int(target_ds_id) if target_ds_id is not None else None,
3671
+ )
3546
3672
  target_view_before = None
3673
+ blank_warnings: list[str] = []
3547
3674
  target_only_columns: list[str] = []
3548
3675
  mapped_type_warnings: list[str] = []
3676
+ hidden_left_out = (
3677
+ _hidden_column_names(service, dataset_id, dataview_id, project_id)
3678
+ if is_dataset_route and target_ds_id is None
3679
+ else []
3680
+ )
3549
3681
  if target_ds_id is not None:
3550
- # Each `view export dataset` call creates its own PERSISTENT
3551
- # export trigger on this view, which re-runs on every pipeline
3552
- # run; REPLACE_IN_DS/APPEND_TO_DS only ever touch that trigger's
3553
- # OWN rows. A second export from this view into the same target
3554
- # is therefore a second writer, not a refresh of the first --
3555
- # the fix is to rerun the pipeline, not to export again.
3556
- existing_export = _existing_internal_dataset_export(
3557
- service, dataview_id, dataset_id, int(target_ds_id)
3558
- )
3559
- if existing_export is not None:
3560
- raise CliError(
3561
- code="export_already_exists",
3562
- message=(
3563
- f"View {dataview_id} already exports into dataset "
3564
- f"{int(target_ds_id)} (export {existing_export.id}). This export "
3565
- "re-runs on every pipeline run, and REPLACE_IN_DS replaces only "
3566
- "its own rows -- a second export into the same target duplicates "
3567
- "rows instead of refreshing them."
3568
- ),
3569
- exit_status=EXIT_USAGE,
3570
- hint="Refresh the target by rerunning the pipeline, not by exporting again.",
3571
- recovery_commands=[f"mammoth view pipeline rerun {dataview_id}"],
3572
- details={
3573
- "dataview_id": dataview_id,
3574
- "target_ds_id": int(target_ds_id),
3575
- "export_id": existing_export.id,
3576
- },
3577
- )
3578
3682
  # rows_before is the target's own count ahead of this write, read
3579
3683
  # before the call so a later re-read can never be confused with it.
3580
3684
  target_view_before = _dataset_view_info(service, int(target_ds_id), result_project_id)
3581
- if save_as_mode == "APPEND_TO_DS":
3582
- target_only_columns, mapped_type_warnings = _reject_append_schema_mismatch(
3583
- service,
3584
- dataview_id,
3585
- dataset_id,
3586
- int(target_ds_id),
3587
- kwargs.get("column_mapping"),
3588
- )
3685
+ # REPLACE_IN_DS into an existing target runs the same backend
3686
+ # append code as APPEND_TO_DS (same mapping and schema handling),
3687
+ # so both modes get the same guards.
3688
+ target_only_columns, mapped_type_warnings = _reject_append_schema_mismatch(
3689
+ service,
3690
+ dataview_id,
3691
+ dataset_id,
3692
+ int(target_ds_id),
3693
+ kwargs.get("column_mapping"),
3694
+ )
3695
+ blank_warnings = acknowledged_blank_columns(
3696
+ int(target_ds_id), target_only_columns, blank_columns
3697
+ )
3589
3698
  try:
3590
3699
  data = service.call_view(dataview_id, method, dataset_id=dataset_id, **kwargs)
3591
3700
  except CliError as error:
@@ -3642,19 +3751,19 @@ def view_export_specialized(invocation: Invocation) -> HandlerResult:
3642
3751
  # a non-increase here as unverified, not just an omitted count.
3643
3752
  row_check["expected_row_increase"] = True
3644
3753
  warnings = list(mapped_type_warnings)
3645
- if target_only_columns:
3646
- # Allowed -- an append never has to cover every target column
3647
- # -- but worth surfacing rather than leaving silent.
3648
- warnings.insert(
3649
- 0,
3650
- "target dataset has column(s) the source view does not (kept "
3651
- "as-is): " + ", ".join(target_only_columns),
3754
+ if hidden_left_out:
3755
+ warnings.append(
3756
+ "hidden column(s) of the source view are not in the new dataset: "
3757
+ + ", ".join(hidden_left_out)
3652
3758
  )
3759
+ warnings[:0] = blank_warnings
3653
3760
  if warnings:
3654
3761
  row_check["warnings"] = warnings
3655
3762
  data["row_check"] = row_check
3656
3763
  if pipeline_error is not None:
3657
3764
  data["pipeline_error"] = pipeline_error
3765
+ if table_note is not None:
3766
+ data = _with_recurrence_note(data, table_note)
3658
3767
  return data, _meta(invocation, auth.workspace_id, project_id)
3659
3768
 
3660
3769
 
@@ -3721,6 +3830,82 @@ def _existing_internal_dataset_export(
3721
3830
  return max(matches, key=lambda export: export.id or 0) if matches else None
3722
3831
 
3723
3832
 
3833
+ def _hidden_column_names(
3834
+ service: Any, dataset_id: int, dataview_id: int, project_id: int | None
3835
+ ) -> list[str]:
3836
+ """Display names of the view's hidden columns, which a new-dataset export leaves out."""
3837
+ info = apply_column_renames(
3838
+ service.call(
3839
+ _DATAVIEW_GET_SYMBOL,
3840
+ dataset_id=dataset_id,
3841
+ dataview_id=dataview_id,
3842
+ project_id=project_id,
3843
+ )
3844
+ )
3845
+ if not isinstance(info, dict):
3846
+ return []
3847
+ display = info.get("display_properties")
3848
+ hidden = display.get("HIDDEN_COLUMNS") if isinstance(display, dict) else None
3849
+ if not isinstance(hidden, list) or not hidden:
3850
+ return []
3851
+ names = {
3852
+ column.get(_INTERNAL_NAME_KEY): column.get(_DISPLAY_NAME_KEY) or column.get("name")
3853
+ for column in info.get(_METADATA_KEY) or []
3854
+ if isinstance(column, dict)
3855
+ }
3856
+ return [str(names.get(column) or column) for column in hidden]
3857
+
3858
+
3859
+ def reject_dataset_export_conflicts(
3860
+ service: Any, dataview_id: int, dataset_id: int, target_ds_id: int | None
3861
+ ) -> None:
3862
+ """Refuse a dataset export (or crosstab) that cannot write, or would double-write.
3863
+
3864
+ Not into the view's own dataset, not from a view holding its changes in a
3865
+ draft, and not into a target this view already exports into. Each dataset
3866
+ export creates its own PERSISTENT trigger on the view, which re-runs on
3867
+ every pipeline run; REPLACE_IN_DS/APPEND_TO_DS only touch that trigger's OWN
3868
+ rows, so a second export into the same target is a second writer, not a
3869
+ refresh -- the fix is to rerun the pipeline, not to export again.
3870
+ """
3871
+ if target_ds_id is not None and target_ds_id == dataset_id:
3872
+ raise CliError(
3873
+ code=CODE_INVALID_ARGUMENTS,
3874
+ message="target_ds_id equals the view's own dataset.",
3875
+ exit_status=EXIT_USAGE,
3876
+ hint=(
3877
+ "A view cannot write into its own dataset. Export to a new "
3878
+ "dataset name (omit target_ds_id), or target a different dataset."
3879
+ ),
3880
+ details={"dataset_id": dataset_id, "target_ds_id": target_ds_id},
3881
+ )
3882
+ _reject_export_into_draft(service, dataview_id, dataset_id)
3883
+ if target_ds_id is None:
3884
+ return
3885
+ existing_export = _existing_internal_dataset_export(
3886
+ service, dataview_id, dataset_id, target_ds_id
3887
+ )
3888
+ if existing_export is not None:
3889
+ raise CliError(
3890
+ code="export_already_exists",
3891
+ message=(
3892
+ f"View {dataview_id} already exports into dataset "
3893
+ f"{target_ds_id} (export {existing_export.id}). This export "
3894
+ "re-runs on every pipeline run, and REPLACE_IN_DS replaces only "
3895
+ "its own rows -- a second export into the same target duplicates "
3896
+ "rows instead of refreshing them."
3897
+ ),
3898
+ exit_status=EXIT_USAGE,
3899
+ hint="Refresh the target by rerunning the pipeline, not by exporting again.",
3900
+ recovery_commands=[f"mammoth view pipeline rerun {dataview_id}"],
3901
+ details={
3902
+ "dataview_id": dataview_id,
3903
+ "target_ds_id": target_ds_id,
3904
+ "export_id": existing_export.id,
3905
+ },
3906
+ )
3907
+
3908
+
3724
3909
  _DATASET_GET_SYMBOL = "mammoth.api.datasets.DatasetsAPI.get"
3725
3910
 
3726
3911
 
@@ -3874,7 +4059,8 @@ def _reject_append_schema_mismatch(
3874
4059
  target_ds_id: int,
3875
4060
  column_mapping: Any,
3876
4061
  ) -> tuple[list[str], list[str]]:
3877
- """Refuse an ``APPEND_TO_DS`` export whose source has a column the target
4062
+ """Refuse an ``APPEND_TO_DS``/``REPLACE_IN_DS`` export into an existing target
4063
+ whose source has a column the target
3878
4064
  dataset's schema does not, and ``column_mapping`` does not cover.
3879
4065
 
3880
4066
  A non-empty ``column_mapping`` is stricter: the backend then copies only
@@ -36,6 +36,7 @@ from mammoth_cli.commands.view import (
36
36
  join_after_snapshot,
37
37
  join_dry_run_preview,
38
38
  join_snapshot,
39
+ reject_dataset_export_conflicts,
39
40
  wait_for_followon_job,
40
41
  wait_for_pipeline_to_settle,
41
42
  wait_for_view_row_count,
@@ -230,6 +231,32 @@ def _resolve_exact_dataset_id(
230
231
  return value
231
232
 
232
233
 
234
+ def _require_one_destination(document: dict[str, Any], command_id: str) -> None:
235
+ """Refuse a transform that names both ``new_column`` and ``existing_column``, or neither.
236
+
237
+ The SDK silently keeps only ``new_column`` when both are given, and a step
238
+ with neither is saved and then fails in the worker ("No destination for
239
+ operation") after the pipeline has already been changed.
240
+ """
241
+ given = [name for name in ("new_column", "existing_column") if document.get(name) is not None]
242
+ if len(given) == 1:
243
+ return
244
+ command = command_id.replace(".", " ")
245
+ raise CliError(
246
+ code=CODE_INVALID_ARGUMENTS,
247
+ message=(
248
+ f"'{command}' needs exactly one of new_column or existing_column, "
249
+ + ("not both: existing_column would be ignored." if given else "but got neither.")
250
+ ),
251
+ exit_status=EXIT_USAGE,
252
+ hint=(
253
+ "Set new_column to write a new column, or existing_column to overwrite "
254
+ "one that is already in the view."
255
+ ),
256
+ details={"given": given},
257
+ )
258
+
259
+
233
260
  def _require_field(document: dict[str, Any] | None, field: str) -> Any:
234
261
  """Return a required field from the ``--input`` document, or raise usage."""
235
262
  if document is None or field not in document:
@@ -979,6 +1006,7 @@ def view_transform_combine_columns(invocation: Invocation) -> HandlerResult:
979
1006
  document = invocation.load_input()
980
1007
  _require_field(document, "sources")
981
1008
  assert document is not None
1009
+ _require_one_destination(document, invocation.command_id)
982
1010
  kwargs = _bind_transform_inputs(invocation, document)
983
1011
  return _dispatch_view(invocation, view_id, "combine_columns", **kwargs)
984
1012
 
@@ -1063,7 +1091,27 @@ def view_transform_crosstab(invocation: Invocation) -> HandlerResult:
1063
1091
  _require_field(document, "dataset_name")
1064
1092
  assert document is not None
1065
1093
  kwargs = _bind_transform_inputs(invocation, document)
1066
- return _dispatch_view(invocation, view_id, "crosstab", **kwargs)
1094
+
1095
+ def guard_target(service: Any, dataset_id: int, call_kwargs: dict[str, Any]) -> None:
1096
+ target = call_kwargs.get("target_ds_id")
1097
+ reject_dataset_export_conflicts(
1098
+ service, view_id, dataset_id, int(target) if target is not None else None
1099
+ )
1100
+
1101
+ def name_persistence(_service: Any, _dataset_id: int, _state: Any, data: Any) -> Any:
1102
+ return {
1103
+ "dataset_id": data,
1104
+ "source_view_id": view_id,
1105
+ "refreshes_on_pipeline_run": True,
1106
+ "note": (
1107
+ f"This crosstab is a standing export on view {view_id}: it rewrites dataset "
1108
+ f"{data} on every pipeline run of that view, not just now."
1109
+ ),
1110
+ }
1111
+
1112
+ return _dispatch_view(
1113
+ invocation, view_id, "crosstab", prepare=guard_target, after=name_persistence, **kwargs
1114
+ )
1067
1115
 
1068
1116
 
1069
1117
  def view_transform_date_diff(invocation: Invocation) -> HandlerResult:
@@ -1325,6 +1373,7 @@ def view_transform_lookup(invocation: Invocation) -> HandlerResult:
1325
1373
  _require_field(document, "key")
1326
1374
  _require_field(document, "value")
1327
1375
  assert document is not None
1376
+ _require_one_destination(document, invocation.command_id)
1328
1377
  kwargs = _bind_transform_inputs(invocation, document)
1329
1378
  return _dispatch_view(invocation, view_id, "lookup", **kwargs)
1330
1379
 
@@ -1335,6 +1384,7 @@ def view_transform_math(invocation: Invocation) -> HandlerResult:
1335
1384
  document = invocation.load_input()
1336
1385
  _require_field(document, "expression")
1337
1386
  assert document is not None
1387
+ _require_one_destination(document, invocation.command_id)
1338
1388
  kwargs = _bind_transform_inputs(invocation, document)
1339
1389
  return _dispatch_view(invocation, view_id, "math", **kwargs)
1340
1390
 
@@ -1369,6 +1419,7 @@ def view_transform_set_values(invocation: Invocation) -> HandlerResult:
1369
1419
  document = invocation.load_input()
1370
1420
  _require_field(document, "values")
1371
1421
  assert document is not None
1422
+ _require_one_destination(document, invocation.command_id)
1372
1423
  kwargs = _bind_transform_inputs(invocation, document)
1373
1424
  return _dispatch_view(invocation, view_id, "set_values", **kwargs)
1374
1425
 
@@ -1432,5 +1483,6 @@ def view_transform_window(invocation: Invocation) -> HandlerResult:
1432
1483
  document = invocation.load_input()
1433
1484
  _require_field(document, "function")
1434
1485
  assert document is not None
1486
+ _require_one_destination(document, invocation.command_id)
1435
1487
  kwargs = _bind_transform_inputs(invocation, document)
1436
1488
  return _dispatch_view(invocation, view_id, "window", **kwargs)