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.
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/PKG-INFO +1 -1
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/__init__.py +1 -1
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/about-mammoth.md +7 -2
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/exports.md +16 -2
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/batch.py +26 -4
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/view.py +243 -57
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/view_ops.py +53 -1
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/errors/envelope.py +7 -0
- mammoth_cli-2.2.32/mammoth_cli/services/append_blank_columns.py +57 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/command_contract.py +17 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/pyproject.toml +1 -1
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/batch.yaml +1 -1
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/view.yaml +18 -13
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/.gitignore +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/LICENSE +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/README.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/__main__.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/app.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/SKILL.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/auth.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/capabilities-misc.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/capabilities.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/command-index.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/activity.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/addon.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/agent.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/ai.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/annotation.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/auth.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/automation.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/batch.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/billing.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/browse.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/calc.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/capability.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/client-app.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/completion.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/config.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/connector.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/context.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard-qa.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard-style.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard-template.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/data-app.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dataset.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/doctor.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/external-key.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/file.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/folder.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/job.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/link.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/log.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/notification.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/parameter.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/project.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/report.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/schedule.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/schema.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/skill.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/snippet.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/support.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/template.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/trash.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/upgrade.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/user.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/version.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/view-transform.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/view.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/webhook.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/workflow.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/workspace.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/handoff.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/input.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/jobs-drafts.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/machine-output.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/operations.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/auth-scope.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/cleanup.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/dashboards.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/database-import.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/end-to-end.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/index.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/need-action.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/resources.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/scheduling.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/transforms.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/typed-etl-discovery.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/recovery.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/report-checklist.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/retention.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/safety.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/bundled_skill/mammoth-cli/references/task-start.md +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/__init__.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/activity.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/addon.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/agent.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/ai.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/annotation.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/auth.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/automation.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/billing.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/browse.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/calc.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/capability.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/client_app.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/completion.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/config.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/connector.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/context.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/dashboard.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/data_app.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/dataset.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/doctor.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/external_key.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/file.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/folder.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/job.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/link.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/notification.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/parameter.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/project.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/registry.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/report.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/schedule.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/schema.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/skill.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/snippet.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/support.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/template.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/trash.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/upgrade.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/user.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/view_profile.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/webhook.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/workflow.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/commands/workspace.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/context/__init__.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/context/credentials.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/context/endpoint.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/context/profiles.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/context/resolver.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/contracts/__init__.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/contracts/auth.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/embed.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/errors/__init__.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/manifest/__init__.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/manifest/loader.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/messages/__init__.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/output/__init__.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/output/envelope.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/output/normalize.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/output/policy.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/output/progress.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/output/render.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/py.typed +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/__init__.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/confirm.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/dataset_health.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/dryrun.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/dryrun_targets.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/embedded.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/executor.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/input_loader.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/intent_only.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/invocation.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/new_data.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/options.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/parents.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/runlog.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/session.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/state.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/strict.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/updates.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/validate.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/runtime/verify.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/__init__.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/argspec.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/board_values.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/coerce.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/conditional_format.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/conditions.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/dashboard_filters.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/dashboard_pages.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/dashboard_review.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/data_profile.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/data_quality.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/dispatch.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/factory.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/input_fields.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/listing.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/local_time.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/mapping.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/openapi_types.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/positionals.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/protocol.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/read_queries.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/sdk_service.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/sql_check.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/stored_stats.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/testing.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/text_dates.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/type_system.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/services/write_impact.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/skills/__init__.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/skills/installer.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/skills/steering.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/mammoth_cli/testing.py +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/activity.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/addon.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/agent.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/ai.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/annotation.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/auth.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/automation.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/billing.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/browse.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/calc.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/capability.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/client-app.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/completion.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/config.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/connector.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/context.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/dashboard.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/data-app.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/dataset.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/doctor.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/external-key.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/file.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/folder.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/job.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/link.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/log.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/notification.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/parameter.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/project.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/report.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/schedule.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/schema.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/skill.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/snippet.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/support.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/template.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/trash.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/upgrade.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/user.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/version.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/webhook.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/workflow.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/commands/workspace.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/edits-target.source.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/openapi-operations.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/schema-v1.json +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/sdk-catalog.source.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/manifests/sdk-methods.yaml +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/openapi/metadata.json +0 -0
- {mammoth_cli-2.2.30 → mammoth_cli-2.2.32}/spec/openapi/openapi.json +0 -0
|
@@ -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).
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
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
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
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
|
|
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
|
-
|
|
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)
|