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