mammoth-cli 2.0.40__tar.gz → 2.0.41__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.40 → mammoth_cli-2.0.41}/PKG-INFO +1 -1
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/__init__.py +1 -1
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/SKILL.md +14 -9
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/dashboards.md +9 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/dashboard.py +69 -1
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/file.py +15 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/view.py +106 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/view_ops.py +11 -3
- mammoth_cli-2.0.41/mammoth_cli/services/dashboard_review.py +284 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/pyproject.toml +1 -1
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/.gitignore +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/LICENSE +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/README.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/__main__.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/app.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/about-mammoth.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/auth.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/capabilities-misc.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/capabilities.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/command-index.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/activity.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/addon.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/agent.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/ai.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/annotation.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/auth.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/automation.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/batch.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/billing.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/browse.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/capability.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/client-app.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/completion.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/config.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/connector.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/context.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dashboard.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/data-app.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/dataset.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/doctor.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/external-key.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/file.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/folder.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/job.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/log.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/notification.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/parameter.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/project.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/report.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/schedule.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/schema.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/skill.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/snippet.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/support.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/template.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/trash.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/upgrade.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/user.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/version.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/view.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/webhook.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/workflow.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/commands/workspace.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/handoff.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/input.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/jobs-drafts.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/machine-output.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/operations.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/auth-scope.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/cleanup.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/end-to-end.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/exports.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/index.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/need-action.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/resources.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/transforms.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/recipes/typed-etl-discovery.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/recovery.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/report-checklist.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/retention.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/safety.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/bundled_skill/mammoth-cli/references/task-start.md +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/__init__.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/activity.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/addon.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/agent.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/ai.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/annotation.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/auth.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/automation.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/batch.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/billing.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/browse.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/capability.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/client_app.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/completion.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/config.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/connector.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/context.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/data_app.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/dataset.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/doctor.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/external_key.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/folder.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/job.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/notification.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/parameter.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/project.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/registry.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/report.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/schedule.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/schema.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/skill.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/snippet.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/support.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/template.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/trash.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/upgrade.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/user.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/webhook.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/workflow.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/commands/workspace.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/context/__init__.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/context/credentials.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/context/endpoint.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/context/profiles.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/context/resolver.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/contracts/__init__.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/contracts/auth.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/errors/__init__.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/errors/envelope.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/manifest/__init__.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/manifest/loader.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/messages/__init__.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/output/__init__.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/output/envelope.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/output/normalize.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/output/policy.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/output/progress.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/output/render.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/py.typed +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/__init__.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/confirm.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/dryrun.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/executor.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/input_loader.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/invocation.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/options.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/parents.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/runlog.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/session.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/strict.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/updates.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/runtime/validate.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/__init__.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/argspec.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/coerce.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/command_contract.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/conditions.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/dashboard_pages.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/data_quality.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/dispatch.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/factory.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/input_fields.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/mapping.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/openapi_types.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/positionals.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/protocol.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/sdk_service.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/testing.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/services/type_system.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/skills/__init__.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/skills/installer.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/skills/steering.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/mammoth_cli/testing.py +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/activity.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/addon.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/agent.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/ai.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/annotation.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/auth.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/automation.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/batch.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/billing.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/browse.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/capability.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/client-app.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/completion.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/config.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/connector.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/context.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/dashboard.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/data-app.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/dataset.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/doctor.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/external-key.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/file.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/folder.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/job.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/log.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/notification.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/parameter.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/project.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/report.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/schedule.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/schema.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/skill.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/snippet.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/support.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/template.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/trash.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/upgrade.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/user.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/version.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/view.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/webhook.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/workflow.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/commands/workspace.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/openapi-operations.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/schema-v1.json +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/sdk-catalog.source.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/manifests/sdk-methods.yaml +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/spec/openapi/metadata.json +0 -0
- {mammoth_cli-2.0.40 → mammoth_cli-2.0.41}/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.41
|
|
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,10 +93,11 @@ 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. Upload every file
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
1. Upload every file. Each upload result carries a `view` preview of what
|
|
97
|
+
Mammoth made of the file (`view_id`, column types, sample rows,
|
|
98
|
+
`column_warnings`, and `before_dashboard`). Read every view in Mammoth, not
|
|
99
|
+
the local files: `view data get VIEW_ID` for each one. The warnings list
|
|
100
|
+
numbers or dates stored as text, and blanks, each with the fix.
|
|
100
101
|
2. Find the keys. A column in one view whose values appear in a column of
|
|
101
102
|
the other (`customer_id` and `id`, `order_ref` and `order_no`) is a
|
|
102
103
|
foreign key; the view with many rows per key is the main one.
|
|
@@ -112,10 +113,14 @@ dashboard from t_a and t_b"). Work it out from the data before you build:
|
|
|
112
113
|
result's `join_check` gives `match_rate`, `unmatched_rows` and
|
|
113
114
|
`unmatched_keys`; put them in your report, and stop to compare the keys
|
|
114
115
|
if more than a few rows found no match.
|
|
115
|
-
6.
|
|
116
|
-
(
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
6. If the data has money, add it before you make the dashboard: `revenue`
|
|
117
|
+
(`math`, `qty * price`, `new_column`), the `fix` in `before_dashboard`.
|
|
118
|
+
A dashboard sees only the columns the view had when it was made.
|
|
119
|
+
7. Build the dashboard from the joined view
|
|
120
|
+
([dashboards](references/recipes/dashboards.md)) and chart the sum of
|
|
121
|
+
revenue. Do not sum a unit price. `create-blank`, `canvas save` and
|
|
122
|
+
`pages add` return `deliverable_check`: fix each warning, or say in the
|
|
123
|
+
report why not.
|
|
119
124
|
|
|
120
125
|
For the defaults that most pipelines use (new column or overwrite, `LEFT`
|
|
121
126
|
joins, date steps), see
|
|
@@ -88,10 +88,19 @@ mammoth view transform math VIEW_ID --project PROJECT_ID \
|
|
|
88
88
|
--input '{"dataset_id":DATASET_ID,"expression":"qty * price","new_column":"revenue"}'
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
- Add every column the board needs before `create-blank`. A dashboard sees
|
|
92
|
+
only the columns the view had when the dashboard was made: `pages add`
|
|
93
|
+
refuses a later column ("isn't a measure in this data"). If you added it
|
|
94
|
+
after, make a new dashboard and delete the old one if you made it for this
|
|
95
|
+
task. The upload result's `before_dashboard` names the column to add.
|
|
91
96
|
- Put revenue in a KPI (`focus.kpis`, `agg` `sum`, `unit.prefix` for the
|
|
92
97
|
currency) and in one or more charts (revenue by product, by segment, by
|
|
93
98
|
month). Rows with an empty price give an empty revenue. Say how many there
|
|
94
99
|
are in your report.
|
|
100
|
+
- `create-blank`, `canvas save` and `pages add` return `deliverable_check`:
|
|
101
|
+
`money_not_shown` (with the `math` command in `fix`), `unit_price_summed`,
|
|
102
|
+
and one `blank_values` entry for each column on the board that has blanks.
|
|
103
|
+
Fix each warning, or say in your report why you kept it.
|
|
95
104
|
|
|
96
105
|
**Read the bindings back, then the numbers.** `dashboard canvas get` returns
|
|
97
106
|
`data.meta.figures` — `"p1:kpi:2": {"descriptors": {"value": "<id>"}}` per
|
|
@@ -16,6 +16,8 @@ from typing import Any
|
|
|
16
16
|
from mammoth.models.dashboards import AddPagesSpec
|
|
17
17
|
from pydantic import ValidationError
|
|
18
18
|
|
|
19
|
+
from mammoth_cli.commands.view import view_profiles
|
|
20
|
+
from mammoth_cli.context import profiles
|
|
19
21
|
from mammoth_cli.errors.envelope import (
|
|
20
22
|
CODE_INVALID_ARGUMENT,
|
|
21
23
|
CODE_MISSING_ARGUMENT,
|
|
@@ -25,6 +27,7 @@ from mammoth_cli.errors.envelope import (
|
|
|
25
27
|
CliError,
|
|
26
28
|
)
|
|
27
29
|
from mammoth_cli.manifest.loader import command_by_id
|
|
30
|
+
from mammoth_cli.runtime import parents
|
|
28
31
|
from mammoth_cli.runtime.confirm import (
|
|
29
32
|
POLICY_CONFIRM_TARGET,
|
|
30
33
|
POLICY_NONE,
|
|
@@ -33,10 +36,16 @@ from mammoth_cli.runtime.confirm import (
|
|
|
33
36
|
enforce_confirmation,
|
|
34
37
|
)
|
|
35
38
|
from mammoth_cli.runtime.invocation import Invocation
|
|
36
|
-
from mammoth_cli.runtime.session import open_service, require_project
|
|
39
|
+
from mammoth_cli.runtime.session import open_service, require_project, resolved_project
|
|
37
40
|
from mammoth_cli.services.argspec import arg_spec
|
|
38
41
|
from mammoth_cli.services.command_contract import bind_command_inputs
|
|
39
42
|
from mammoth_cli.services.dashboard_pages import check_added_pages
|
|
43
|
+
from mammoth_cli.services.dashboard_review import (
|
|
44
|
+
CANVAS_GET,
|
|
45
|
+
NEW_COLUMN_NOTE,
|
|
46
|
+
has_profiles,
|
|
47
|
+
review,
|
|
48
|
+
)
|
|
40
49
|
from mammoth_cli.services.positionals import resolve_positionals
|
|
41
50
|
|
|
42
51
|
HandlerResult = tuple[Any, dict[str, Any]]
|
|
@@ -675,9 +684,68 @@ def generated_dashboard(invocation: Invocation) -> HandlerResult:
|
|
|
675
684
|
if invocation.command_id == "dashboard.pages.add" and "body" in kwargs:
|
|
676
685
|
requested = kwargs["body"].get("params", {}).get("pages", [])
|
|
677
686
|
data = check_added_pages(service, positionals["dashboard_id"], requested, data)
|
|
687
|
+
if invocation.command_id in _REVIEWED_COMMANDS:
|
|
688
|
+
data = _with_deliverable_check(invocation, service, auth, positionals, data)
|
|
678
689
|
return data, _meta(invocation, auth.workspace_id)
|
|
679
690
|
|
|
680
691
|
|
|
692
|
+
#: Authoring steps whose result carries ``deliverable_check``.
|
|
693
|
+
_REVIEWED_COMMANDS = frozenset(
|
|
694
|
+
{"dashboard.create-blank", "dashboard.canvas.save", "dashboard.pages.add"}
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _with_deliverable_check(
|
|
699
|
+
invocation: Invocation, service: Any, auth: Any, positionals: dict[str, Any], data: Any
|
|
700
|
+
) -> Any:
|
|
701
|
+
"""Add ``deliverable_check`` (money shown? blanks decided?) to an authoring result.
|
|
702
|
+
|
|
703
|
+
One canvas read; advice only, so a failed read leaves the result as it was.
|
|
704
|
+
"""
|
|
705
|
+
if hasattr(data, "model_dump"):
|
|
706
|
+
data = data.model_dump(mode="json")
|
|
707
|
+
if not isinstance(data, dict):
|
|
708
|
+
return data
|
|
709
|
+
dashboard_id = positionals.get("dashboard_id") or data.get("id")
|
|
710
|
+
if not isinstance(dashboard_id, int):
|
|
711
|
+
return data
|
|
712
|
+
try:
|
|
713
|
+
canvas_doc = service.call(CANVAS_GET, dashboard_id=dashboard_id)
|
|
714
|
+
if hasattr(canvas_doc, "model_dump"):
|
|
715
|
+
canvas_doc = canvas_doc.model_dump(mode="json")
|
|
716
|
+
source = (canvas_doc.get("canvas") or {}).get("dataset") or {}
|
|
717
|
+
view_id = source.get("dataview_id")
|
|
718
|
+
profile = invocation.profile or profiles.get_selected()
|
|
719
|
+
dataset_id = (
|
|
720
|
+
parents.lookup(profile, auth.workspace_id, view_id)
|
|
721
|
+
if isinstance(view_id, int)
|
|
722
|
+
else None
|
|
723
|
+
)
|
|
724
|
+
fallback = None
|
|
725
|
+
if not has_profiles(canvas_doc) and isinstance(view_id, int) and dataset_id is not None:
|
|
726
|
+
fallback = view_profiles(service, dataset_id, view_id, resolved_project(invocation))
|
|
727
|
+
warnings = review(canvas_doc, dataset_id, fallback)
|
|
728
|
+
except Exception: # noqa: BLE001 -- advice must never fail the authoring step
|
|
729
|
+
return data
|
|
730
|
+
if warnings:
|
|
731
|
+
data = {
|
|
732
|
+
**data,
|
|
733
|
+
"deliverable_check": {
|
|
734
|
+
"warnings": warnings,
|
|
735
|
+
"note": (
|
|
736
|
+
"Fix these before you report the dashboard as done, or say in your "
|
|
737
|
+
"report why not."
|
|
738
|
+
+ (
|
|
739
|
+
" " + NEW_COLUMN_NOTE
|
|
740
|
+
if any("fix" in warning for warning in warnings)
|
|
741
|
+
else ""
|
|
742
|
+
)
|
|
743
|
+
),
|
|
744
|
+
},
|
|
745
|
+
}
|
|
746
|
+
return data
|
|
747
|
+
|
|
748
|
+
|
|
681
749
|
def dashboard_assess_twb(invocation: Invocation) -> HandlerResult:
|
|
682
750
|
"""Assess a local Tableau workbook through the SDK multipart seam."""
|
|
683
751
|
file_path = _require_str_positional(invocation, "file")
|
|
@@ -15,6 +15,8 @@ from collections.abc import Callable
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Any
|
|
17
17
|
|
|
18
|
+
from mammoth_cli.commands.view import upload_preview
|
|
19
|
+
from mammoth_cli.context import profiles
|
|
18
20
|
from mammoth_cli.errors.envelope import (
|
|
19
21
|
CODE_INVALID_ARGUMENT,
|
|
20
22
|
CODE_MISSING_ARGUMENT,
|
|
@@ -24,6 +26,7 @@ from mammoth_cli.errors.envelope import (
|
|
|
24
26
|
CliError,
|
|
25
27
|
)
|
|
26
28
|
from mammoth_cli.manifest.loader import command_by_id
|
|
29
|
+
from mammoth_cli.runtime import parents
|
|
27
30
|
from mammoth_cli.runtime.confirm import (
|
|
28
31
|
POLICY_CONFIRM_TARGET,
|
|
29
32
|
POLICY_PROMPT_OR_YES,
|
|
@@ -226,6 +229,18 @@ def file_upload(invocation: Invocation) -> HandlerResult:
|
|
|
226
229
|
# ambiguous date column lands in ``need_action`` with no view. Report
|
|
227
230
|
# the status the platform holds for each dataset, not an assumed one.
|
|
228
231
|
result = _upload_result(data, lambda dataset_id: _dataset_status(service, dataset_id))
|
|
232
|
+
project_id = resolved_project(invocation)
|
|
233
|
+
for entry in result["datasets"]:
|
|
234
|
+
if entry.get("status") != "ready":
|
|
235
|
+
continue
|
|
236
|
+
preview = upload_preview(service, int(entry["id"]), project_id)
|
|
237
|
+
if preview is not None:
|
|
238
|
+
entry["view"] = preview
|
|
239
|
+
parents.remember(
|
|
240
|
+
invocation.profile or profiles.get_selected(),
|
|
241
|
+
auth.workspace_id,
|
|
242
|
+
{preview["view_id"]: int(entry["id"])},
|
|
243
|
+
)
|
|
229
244
|
return result, _meta(invocation, auth.workspace_id, resolved_project(invocation))
|
|
230
245
|
|
|
231
246
|
|
|
@@ -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.dashboard_review import UPLOAD_NOTE, upload_hints
|
|
49
50
|
from mammoth_cli.services.data_quality import column_warnings
|
|
50
51
|
|
|
51
52
|
HandlerResult = tuple[Any, dict[str, Any]]
|
|
@@ -2065,3 +2066,108 @@ def view_export_specialized(invocation: Invocation) -> HandlerResult:
|
|
|
2065
2066
|
+ (f" --project {int(target_project)}" if target_project is not None else ""),
|
|
2066
2067
|
}
|
|
2067
2068
|
return data, _meta(invocation, auth.workspace_id, project_id)
|
|
2069
|
+
|
|
2070
|
+
|
|
2071
|
+
_DATAVIEW_LIST_SYMBOL = "mammoth.api.dataviews.DataviewsAPI.list"
|
|
2072
|
+
#: Rows an upload shows of each new view (enough to see keys and formats).
|
|
2073
|
+
_UPLOAD_SAMPLE_ROWS = 3
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
def upload_preview(service: Any, dataset_id: int, project_id: int | None) -> dict[str, Any] | None:
|
|
2077
|
+
"""The first view of a new dataset: columns, types, sample rows, warnings.
|
|
2078
|
+
|
|
2079
|
+
``before_dashboard`` names the columns to add before any dashboard is
|
|
2080
|
+
made (revenue from a unit price and a quantity), since a dashboard does
|
|
2081
|
+
not see columns added after it.
|
|
2082
|
+
|
|
2083
|
+
An agent that has just uploaded files tends to read the local copies
|
|
2084
|
+
instead of the data in Mammoth, and so misses what Mammoth made of them
|
|
2085
|
+
(a price read as TEXT, blanks). The upload result carries the answer.
|
|
2086
|
+
Best effort: any failure returns ``None`` and the upload result is
|
|
2087
|
+
unchanged.
|
|
2088
|
+
"""
|
|
2089
|
+
try:
|
|
2090
|
+
listing = service.call(_DATAVIEW_LIST_SYMBOL, dataset_id=dataset_id, project_id=project_id)
|
|
2091
|
+
views = listing.get("dataviews") if isinstance(listing, dict) else None
|
|
2092
|
+
if not isinstance(views, list) or not views or not isinstance(views[0], dict):
|
|
2093
|
+
return None
|
|
2094
|
+
record = apply_column_renames(views[0])
|
|
2095
|
+
view_id = record.get("id")
|
|
2096
|
+
if not isinstance(view_id, int):
|
|
2097
|
+
return None
|
|
2098
|
+
mapping: dict[str, str] = {}
|
|
2099
|
+
types: dict[str, str] = {}
|
|
2100
|
+
for column in record.get(_METADATA_KEY) or []:
|
|
2101
|
+
if not isinstance(column, dict):
|
|
2102
|
+
continue
|
|
2103
|
+
internal = column.get(_INTERNAL_NAME_KEY)
|
|
2104
|
+
display = column.get(_DISPLAY_NAME_KEY)
|
|
2105
|
+
if isinstance(internal, str) and isinstance(display, str):
|
|
2106
|
+
mapping[internal] = display
|
|
2107
|
+
types[display] = str(column.get("type") or "")
|
|
2108
|
+
page = service.call(
|
|
2109
|
+
_DATA_PAGE_SYMBOL, dataset_id=dataset_id, dataview_id=view_id, project_id=project_id
|
|
2110
|
+
)
|
|
2111
|
+
page = _relabel_columns(service, dataset_id, view_id, project_id, page, mapping)
|
|
2112
|
+
rows = page.get(_ROWS_KEY) if isinstance(page, dict) else None
|
|
2113
|
+
rows = [row for row in rows or [] if isinstance(row, dict)]
|
|
2114
|
+
except Exception: # noqa: BLE001 -- a preview must never fail the upload
|
|
2115
|
+
return None
|
|
2116
|
+
preview: dict[str, Any] = {
|
|
2117
|
+
"view_id": view_id,
|
|
2118
|
+
"row_count": record.get("row_count"),
|
|
2119
|
+
"columns": types,
|
|
2120
|
+
"sample_rows": rows[:_UPLOAD_SAMPLE_ROWS],
|
|
2121
|
+
}
|
|
2122
|
+
try:
|
|
2123
|
+
warnings = column_warnings(rows, types, view_id) if rows else []
|
|
2124
|
+
except Exception: # noqa: BLE001
|
|
2125
|
+
warnings = []
|
|
2126
|
+
if warnings:
|
|
2127
|
+
preview["column_warnings"] = warnings
|
|
2128
|
+
try:
|
|
2129
|
+
text_numbers = [
|
|
2130
|
+
str(w.get("column"))
|
|
2131
|
+
for w in warnings
|
|
2132
|
+
if isinstance(w, dict) and w.get("issue") == "numbers_stored_as_text"
|
|
2133
|
+
]
|
|
2134
|
+
hints = upload_hints(
|
|
2135
|
+
view_id, record.get(_METADATA_KEY) or [], rows, dataset_id, text_numbers
|
|
2136
|
+
)
|
|
2137
|
+
except Exception: # noqa: BLE001
|
|
2138
|
+
hints = []
|
|
2139
|
+
if hints:
|
|
2140
|
+
preview["before_dashboard"] = {"warnings": hints, "note": UPLOAD_NOTE}
|
|
2141
|
+
return preview
|
|
2142
|
+
|
|
2143
|
+
|
|
2144
|
+
def view_profiles(
|
|
2145
|
+
service: Any, dataset_id: int, view_id: int, project_id: int | None
|
|
2146
|
+
) -> list[dict[str, Any]] | None:
|
|
2147
|
+
"""Column profile of a view (types and blank shares) for the dashboard check."""
|
|
2148
|
+
from mammoth_cli.services.dashboard_review import profiles_from_view
|
|
2149
|
+
|
|
2150
|
+
try:
|
|
2151
|
+
info = apply_column_renames(
|
|
2152
|
+
service.call(
|
|
2153
|
+
_DATAVIEW_GET_SYMBOL,
|
|
2154
|
+
dataset_id=dataset_id,
|
|
2155
|
+
dataview_id=view_id,
|
|
2156
|
+
project_id=project_id,
|
|
2157
|
+
)
|
|
2158
|
+
)
|
|
2159
|
+
metadata = [c for c in (info or {}).get(_METADATA_KEY) or [] if isinstance(c, dict)]
|
|
2160
|
+
mapping = {
|
|
2161
|
+
c[_INTERNAL_NAME_KEY]: c[_DISPLAY_NAME_KEY]
|
|
2162
|
+
for c in metadata
|
|
2163
|
+
if isinstance(c.get(_INTERNAL_NAME_KEY), str)
|
|
2164
|
+
and isinstance(c.get(_DISPLAY_NAME_KEY), str)
|
|
2165
|
+
}
|
|
2166
|
+
page = service.call(
|
|
2167
|
+
_DATA_PAGE_SYMBOL, dataset_id=dataset_id, dataview_id=view_id, project_id=project_id
|
|
2168
|
+
)
|
|
2169
|
+
page = _relabel_columns(service, dataset_id, view_id, project_id, page, mapping)
|
|
2170
|
+
rows = page.get(_ROWS_KEY) if isinstance(page, dict) else None
|
|
2171
|
+
return profiles_from_view(metadata, rows or [])
|
|
2172
|
+
except Exception: # noqa: BLE001 -- advice only
|
|
2173
|
+
return None
|
|
@@ -290,6 +290,11 @@ def _dispatch_view(
|
|
|
290
290
|
reject_pipeline_reference_errors(service, view_id, dataset_id, response)
|
|
291
291
|
raise
|
|
292
292
|
reject_pipeline_reference_errors(service, view_id, dataset_id, data)
|
|
293
|
+
if dataset_id is not None:
|
|
294
|
+
# Later commands that know only the view (a dashboard's check) use it.
|
|
295
|
+
parents.remember(
|
|
296
|
+
_profile_name(invocation), auth.workspace_id, {view_id: int(dataset_id)}
|
|
297
|
+
)
|
|
293
298
|
if after is not None and dataset_id is not None:
|
|
294
299
|
data = after(service, int(dataset_id), state, data)
|
|
295
300
|
return data, _meta(invocation, auth.workspace_id)
|
|
@@ -513,9 +518,12 @@ def view_get(invocation: Invocation) -> HandlerResult:
|
|
|
513
518
|
"dataview_id": view_id,
|
|
514
519
|
# Server-side projection; the brief set (plus the display
|
|
515
520
|
# properties that carry column renames) unless the caller asks.
|
|
516
|
-
"fields":
|
|
517
|
-
|
|
518
|
-
|
|
521
|
+
"fields": (
|
|
522
|
+
document.get("fields")
|
|
523
|
+
or ",".join(
|
|
524
|
+
[key for key in BRIEF_VIEW_FIELDS if key != "dataset_id"]
|
|
525
|
+
+ ["display_properties"]
|
|
526
|
+
)
|
|
519
527
|
),
|
|
520
528
|
}
|
|
521
529
|
with open_service(invocation) as (service, auth):
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Check a dashboard against what its data holds, after each authoring step.
|
|
2
|
+
|
|
3
|
+
The skill tells an agent to put the money on the board and to decide on
|
|
4
|
+
blanks, but an agent that authors the canvas directly never reads that part
|
|
5
|
+
(cold-agent eval, CLI 2.0.40: counts only, blanks undecided). The check runs
|
|
6
|
+
where the agent already looks: the result of ``create-blank``, ``canvas
|
|
7
|
+
save`` and ``pages add``. It uses the column profile the dashboard backend
|
|
8
|
+
computes for the canvas (``plan.hints._profiles``), so it costs one read.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Iterable, Mapping
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
CANVAS_GET = "mammoth.api.dashboards.DashboardsAPI.canvas_get"
|
|
19
|
+
|
|
20
|
+
#: A dashboard reads the view as it was when the dashboard was made; a column
|
|
21
|
+
#: added later is not a measure there (release, CLI 2.0.41: pages add refused
|
|
22
|
+
#: ``revenue`` on a dashboard made before it, and ``swap-data`` did not always
|
|
23
|
+
#: pick it up). A dashboard made after the column sees it.
|
|
24
|
+
NEW_COLUMN_NOTE = (
|
|
25
|
+
"A dashboard sees only the columns the view had when the dashboard was made. "
|
|
26
|
+
"Run each fix first, then make a new dashboard (dashboard create-blank) and build "
|
|
27
|
+
"on that one; delete this one if you made it for this task."
|
|
28
|
+
)
|
|
29
|
+
#: The same fact, for the upload result, where no dashboard exists yet.
|
|
30
|
+
UPLOAD_NOTE = (
|
|
31
|
+
"Run these before you make a dashboard: a dashboard sees only the columns "
|
|
32
|
+
"the view had when the dashboard was made."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
_MONEY = re.compile(
|
|
36
|
+
r"price|amount|revenue|sales|cost|spend|fee|income|profit|margin|total|value|gmv|arr|mrr",
|
|
37
|
+
re.IGNORECASE,
|
|
38
|
+
)
|
|
39
|
+
_UNIT = re.compile(r"price|rate|unit|per[_ ]", re.IGNORECASE)
|
|
40
|
+
_QUANTITY = re.compile(r"^(qty|quantity|units?|count|volume|pieces|items?)\b", re.IGNORECASE)
|
|
41
|
+
_MEASURE_KEYS = ("measure", "measure2", "measure_x", "measure_y", "measure_size")
|
|
42
|
+
_LIST_MEASURE_KEYS = ("measures", "extra_measures")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _profiles(canvas_doc: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
46
|
+
plan = canvas_doc.get("plan")
|
|
47
|
+
hints = plan.get("hints") if isinstance(plan, Mapping) else None
|
|
48
|
+
profiles = hints.get("_profiles") if isinstance(hints, Mapping) else None
|
|
49
|
+
return [p for p in profiles or [] if isinstance(p, dict) and isinstance(p.get("name"), str)]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _charts(canvas: Mapping[str, Any]) -> Iterable[tuple[Mapping[str, Any], Mapping[str, Any]]]:
|
|
53
|
+
"""Yield ``(container, figure)`` for every authored figure and focus."""
|
|
54
|
+
containers = [canvas, *[p for p in canvas.get("pages") or [] if isinstance(p, Mapping)]]
|
|
55
|
+
for container in containers:
|
|
56
|
+
focus = container.get("focus")
|
|
57
|
+
if isinstance(focus, Mapping):
|
|
58
|
+
yield container, focus
|
|
59
|
+
for figure in container.get("added") or []:
|
|
60
|
+
if isinstance(figure, Mapping):
|
|
61
|
+
yield container, figure
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _used(canvas: Mapping[str, Any]) -> tuple[set[str], set[str], list[tuple[str, str]]]:
|
|
65
|
+
"""Measures and dimensions the board uses, and ``(measure, agg)`` pairs."""
|
|
66
|
+
derived = {
|
|
67
|
+
str(d.get("id")): [d.get("numerator"), d.get("denominator")]
|
|
68
|
+
for d in canvas.get("derived") or []
|
|
69
|
+
if isinstance(d, Mapping)
|
|
70
|
+
}
|
|
71
|
+
measures: set[str] = set()
|
|
72
|
+
dims: set[str] = set()
|
|
73
|
+
aggs: list[tuple[str, str]] = []
|
|
74
|
+
|
|
75
|
+
def add(name: Any, agg: Any) -> None:
|
|
76
|
+
if not isinstance(name, str):
|
|
77
|
+
return
|
|
78
|
+
for part in derived.get(name, [name]):
|
|
79
|
+
if isinstance(part, str):
|
|
80
|
+
measures.add(part)
|
|
81
|
+
aggs.append((part, str(agg or "sum").lower()))
|
|
82
|
+
|
|
83
|
+
for _, figure in _charts(canvas):
|
|
84
|
+
agg = figure.get("agg")
|
|
85
|
+
for key in _MEASURE_KEYS:
|
|
86
|
+
add(figure.get(key), agg)
|
|
87
|
+
for key in _LIST_MEASURE_KEYS:
|
|
88
|
+
for name in figure.get(key) or []:
|
|
89
|
+
add(name.get("field") if isinstance(name, Mapping) else name, agg)
|
|
90
|
+
for kpi in figure.get("kpis") or []:
|
|
91
|
+
if isinstance(kpi, Mapping):
|
|
92
|
+
add(kpi.get("field"), kpi.get("agg"))
|
|
93
|
+
for key in ("dim", "col_dim", "row_dim", "series", "date_field"):
|
|
94
|
+
if isinstance(figure.get(key), str):
|
|
95
|
+
dims.add(figure[key])
|
|
96
|
+
return measures, dims, aggs
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _math_fix(view_id: Any, dataset_id: Any, quantity: str, price: str) -> str:
|
|
100
|
+
spec = json.dumps(
|
|
101
|
+
{
|
|
102
|
+
"dataset_id": dataset_id if dataset_id is not None else "DATASET_ID",
|
|
103
|
+
"expression": f"{quantity} * {price}",
|
|
104
|
+
"new_column": "revenue",
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return f"mammoth view transform math {view_id} --input '{spec}'"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def profiles_from_view(
|
|
111
|
+
metadata: Iterable[Mapping[str, Any]], rows: Iterable[Mapping[str, Any]]
|
|
112
|
+
) -> list[dict[str, Any]]:
|
|
113
|
+
"""A stand-in for ``plan.hints._profiles`` from view metadata and a data page.
|
|
114
|
+
|
|
115
|
+
A blank canvas has no profile until its first bake, so ``create-blank``
|
|
116
|
+
builds one: NUMERIC columns are measures, and ``nullRate`` is the share of
|
|
117
|
+
blank values in the rows given (display-name keyed).
|
|
118
|
+
"""
|
|
119
|
+
materialised = [row for row in rows if isinstance(row, Mapping)]
|
|
120
|
+
profiles: list[dict[str, Any]] = []
|
|
121
|
+
for column in metadata:
|
|
122
|
+
name = column.get("display_name")
|
|
123
|
+
if not isinstance(name, str):
|
|
124
|
+
continue
|
|
125
|
+
kind = str(column.get("type") or "").upper()
|
|
126
|
+
profile: dict[str, Any] = {
|
|
127
|
+
"name": name,
|
|
128
|
+
"type": "measure" if kind == "NUMERIC" else "date" if kind == "DATE" else "dimension",
|
|
129
|
+
}
|
|
130
|
+
# Only rows that carry the column count; a column the page left out
|
|
131
|
+
# is unknown, not blank.
|
|
132
|
+
values = [row[name] for row in materialised if name in row]
|
|
133
|
+
blank = sum(1 for v in values if v is None or (isinstance(v, str) and not v.strip()))
|
|
134
|
+
if blank:
|
|
135
|
+
profile["nullRate"] = blank / len(values)
|
|
136
|
+
profiles.append(profile)
|
|
137
|
+
return profiles
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def review(
|
|
141
|
+
canvas_doc: Any,
|
|
142
|
+
dataset_id: int | None = None,
|
|
143
|
+
fallback_profiles: list[dict[str, Any]] | None = None,
|
|
144
|
+
) -> list[dict[str, Any]]:
|
|
145
|
+
"""Return warnings about money and blanks for a canvas read.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
canvas_doc: A ``dashboard canvas get`` payload (``canvas``, ``plan``).
|
|
149
|
+
dataset_id: The view's parent dataset, for the suggested command.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
``money_not_shown``, ``unit_price_summed`` and ``blank_values``
|
|
153
|
+
records (``issue``, ``detail``, optional ``fix``); empty when none.
|
|
154
|
+
"""
|
|
155
|
+
if not isinstance(canvas_doc, Mapping):
|
|
156
|
+
return []
|
|
157
|
+
canvas = canvas_doc.get("canvas")
|
|
158
|
+
if not isinstance(canvas, Mapping):
|
|
159
|
+
return []
|
|
160
|
+
profiles = _profiles(canvas_doc) or list(fallback_profiles or [])
|
|
161
|
+
if not profiles:
|
|
162
|
+
return []
|
|
163
|
+
source = canvas.get("dataset")
|
|
164
|
+
view_id = source.get("dataview_id") if isinstance(source, Mapping) else None
|
|
165
|
+
view_ref = view_id if view_id is not None else "VIEW_ID"
|
|
166
|
+
measures, dims, aggs = _used(canvas)
|
|
167
|
+
numeric = [p for p in profiles if p.get("type") == "measure"]
|
|
168
|
+
unit_prices = [
|
|
169
|
+
p["name"]
|
|
170
|
+
for p in numeric
|
|
171
|
+
if p.get("perUnit") is True or (_UNIT.search(p["name"]) and _MONEY.search(p["name"]))
|
|
172
|
+
]
|
|
173
|
+
money = [
|
|
174
|
+
p["name"] for p in numeric if _MONEY.search(p["name"]) and p["name"] not in unit_prices
|
|
175
|
+
]
|
|
176
|
+
quantities = [
|
|
177
|
+
p["name"] for p in numeric if p.get("semantic") == "count" or _QUANTITY.search(p["name"])
|
|
178
|
+
]
|
|
179
|
+
warnings: list[dict[str, Any]] = []
|
|
180
|
+
shows_money = any(name in measures for name in money + unit_prices)
|
|
181
|
+
if (money or unit_prices) and not shows_money:
|
|
182
|
+
if money:
|
|
183
|
+
detail = (
|
|
184
|
+
f"The view has money ({', '.join(money)}) but no chart or KPI shows it. "
|
|
185
|
+
f"Add a KPI and a chart with the sum of {money[0]}."
|
|
186
|
+
)
|
|
187
|
+
warnings.append({"issue": "money_not_shown", "detail": detail})
|
|
188
|
+
else:
|
|
189
|
+
record: dict[str, Any] = {
|
|
190
|
+
"issue": "money_not_shown",
|
|
191
|
+
"detail": (
|
|
192
|
+
f"The view has a unit price ({', '.join(unit_prices)}) but no chart shows "
|
|
193
|
+
"money. A unit price is not revenue: "
|
|
194
|
+
+ (
|
|
195
|
+
f"add revenue = {quantities[0]} * {unit_prices[0]}, then chart its sum."
|
|
196
|
+
if quantities
|
|
197
|
+
else "chart revenue, not the sum of the price."
|
|
198
|
+
)
|
|
199
|
+
),
|
|
200
|
+
}
|
|
201
|
+
if quantities:
|
|
202
|
+
record["fix"] = _math_fix(view_ref, dataset_id, quantities[0], unit_prices[0])
|
|
203
|
+
warnings.append(record)
|
|
204
|
+
summed = sorted({name for name, agg in aggs if name in unit_prices and agg == "sum"})
|
|
205
|
+
if summed:
|
|
206
|
+
record = {
|
|
207
|
+
"issue": "unit_price_summed",
|
|
208
|
+
"detail": (
|
|
209
|
+
f"A chart sums the unit price {summed[0]}; that total means nothing. "
|
|
210
|
+
+ (
|
|
211
|
+
f"Chart the sum of revenue = {quantities[0]} * {summed[0]} instead."
|
|
212
|
+
if quantities
|
|
213
|
+
else "Use avg, or chart revenue instead."
|
|
214
|
+
)
|
|
215
|
+
),
|
|
216
|
+
}
|
|
217
|
+
if quantities:
|
|
218
|
+
record["fix"] = _math_fix(view_ref, dataset_id, quantities[0], summed[0])
|
|
219
|
+
warnings.append(record)
|
|
220
|
+
shown = measures | dims
|
|
221
|
+
for profile in profiles:
|
|
222
|
+
name = profile["name"]
|
|
223
|
+
rate = profile.get("nullRate") or profile.get("emptyRate") or 0
|
|
224
|
+
if not isinstance(rate, (int, float)) or rate <= 0:
|
|
225
|
+
continue
|
|
226
|
+
if shown and name not in shown:
|
|
227
|
+
continue
|
|
228
|
+
warnings.append(
|
|
229
|
+
{
|
|
230
|
+
"issue": "blank_values",
|
|
231
|
+
"column": name,
|
|
232
|
+
"detail": (
|
|
233
|
+
f"{round(rate * 100)}% of {name} is blank. Say in your report what you "
|
|
234
|
+
"did: filled, filtered, or kept (and how the board shows them)."
|
|
235
|
+
),
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
return warnings
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def upload_hints(
|
|
242
|
+
view_id: Any,
|
|
243
|
+
metadata: Iterable[Mapping[str, Any]],
|
|
244
|
+
rows: Iterable[Mapping[str, Any]],
|
|
245
|
+
dataset_id: Any = None,
|
|
246
|
+
text_numbers: Iterable[str] = (),
|
|
247
|
+
) -> list[dict[str, Any]]:
|
|
248
|
+
"""The money warnings for a new view, before any dashboard exists.
|
|
249
|
+
|
|
250
|
+
The fix is cheapest here: a column added before ``create-blank`` is on
|
|
251
|
+
the dashboard, one added after is not (``NEW_COLUMN_NOTE``).
|
|
252
|
+
``text_numbers`` are the columns ``column_warnings`` found to be numbers
|
|
253
|
+
stored as text; they count as numbers here, and the warning says to
|
|
254
|
+
convert them first.
|
|
255
|
+
"""
|
|
256
|
+
pending = set(text_numbers)
|
|
257
|
+
profiles = [
|
|
258
|
+
{**profile, "type": "measure"} if profile["name"] in pending else profile
|
|
259
|
+
for profile in profiles_from_view(metadata, rows)
|
|
260
|
+
]
|
|
261
|
+
canvas_doc = {"canvas": {"dataset": {"dataview_id": view_id}}}
|
|
262
|
+
warnings = []
|
|
263
|
+
# Blanks are already in the upload's column_warnings.
|
|
264
|
+
for warning in review(canvas_doc, dataset_id, profiles):
|
|
265
|
+
if warning["issue"] == "blank_values":
|
|
266
|
+
continue
|
|
267
|
+
# No board exists yet: drop the "no chart shows it" clause.
|
|
268
|
+
detail = (
|
|
269
|
+
str(warning["detail"])
|
|
270
|
+
.replace(" but no chart shows money", "")
|
|
271
|
+
.replace(" but no chart or KPI shows it", "")
|
|
272
|
+
)
|
|
273
|
+
convert = sorted(name for name in pending if name in detail)
|
|
274
|
+
if convert:
|
|
275
|
+
detail = (
|
|
276
|
+
f"Convert {', '.join(convert)} to NUMERIC first (see column_warnings). " + detail
|
|
277
|
+
)
|
|
278
|
+
warnings.append({**warning, "detail": detail})
|
|
279
|
+
return warnings
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def has_profiles(canvas_doc: Any) -> bool:
|
|
283
|
+
"""Whether a canvas read carries the backend's column profile."""
|
|
284
|
+
return isinstance(canvas_doc, Mapping) and bool(_profiles(canvas_doc))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|