tableau-cloud-audit 0.2.0__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.
- tableau_cloud_audit-0.2.0/.github/ISSUE_TEMPLATE/bug_report.yml +85 -0
- tableau_cloud_audit-0.2.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- tableau_cloud_audit-0.2.0/.github/ISSUE_TEMPLATE/scale_report.yml +69 -0
- tableau_cloud_audit-0.2.0/.github/workflows/ci.yml +49 -0
- tableau_cloud_audit-0.2.0/.github/workflows/release.yml +47 -0
- tableau_cloud_audit-0.2.0/.gitignore +25 -0
- tableau_cloud_audit-0.2.0/AI_GUIDE.md +278 -0
- tableau_cloud_audit-0.2.0/CHANGELOG.md +174 -0
- tableau_cloud_audit-0.2.0/CLAUDE.md +559 -0
- tableau_cloud_audit-0.2.0/CODE_OF_CONDUCT.md +129 -0
- tableau_cloud_audit-0.2.0/CONTRIBUTING.md +61 -0
- tableau_cloud_audit-0.2.0/LICENSE +202 -0
- tableau_cloud_audit-0.2.0/NOTICE +12 -0
- tableau_cloud_audit-0.2.0/PKG-INFO +221 -0
- tableau_cloud_audit-0.2.0/README.md +181 -0
- tableau_cloud_audit-0.2.0/SECURITY.md +67 -0
- tableau_cloud_audit-0.2.0/collector.toml.example +34 -0
- tableau_cloud_audit-0.2.0/docs/api-coverage.md +119 -0
- tableau_cloud_audit-0.2.0/docs/assets/demo.gif +0 -0
- tableau_cloud_audit-0.2.0/docs/assets/demo.tape +35 -0
- tableau_cloud_audit-0.2.0/docs/assets/query.gif +0 -0
- tableau_cloud_audit-0.2.0/docs/assets/query.tape +43 -0
- tableau_cloud_audit-0.2.0/docs/cli-reference.md +136 -0
- tableau_cloud_audit-0.2.0/docs/cookbook/activity-over-time.md +25 -0
- tableau_cloud_audit-0.2.0/docs/cookbook/all-users-grants.md +26 -0
- tableau_cloud_audit-0.2.0/docs/cookbook/certified-content.md +22 -0
- tableau_cloud_audit-0.2.0/docs/cookbook/embedded-credentials.md +24 -0
- tableau_cloud_audit-0.2.0/docs/cookbook/inactive-users.md +28 -0
- tableau_cloud_audit-0.2.0/docs/cookbook/index.md +42 -0
- tableau_cloud_audit-0.2.0/docs/cookbook/ownerless-content.md +25 -0
- tableau_cloud_audit-0.2.0/docs/cookbook/unused-content.md +29 -0
- tableau_cloud_audit-0.2.0/docs/getting-started.md +206 -0
- tableau_cloud_audit-0.2.0/docs/index.md +69 -0
- tableau_cloud_audit-0.2.0/docs/package-file-schema.md +122 -0
- tableau_cloud_audit-0.2.0/docs/runbook.md +136 -0
- tableau_cloud_audit-0.2.0/docs/troubleshooting.md +140 -0
- tableau_cloud_audit-0.2.0/docs/what-we-collect.md +149 -0
- tableau_cloud_audit-0.2.0/mkdocs.yml +64 -0
- tableau_cloud_audit-0.2.0/overrides/main.html +33 -0
- tableau_cloud_audit-0.2.0/pyproject.toml +79 -0
- tableau_cloud_audit-0.2.0/src/tca/__init__.py +7 -0
- tableau_cloud_audit-0.2.0/src/tca/cli.py +950 -0
- tableau_cloud_audit-0.2.0/src/tca/config.py +127 -0
- tableau_cloud_audit-0.2.0/src/tca/modules/__init__.py +0 -0
- tableau_cloud_audit-0.2.0/src/tca/modules/activity.py +118 -0
- tableau_cloud_audit-0.2.0/src/tca/modules/automation.py +33 -0
- tableau_cloud_audit-0.2.0/src/tca/modules/base.py +128 -0
- tableau_cloud_audit-0.2.0/src/tca/modules/content.py +79 -0
- tableau_cloud_audit-0.2.0/src/tca/modules/metadata.py +70 -0
- tableau_cloud_audit-0.2.0/src/tca/modules/permissions.py +114 -0
- tableau_cloud_audit-0.2.0/src/tca/modules/rest_core.py +52 -0
- tableau_cloud_audit-0.2.0/src/tca/normalize.py +588 -0
- tableau_cloud_audit-0.2.0/src/tca/pseudo/__init__.py +0 -0
- tableau_cloud_audit-0.2.0/src/tca/pseudo/manifest.py +478 -0
- tableau_cloud_audit-0.2.0/src/tca/pseudo/scrubber.py +259 -0
- tableau_cloud_audit-0.2.0/src/tca/sources/__init__.py +0 -0
- tableau_cloud_audit-0.2.0/src/tca/sources/metadata.py +73 -0
- tableau_cloud_audit-0.2.0/src/tca/sources/rest.py +107 -0
- tableau_cloud_audit-0.2.0/src/tca/sources/vds.py +39 -0
- tableau_cloud_audit-0.2.0/src/tca/storage/__init__.py +0 -0
- tableau_cloud_audit-0.2.0/src/tca/storage/migrations/001_init.sql +80 -0
- tableau_cloud_audit-0.2.0/src/tca/storage/migrations/002_event_history.sql +50 -0
- tableau_cloud_audit-0.2.0/src/tca/storage/migrations/003_typed_state.sql +120 -0
- tableau_cloud_audit-0.2.0/src/tca/storage/migrations/004_clear_views.sql +62 -0
- tableau_cloud_audit-0.2.0/src/tca/storage/migrations/005_job_history.sql +41 -0
- tableau_cloud_audit-0.2.0/src/tca/storage/migrations/006_vds_metadata_state.sql +185 -0
- tableau_cloud_audit-0.2.0/src/tca/storage/migrations/007_current_view_semantics.sql +50 -0
- tableau_cloud_audit-0.2.0/src/tca/storage/migrations/008_state_current_only.sql +74 -0
- tableau_cloud_audit-0.2.0/src/tca/storage/writer.py +742 -0
- tableau_cloud_audit-0.2.0/src/tca/transport/__init__.py +0 -0
- tableau_cloud_audit-0.2.0/src/tca/transport/auth.py +45 -0
- tableau_cloud_audit-0.2.0/src/tca/transport/client.py +215 -0
- tableau_cloud_audit-0.2.0/tests/test_activity.py +267 -0
- tableau_cloud_audit-0.2.0/tests/test_automation.py +122 -0
- tableau_cloud_audit-0.2.0/tests/test_cli.py +281 -0
- tableau_cloud_audit-0.2.0/tests/test_config.py +55 -0
- tableau_cloud_audit-0.2.0/tests/test_content.py +174 -0
- tableau_cloud_audit-0.2.0/tests/test_docs.py +33 -0
- tableau_cloud_audit-0.2.0/tests/test_export.py +107 -0
- tableau_cloud_audit-0.2.0/tests/test_history.py +153 -0
- tableau_cloud_audit-0.2.0/tests/test_metadata.py +266 -0
- tableau_cloud_audit-0.2.0/tests/test_migrations.py +73 -0
- tableau_cloud_audit-0.2.0/tests/test_normalize.py +489 -0
- tableau_cloud_audit-0.2.0/tests/test_peek.py +103 -0
- tableau_cloud_audit-0.2.0/tests/test_permissions.py +169 -0
- tableau_cloud_audit-0.2.0/tests/test_pseudo.py +219 -0
- tableau_cloud_audit-0.2.0/tests/test_rest_core.py +111 -0
- tableau_cloud_audit-0.2.0/tests/test_resume.py +137 -0
- tableau_cloud_audit-0.2.0/tests/test_schema_contract.py +419 -0
- tableau_cloud_audit-0.2.0/tests/test_smoke.py +19 -0
- tableau_cloud_audit-0.2.0/tests/test_storage.py +163 -0
- tableau_cloud_audit-0.2.0/tests/test_transport.py +175 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something went wrong or behaved unexpectedly.
|
|
3
|
+
title: "[bug] "
|
|
4
|
+
labels: ["bug"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
Thanks for reporting! A few structured details make bugs much quicker to fix.
|
|
10
|
+
|
|
11
|
+
**Privacy:** the safest way to hand us context is `tca diagnostics --markdown` —
|
|
12
|
+
PII-free by design. If you paste an error or traceback, glance over it first and
|
|
13
|
+
redact anything site-specific: the collector masks the PAT and pseudonymises
|
|
14
|
+
identities, but a raw Python traceback can still echo payload fragments.
|
|
15
|
+
- type: textarea
|
|
16
|
+
id: what-happened
|
|
17
|
+
attributes:
|
|
18
|
+
label: What happened
|
|
19
|
+
description: A clear description of the bug — what you expected versus what actually happened.
|
|
20
|
+
placeholder: "tca collect crashed on the permissions module; I expected it to finish or skip the item."
|
|
21
|
+
validations:
|
|
22
|
+
required: true
|
|
23
|
+
- type: dropdown
|
|
24
|
+
id: command
|
|
25
|
+
attributes:
|
|
26
|
+
label: Which command?
|
|
27
|
+
options:
|
|
28
|
+
- init
|
|
29
|
+
- verify
|
|
30
|
+
- collect
|
|
31
|
+
- summary
|
|
32
|
+
- runs
|
|
33
|
+
- diagnostics
|
|
34
|
+
- peek
|
|
35
|
+
- resolve
|
|
36
|
+
- export
|
|
37
|
+
- other / not sure
|
|
38
|
+
validations:
|
|
39
|
+
required: false
|
|
40
|
+
- type: textarea
|
|
41
|
+
id: repro
|
|
42
|
+
attributes:
|
|
43
|
+
label: Steps to reproduce
|
|
44
|
+
description: What you ran, in order — the exact command(s), but NEVER the PAT secret.
|
|
45
|
+
placeholder: |
|
|
46
|
+
1. tca collect
|
|
47
|
+
2. ...
|
|
48
|
+
render: shell
|
|
49
|
+
validations:
|
|
50
|
+
required: false
|
|
51
|
+
- type: textarea
|
|
52
|
+
id: error
|
|
53
|
+
attributes:
|
|
54
|
+
label: Error output / traceback
|
|
55
|
+
description: Paste the message or traceback. Redact anything site-specific first.
|
|
56
|
+
render: text
|
|
57
|
+
validations:
|
|
58
|
+
required: false
|
|
59
|
+
- type: textarea
|
|
60
|
+
id: diagnostics
|
|
61
|
+
attributes:
|
|
62
|
+
label: "`tca diagnostics --markdown` output"
|
|
63
|
+
description: Run `tca diagnostics --markdown` and paste it — PII-free by design; gives us versions + scale at a glance.
|
|
64
|
+
placeholder: |
|
|
65
|
+
## tca diagnostics
|
|
66
|
+
_Safe to share — no names, e-mails, LUIDs or site identity._
|
|
67
|
+
...
|
|
68
|
+
render: markdown
|
|
69
|
+
validations:
|
|
70
|
+
required: false
|
|
71
|
+
- type: input
|
|
72
|
+
id: version
|
|
73
|
+
attributes:
|
|
74
|
+
label: Collector version
|
|
75
|
+
description: From `tca --version` (skip if you pasted the diagnostics output above).
|
|
76
|
+
placeholder: "0.1.0"
|
|
77
|
+
validations:
|
|
78
|
+
required: false
|
|
79
|
+
- type: checkboxes
|
|
80
|
+
id: privacy-ack
|
|
81
|
+
attributes:
|
|
82
|
+
label: Privacy check
|
|
83
|
+
options:
|
|
84
|
+
- label: I've reviewed what I'm pasting — it contains no real names, e-mails, user LUIDs or my PAT secret.
|
|
85
|
+
required: true
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: Scale report
|
|
2
|
+
description: Tell us how the collector ran on your Tableau Cloud site — size, timing, anything that broke.
|
|
3
|
+
title: "[scale] "
|
|
4
|
+
labels: ["scale-report"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
Thanks for running **tableau-cloud-audit** on a real site! Reports from larger
|
|
10
|
+
estates are how we harden it beyond the tiny dev sandbox it is built on.
|
|
11
|
+
|
|
12
|
+
**Privacy first — the easy way to share numbers is `tca diagnostics --markdown`.**
|
|
13
|
+
Its output is PII-free *by construction*: no names, e-mails, LUIDs or your site
|
|
14
|
+
identity, and it passes a final self-gate before printing. Paste it below and you
|
|
15
|
+
are done. (Prefer not to? Skip it and just fill in the rough scale.)
|
|
16
|
+
- type: textarea
|
|
17
|
+
id: diagnostics
|
|
18
|
+
attributes:
|
|
19
|
+
label: "`tca diagnostics --markdown` output"
|
|
20
|
+
description: Run `tca diagnostics --markdown` and paste the whole report here.
|
|
21
|
+
placeholder: |
|
|
22
|
+
## tca diagnostics
|
|
23
|
+
_Safe to share — no names, e-mails, LUIDs or site identity._
|
|
24
|
+
...
|
|
25
|
+
render: markdown
|
|
26
|
+
validations:
|
|
27
|
+
required: false
|
|
28
|
+
- type: dropdown
|
|
29
|
+
id: outcome
|
|
30
|
+
attributes:
|
|
31
|
+
label: Did the collection complete?
|
|
32
|
+
options:
|
|
33
|
+
- "Yes — tca collect finished (status ok)"
|
|
34
|
+
- "Partial — it stopped and was resumable"
|
|
35
|
+
- "No — it failed"
|
|
36
|
+
- "Haven't run a full collect yet"
|
|
37
|
+
validations:
|
|
38
|
+
required: true
|
|
39
|
+
- type: input
|
|
40
|
+
id: scale
|
|
41
|
+
attributes:
|
|
42
|
+
label: Rough site scale
|
|
43
|
+
description: Ballpark is fine — useful especially if you skipped the diagnostics output.
|
|
44
|
+
placeholder: "~4000 workbooks, ~600 datasources, ~1500 users, ~200 projects"
|
|
45
|
+
validations:
|
|
46
|
+
required: false
|
|
47
|
+
- type: textarea
|
|
48
|
+
id: observations
|
|
49
|
+
attributes:
|
|
50
|
+
label: What happened
|
|
51
|
+
description: Timing, memory, rate-limiting, errors — anything that felt slow or broke. Which module was the slow/failing one, if you noticed.
|
|
52
|
+
placeholder: "collect took ~40 min; the permissions module dominated; one 429 backoff; ..."
|
|
53
|
+
validations:
|
|
54
|
+
required: false
|
|
55
|
+
- type: input
|
|
56
|
+
id: version
|
|
57
|
+
attributes:
|
|
58
|
+
label: Collector version
|
|
59
|
+
description: From `tca --version` (also shown in the diagnostics output).
|
|
60
|
+
placeholder: "0.1.0"
|
|
61
|
+
validations:
|
|
62
|
+
required: false
|
|
63
|
+
- type: checkboxes
|
|
64
|
+
id: privacy-ack
|
|
65
|
+
attributes:
|
|
66
|
+
label: Privacy check
|
|
67
|
+
options:
|
|
68
|
+
- label: I've reviewed what I'm pasting — it contains no real names, e-mails or user LUIDs. (The `tca diagnostics` output is safe by design; this only matters for any free text I added myself.)
|
|
69
|
+
required: true
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
cache: pip
|
|
21
|
+
- name: Install
|
|
22
|
+
run: pip install -e ".[dev]"
|
|
23
|
+
- name: Lint (ruff)
|
|
24
|
+
run: |
|
|
25
|
+
ruff check .
|
|
26
|
+
ruff format --check .
|
|
27
|
+
- name: Type check (mypy)
|
|
28
|
+
run: mypy
|
|
29
|
+
- name: Tests (incl. the schema contract — the additive-only gate)
|
|
30
|
+
run: pytest -q
|
|
31
|
+
|
|
32
|
+
docs:
|
|
33
|
+
# Publishes the MkDocs site to GitHub Pages on every push to main.
|
|
34
|
+
# One-time repo setup: Settings -> Pages -> deploy from `gh-pages` branch.
|
|
35
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
36
|
+
needs: test
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
permissions:
|
|
39
|
+
contents: write
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
- uses: actions/setup-python@v5
|
|
43
|
+
with:
|
|
44
|
+
python-version: "3.13"
|
|
45
|
+
cache: pip
|
|
46
|
+
- name: Install docs toolchain
|
|
47
|
+
run: pip install -e ".[docs]"
|
|
48
|
+
- name: Build and deploy
|
|
49
|
+
run: mkdocs gh-deploy --force
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes via PyPI Trusted Publishing (OIDC) — no tokens stored anywhere.
|
|
4
|
+
# One-time PyPI setup: pypi.org -> Account -> Publishing -> Add pending
|
|
5
|
+
# publisher (project: tableau-cloud-audit, owner: simboli, repo:
|
|
6
|
+
# tableau-cloud-audit, workflow: release.yml, environment: pypi).
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
release:
|
|
10
|
+
types: [published]
|
|
11
|
+
workflow_dispatch: {}
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.13"
|
|
21
|
+
- name: Build sdist and wheel
|
|
22
|
+
run: |
|
|
23
|
+
pip install build
|
|
24
|
+
python -m build
|
|
25
|
+
- name: Sanity check
|
|
26
|
+
run: |
|
|
27
|
+
pip install twine
|
|
28
|
+
twine check dist/*
|
|
29
|
+
- uses: actions/upload-artifact@v4
|
|
30
|
+
with:
|
|
31
|
+
name: dist
|
|
32
|
+
path: dist/
|
|
33
|
+
|
|
34
|
+
publish:
|
|
35
|
+
needs: build
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
environment:
|
|
38
|
+
name: pypi
|
|
39
|
+
url: https://pypi.org/p/tableau-cloud-audit
|
|
40
|
+
permissions:
|
|
41
|
+
id-token: write # OIDC: this IS the credential
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/download-artifact@v4
|
|
44
|
+
with:
|
|
45
|
+
name: dist
|
|
46
|
+
path: dist/
|
|
47
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.venv/
|
|
8
|
+
|
|
9
|
+
# Tooling caches
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
|
|
14
|
+
# Site-specific local files — never commit client data or config
|
|
15
|
+
*.duckdb
|
|
16
|
+
*.duckdb.wal
|
|
17
|
+
*.duckdb.sha256
|
|
18
|
+
collector.toml
|
|
19
|
+
.env
|
|
20
|
+
|
|
21
|
+
# OS / editor
|
|
22
|
+
.DS_Store
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
25
|
+
site/
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
This file is written FOR AN AI ASSISTANT.
|
|
3
|
+
If you are a human: hand this file to Claude (or any capable LLM) and then
|
|
4
|
+
ask your question about tableau-cloud-audit. It gives the assistant enough
|
|
5
|
+
grounding to answer accurately instead of guessing.
|
|
6
|
+
If you are an AI assistant: read this whole file before answering. It is
|
|
7
|
+
the ground truth about this project's purpose, boundaries, architecture,
|
|
8
|
+
and the invariants you must never violate in any suggestion you make.
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
# AI assistant guide to `tableau-cloud-audit`
|
|
12
|
+
|
|
13
|
+
You are helping someone understand, use, or extend **tableau-cloud-audit**
|
|
14
|
+
(CLI name: `tca`). This document is your briefing. Read it fully, then answer
|
|
15
|
+
the user's actual question. When the repository is available to you, prefer
|
|
16
|
+
reading the real code over reciting this file — this is orientation, the code
|
|
17
|
+
is truth. When it is not available, this file is your best grounding; say so
|
|
18
|
+
if you are asked about something it does not cover, rather than inventing it.
|
|
19
|
+
|
|
20
|
+
Answer in the user's language even though this guide is in English.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 1. What this project is (in one paragraph)
|
|
25
|
+
|
|
26
|
+
`tableau-cloud-audit` is an **open-source, client-executed collector** for
|
|
27
|
+
Tableau Cloud. An administrator runs it with a Personal Access Token (PAT);
|
|
28
|
+
it reads their Tableau Cloud site through the REST API, the VizQL Data Service
|
|
29
|
+
(Admin Insights), and the Metadata API (GraphQL), and writes everything into
|
|
30
|
+
**one local DuckDB file** — pseudonymised by construction, optionally
|
|
31
|
+
encrypted at rest. That file is the deliverable: a complete, queryable
|
|
32
|
+
snapshot of a Tableau Cloud estate (users, groups, content, permissions,
|
|
33
|
+
activity history, lineage) that outlives Tableau's own ~90-day retention.
|
|
34
|
+
|
|
35
|
+
Tagline the project uses: *"your entire Tableau Cloud estate in one DuckDB
|
|
36
|
+
file."*
|
|
37
|
+
|
|
38
|
+
## 2. What this project is NOT (this matters — do not cross it)
|
|
39
|
+
|
|
40
|
+
This repository is **only the collector**. It extracts and stores; it does
|
|
41
|
+
**not analyze, score, rank, or evaluate** anything.
|
|
42
|
+
|
|
43
|
+
The analysis engine — effective-permission resolution, usage tiering,
|
|
44
|
+
zombie/dormancy scoring, duplicate-metric clustering, findings generation, the
|
|
45
|
+
report deliverables — is **proprietary and lives in a separate private
|
|
46
|
+
repository**. It is the commercial half of the product; this collector is the
|
|
47
|
+
free, trust-building half and the funnel to it.
|
|
48
|
+
|
|
49
|
+
**The moat rule, which governs every suggestion you make:** if a change would
|
|
50
|
+
teach a reader *how a Tableau site gets evaluated* — any logic that turns raw
|
|
51
|
+
facts into a judgment (tiers, scores, "this is a problem", effective
|
|
52
|
+
permissions, risk levels) — **it does not belong in this repo.** Mechanical
|
|
53
|
+
transformations (renaming a field, parsing a timestamp, flattening a list,
|
|
54
|
+
deduplicating on a natural key) are fine and welcome. Interpretation is not.
|
|
55
|
+
|
|
56
|
+
If a user asks you to add analysis/scoring/tiering here, do not just do it —
|
|
57
|
+
explain that it belongs in the private engine and offer the mechanical,
|
|
58
|
+
in-scope alternative instead. The query cookbook in the docs is allowed to go
|
|
59
|
+
*teaser-deep* (show a stale-content query) but never *moat-deep* (never
|
|
60
|
+
compute effective permissions or a dormancy score).
|
|
61
|
+
|
|
62
|
+
## 3. The privacy model (the other hard invariant)
|
|
63
|
+
|
|
64
|
+
Privacy is **structural, not best-effort**. Internalize this; it constrains
|
|
65
|
+
almost every code suggestion.
|
|
66
|
+
|
|
67
|
+
- **Pseudonymisation is a mandatory gate on the write path.** Nothing reaches
|
|
68
|
+
storage without passing through `Scrubber.scrub()`. Real user identities
|
|
69
|
+
(LUID, username, full name, e-mail, external auth id) are replaced with a
|
|
70
|
+
stable pseudonym `U-####` *before* the payload is written.
|
|
71
|
+
- **The single reviewable PII manifest** (`src/tca/pseudo/manifest.py`) is the
|
|
72
|
+
privacy contract. It declares, per endpoint, *where* personal fields live.
|
|
73
|
+
It is I/O-contract metadata, not business logic. Every endpoint the
|
|
74
|
+
collector calls **must** be registered there — an unregistered endpoint's
|
|
75
|
+
payload is refused, never written.
|
|
76
|
+
- **The identity vault** (`identity.map` table) is the ONLY place real
|
|
77
|
+
identities exist, keyed by pseudonym. It lives inside the same DuckDB file.
|
|
78
|
+
- **The safety net**: after scrubbing, a blocking check rejects any payload
|
|
79
|
+
where an e-mail-shaped string or a known user LUID survived. A gap in the
|
|
80
|
+
manifest fails loudly — it never leaks silently.
|
|
81
|
+
- **Three minimization mechanisms** beyond pseudonymisation: credential
|
|
82
|
+
usernames are redacted to `[redacted]`; VDS and GraphQL sources request only
|
|
83
|
+
a curated column/field list (identity columns are never even fetched);
|
|
84
|
+
e-mail-only sources are reverse-looked-up to `U-####` (unknown → `[redacted]`).
|
|
85
|
+
- **The export boundary**: `tca export` produces the ONLY file allowed to
|
|
86
|
+
leave the client machine. It is a copy with the `identity` schema **dropped**
|
|
87
|
+
— so real identities are structurally absent, not merely filtered — plus a
|
|
88
|
+
re-run of the safety net and a SHA-256 sidecar. The original file never
|
|
89
|
+
leaves the machine.
|
|
90
|
+
|
|
91
|
+
When you suggest adding a new data source, the FIRST thing to check is: does
|
|
92
|
+
it carry identity, and is it in the manifest? Never propose writing a payload
|
|
93
|
+
that bypasses the scrubber.
|
|
94
|
+
|
|
95
|
+
## 4. Architecture — the shape of the code
|
|
96
|
+
|
|
97
|
+
Repository layout (Python package `tca`, installed as CLI `tca`):
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
src/tca/
|
|
101
|
+
cli.py # Typer app: init, verify, collect, summary, peek, resolve, export
|
|
102
|
+
config.py # pydantic config: collector.toml + TCA_* env vars
|
|
103
|
+
normalize.py # mechanical raw → typed `state` tables (runs after each collect)
|
|
104
|
+
transport/ # HTTP layer (NOT named http/ to avoid stdlib clash)
|
|
105
|
+
client.py # httpx wrapper: auth header, retry/backoff, pagination, silent re-auth
|
|
106
|
+
auth.py # PAT sign-in
|
|
107
|
+
sources/ # dumb endpoint wrappers, typed responses (no logic)
|
|
108
|
+
rest.py vds.py metadata.py
|
|
109
|
+
modules/ # one extraction domain each; fetch → scrub → land
|
|
110
|
+
base.py # Module protocol + RunContext + ModuleStats
|
|
111
|
+
rest_core.py content.py automation.py permissions.py activity.py metadata.py
|
|
112
|
+
pseudo/
|
|
113
|
+
manifest.py # THE PII manifest (privacy contract) — REST, VDS, GraphQL sections
|
|
114
|
+
scrubber.py # the mandatory scrub gate + safety net + identity upserts
|
|
115
|
+
storage/
|
|
116
|
+
writer.py # PackageStore: the DuckDB connection, migrations, history, export
|
|
117
|
+
migrations/ # numbered, additive-only SQL (001…007); released files are FROZEN
|
|
118
|
+
tests/ # pytest, one file per module; ~98 tests
|
|
119
|
+
docs/ # PUBLIC docs only (MkDocs Material site)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**The data flow of one `tca collect`:**
|
|
123
|
+
|
|
124
|
+
1. Sign in (PAT → session token + site LUID).
|
|
125
|
+
2. For each selected module, in dependency order: fetch pages from a source,
|
|
126
|
+
pass each through `Scrubber.scrub()`, and `land()` it into
|
|
127
|
+
`raw.api_responses` (one row = one HTTP response page, already scrubbed).
|
|
128
|
+
Every page commits immediately.
|
|
129
|
+
3. History accumulation: TS Events → `history.events`, Job Performance →
|
|
130
|
+
`history.job_runs` (deduplicated on a natural key, cumulative across runs —
|
|
131
|
+
this is what outlives Tableau's retention window).
|
|
132
|
+
4. `normalize.py` rebuilds the typed `state.*` tables from the raw pages of
|
|
133
|
+
this run (delete + insert; raw stays the source of truth).
|
|
134
|
+
|
|
135
|
+
**The package-file schema (current version v0.7)** has these DuckDB schemas:
|
|
136
|
+
|
|
137
|
+
- `meta` — logbook: `file_info`, `collection_runs`, `schema_migrations`,
|
|
138
|
+
`event_coverage`, and views (`v_latest_run`, `v_endpoint_latest_run`,
|
|
139
|
+
`v_event_gaps`).
|
|
140
|
+
- `raw` — `api_responses`: the source of truth, scrubbed JSON payloads.
|
|
141
|
+
- `state` — convenient typed tables, one row per object, rebuilt each run,
|
|
142
|
+
with `v_*_current` views (each following the latest run that actually
|
|
143
|
+
collected that data — see §7 "known subtleties").
|
|
144
|
+
- `history` — `events`, `job_runs`: append-only accumulators.
|
|
145
|
+
- `identity` — `map`: the vault (the only real identities; absent from exports).
|
|
146
|
+
- `clear` — read-time views re-joining `state` with `identity.map` so the
|
|
147
|
+
local user sees real names (`tca peek` reads these; impossible on an export).
|
|
148
|
+
|
|
149
|
+
## 5. The CLI surface (7 commands)
|
|
150
|
+
|
|
151
|
+
Secrets are environment variables ONLY, never flags or files:
|
|
152
|
+
`TCA_PAT_SECRET` (the PAT), `TCA_DB_KEY` (optional encryption passphrase).
|
|
153
|
+
|
|
154
|
+
| Command | Purpose |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `tca init` | Wizard → `collector.toml` + empty package file |
|
|
157
|
+
| `tca verify` | Pre-flight: config, secret, sign-in, Admin Insights/VDS, Metadata API, file. Collects nothing |
|
|
158
|
+
| `tca collect` | The main run: fetch → pseudonymise → land → normalize. `--modules/-m`, `--resume` |
|
|
159
|
+
| `tca summary` | What the file holds: runs, pages, vault size, history window, coverage gaps |
|
|
160
|
+
| `tca peek VIEW` | Browse the latest snapshot WITH real identities (local only): `users`, `members`, `content`, `rules` |
|
|
161
|
+
| `tca resolve U-####` | One pseudonym → full identity, from the local vault |
|
|
162
|
+
| `tca export [OUT]` | The shareable copy: everything except `identity`, verified, + SHA-256 |
|
|
163
|
+
|
|
164
|
+
Modules for `collect` (default set = all six):
|
|
165
|
+
`rest_core` (users, groups, membership), `content` (projects, workbooks,
|
|
166
|
+
views, datasources, connections), `automation` (refresh tasks, jobs,
|
|
167
|
+
subscriptions), `permissions` (project + default-template + per-item rules),
|
|
168
|
+
`activity` (Admin Insights via VDS: TS Events, TS Users, Site Content, Tokens,
|
|
169
|
+
Job Performance, Groups, Permissions, Subscriptions, Viz Load Times),
|
|
170
|
+
`metadata` (Metadata API GraphQL: fields, calculated-field formulas, lineage,
|
|
171
|
+
sheet field usage).
|
|
172
|
+
|
|
173
|
+
Exit codes: `0` success · `1` actionable user error (clear message, no
|
|
174
|
+
traceback) · `130` Ctrl-C (the run is resumable).
|
|
175
|
+
|
|
176
|
+
## 6. Conventions and stack (respect these in any code you write)
|
|
177
|
+
|
|
178
|
+
- **Language**: conversation with the maintainer happens in Italian;
|
|
179
|
+
**everything that ships in the repo is English only** — code, identifiers,
|
|
180
|
+
comments, docstrings, commit messages, CLI output, docs.
|
|
181
|
+
- **Stack**: Python ≥3.11 (developed on 3.13). Runtime deps deliberately short
|
|
182
|
+
— `httpx`, `duckdb` (≥1.4, for native encryption), `typer`, `pydantic`,
|
|
183
|
+
`rich`. Own HTTP client, NOT `tableauserverclient`. Standard `venv`.
|
|
184
|
+
License **Apache-2.0**.
|
|
185
|
+
- **Tooling**: `ruff` (format + lint, line length 100), `mypy` (strict; types
|
|
186
|
+
on all public signatures), `pytest` (one test file per module). CI runs
|
|
187
|
+
ruff + mypy + the full suite on Python 3.11 and 3.13; **the schema contract
|
|
188
|
+
test gates every merge**.
|
|
189
|
+
- **Docstrings** only on the public API surface (CLI commands, module entry
|
|
190
|
+
points). No comments on obvious internal code. A comment states a constraint
|
|
191
|
+
the code can't show — never narrates what the next line does.
|
|
192
|
+
- **Error handling**: fail loud. No silent `except: pass`. Network/auth errors
|
|
193
|
+
propagate with a clear `rich`-formatted message. The PAT is never logged or
|
|
194
|
+
printed, including in error output.
|
|
195
|
+
- **Commits**: Conventional Commits (`feat:`, `fix:`, `docs:`, `test:` …).
|
|
196
|
+
**The maintainer commits personally** — an AI should propose the commit
|
|
197
|
+
message, not run `git commit`, unless explicitly asked.
|
|
198
|
+
- **Schema migrations are additive-only and released files are FROZEN.** Never
|
|
199
|
+
edit `001`…`007`; a new need gets a new numbered file. Tables/columns/views
|
|
200
|
+
never disappear or change type — the analyst's engine depends on this
|
|
201
|
+
guarantee, enforced by `tests/test_schema_contract.py`. When the schema
|
|
202
|
+
grows, update the golden snapshot in that test in the same change.
|
|
203
|
+
- **Docs discipline**: `docs/api-coverage.md` (endpoint status) and
|
|
204
|
+
`docs/package-file-schema.md` / `docs/what-we-collect.md` are kept in sync
|
|
205
|
+
with the code in the same change that alters an endpoint or the schema;
|
|
206
|
+
drift is caught by `tests/test_docs.py`. Only PUBLIC docs live in `docs/` —
|
|
207
|
+
no business/IP material.
|
|
208
|
+
|
|
209
|
+
## 7. Known subtleties (so you don't "rediscover" or break them)
|
|
210
|
+
|
|
211
|
+
- **The package file is long-lived and reused run after run.** It IS the
|
|
212
|
+
archive: deleting it regenerates pseudonyms and breaks cross-run
|
|
213
|
+
comparability. History accumulates in it.
|
|
214
|
+
- **`raw.api_responses` is the resume checkpoint.** A unit is
|
|
215
|
+
`(run_id, endpoint, entity_luid, page)`; resume skips already-landed units.
|
|
216
|
+
History accumulation reads *back from raw*, so a crash between landing and
|
|
217
|
+
accumulating heals on re-run.
|
|
218
|
+
- **`v_*_current` semantics (v0.7)**: each `state.v_*_current` view follows the
|
|
219
|
+
latest ok run that *actually collected that data* (via
|
|
220
|
+
`meta.v_endpoint_latest_run`), NOT the single latest run. This is so a
|
|
221
|
+
partial run (e.g. `-m rest_core`) refreshes only users and doesn't empty the
|
|
222
|
+
permissions/content views. An empty listing still lands a page, so
|
|
223
|
+
genuinely-empty data stays empty (no stale ghosts).
|
|
224
|
+
- **`EXPORT_SAFE_VIEWS`** in `writer.py` recreates identity-free convenience
|
|
225
|
+
views inside the export and must stay in sync with migrations 002/003/007.
|
|
226
|
+
The `clear.*` views must NEVER be added there (they touch identity).
|
|
227
|
+
- **Encryption is opt-in but recommended**: if `TCA_DB_KEY` is unset the file
|
|
228
|
+
is written unencrypted with a clear warning (maintainer's decision — no hard
|
|
229
|
+
error, no interactive prompt). An encrypted file can't be opened with a bare
|
|
230
|
+
`duckdb file.duckdb`; use the CLI or the documented ATTACH+key incantation.
|
|
231
|
+
- **Live-testing detail**: `pod` is the bare pod name (`10ax`), not a URL; the
|
|
232
|
+
site `contentUrl` may differ from the display name (dashes stripped). Both
|
|
233
|
+
are documented failure modes.
|
|
234
|
+
- **Tests are hermetic**: HTTP is mocked with `pytest-httpx`; no test touches a
|
|
235
|
+
real Tableau site. When you add a source, add a test in the same style.
|
|
236
|
+
|
|
237
|
+
## 8. Project status and roadmap (as of mid-2026)
|
|
238
|
+
|
|
239
|
+
The collector is **feature-complete for its MVP scope and verified live**
|
|
240
|
+
against a real Tableau Cloud sandbox. CI is green; the docs site is published.
|
|
241
|
+
Schema is at v0.7. ~98 tests pass.
|
|
242
|
+
|
|
243
|
+
What is deliberately deferred or out of scope (do not "helpfully" build these
|
|
244
|
+
without discussing scope first): the proprietary analysis engine (§2); typed
|
|
245
|
+
state for VDS sources beyond what exists; several backlog REST endpoints
|
|
246
|
+
(flows, virtual connections, data alerts, webhooks, site settings — see
|
|
247
|
+
`docs/api-coverage.md` for the tracker with statuses); a Parquet export escape
|
|
248
|
+
hatch; optional group pseudonymisation (`G-####`).
|
|
249
|
+
|
|
250
|
+
Known honest gaps that are NOT code problems (useful context if the user asks
|
|
251
|
+
"what would make this successful"): it has only been run against one small
|
|
252
|
+
site (scale behaviour on a 10k-user estate is unproven); the free tool lacks a
|
|
253
|
+
one-command "wow" artifact (a teaser `tca report` is the highest-leverage idea
|
|
254
|
+
on the table); distribution/adoption and the first paid assessment are the
|
|
255
|
+
real business risks, not the code; Windows packaging (the typical Tableau
|
|
256
|
+
admin persona) is still via `pip`/`uv` rather than a signed installer.
|
|
257
|
+
|
|
258
|
+
## 9. How to be genuinely useful here
|
|
259
|
+
|
|
260
|
+
- **Ground your answers in the actual code when you can see it.** File paths in
|
|
261
|
+
this guide are stable pointers; open them.
|
|
262
|
+
- **Honor the two invariants above all**: the moat rule (§2) and the privacy
|
|
263
|
+
model (§3). If a request would violate either, say so plainly and offer the
|
|
264
|
+
in-scope alternative — that is more helpful than a compliant-but-wrong patch.
|
|
265
|
+
- **Match the house style** (§6): English in the repo, short deps, fail loud,
|
|
266
|
+
additive-only schema, propose commits rather than making them.
|
|
267
|
+
- **When unsure, say so.** This project values honesty over confident guessing;
|
|
268
|
+
the maintainer explicitly prefers "I don't know, here's how to find out" to a
|
|
269
|
+
fabricated answer. If this guide and the code disagree, the code wins — and
|
|
270
|
+
point out the drift so the docs can be fixed.
|
|
271
|
+
- **Keep public/private separation sacred**: never suggest committing
|
|
272
|
+
business logic, analysis heuristics, or IP material to this public repo.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
*This guide describes the repository's intent and invariants; the code is the
|
|
277
|
+
final authority. If you spot a contradiction between this file and the code,
|
|
278
|
+
that is a bug in this file — mention it.*
|