tableau-cli 0.1.2__tar.gz → 0.1.4__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_cli-0.1.4/.claude/settings.local.json +8 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/PKG-INFO +34 -10
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/README.md +33 -9
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/pyproject.toml +1 -1
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/skills/SKILL.md +8 -5
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/skills/references/cli.md +55 -3
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/api/client.py +93 -3
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/cli.py +9 -4
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/convert_cmd.py +4 -10
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/datasources_cmd.py +12 -4
- tableau_cli-0.1.4/src/tableau_cli/commands/projects_cmd.py +38 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/search_cmd.py +2 -1
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/views_cmd.py +54 -4
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/workbooks_cmd.py +10 -1
- tableau_cli-0.1.4/src/tableau_cli/utils/_convert_worker.py +55 -0
- tableau_cli-0.1.4/src/tableau_cli/utils/convert.py +177 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/utils/datasource_metadata_utils.py +16 -0
- tableau_cli-0.1.4/src/tableau_cli/utils/lineage_utils.py +216 -0
- tableau_cli-0.1.4/src/tableau_cli/utils/tableau_version.py +21 -0
- tableau_cli-0.1.4/src/tableau_cli/utils/web_url.py +33 -0
- tableau_cli-0.1.2/.claude/settings.local.json +0 -20
- tableau_cli-0.1.2/.claude/skills/tableau-cli +0 -1
- tableau_cli-0.1.2/skills-lock.json +0 -10
- tableau_cli-0.1.2/src/tableau_cli/utils/convert.py +0 -99
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/.gitignore +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/LICENSE +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/NOTICE +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/skills/references/installation.md +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/__init__.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/api/__init__.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/auth/__init__.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/auth/with_auth.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/__init__.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/config_cmd.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/config/__init__.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/config/store.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/config/types.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/errors/__init__.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/errors/cli_error.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/errors/vds_error_handler.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/output/__init__.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/output/format.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/output/json_output.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/output/table_output.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/utils/__init__.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/utils/paginate.py +0 -0
- {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/utils/search_content_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tableau-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: CLI for Tableau Server/Cloud, designed for AI agent integration
|
|
5
5
|
Project-URL: Repository, https://github.com/i-richardwang/tableau-cli
|
|
6
6
|
Author: Richard Wang
|
|
@@ -132,7 +132,9 @@ tableau-cli datasources download <datasourceId> -o ./data/
|
|
|
132
132
|
tableau-cli ds download <datasourceId> -o ./data/ --to parquet
|
|
133
133
|
tableau-cli ds download <datasourceId> -o ./data/ --to csv
|
|
134
134
|
|
|
135
|
-
# Get field metadata (VizQL Data Service + Metadata API enrichment)
|
|
135
|
+
# Get field metadata (VizQL Data Service + Metadata API enrichment).
|
|
136
|
+
# On Tableau >= 2025.3 the result also includes the datasource model:
|
|
137
|
+
# logical tables and their relationships.
|
|
136
138
|
tableau-cli datasources metadata <luid>
|
|
137
139
|
|
|
138
140
|
# Query datasource data (VizQL Data Service)
|
|
@@ -143,21 +145,33 @@ tableau-cli ds query <luid> --query '{"fields": [...]}' --limit 100
|
|
|
143
145
|
### Views
|
|
144
146
|
|
|
145
147
|
```bash
|
|
146
|
-
# List views
|
|
148
|
+
# List views (includes upstream datasources via Metadata API enrichment)
|
|
147
149
|
tableau-cli views list
|
|
148
150
|
tableau-cli views list --filter "name:has:Dashboard" --format table
|
|
149
151
|
|
|
152
|
+
# Get view details: upstream datasources, workbook/project/owner info, and web URL
|
|
153
|
+
tableau-cli views get <viewId>
|
|
154
|
+
|
|
150
155
|
# Get view data as CSV
|
|
151
156
|
tableau-cli views data <viewId>
|
|
152
157
|
|
|
158
|
+
# Get view data with view filters applied
|
|
159
|
+
tableau-cli views data <viewId> --vf "Region=West" --vf "Category=Technology"
|
|
160
|
+
|
|
153
161
|
# Download view image
|
|
154
162
|
tableau-cli views image <viewId> -o dashboard.png
|
|
155
163
|
tableau-cli views image <viewId> --width 1200 --height 800 --img-format SVG -o dashboard.svg
|
|
164
|
+
tableau-cli views image <viewId> --vf "Region=West" -o west.png
|
|
156
165
|
```
|
|
157
166
|
|
|
158
167
|
### Convert
|
|
159
168
|
|
|
160
|
-
Convert local TDSX/HYPER files to Parquet or CSV.
|
|
169
|
+
Convert local TDSX/HYPER files to Parquet or CSV.
|
|
170
|
+
|
|
171
|
+
Conversion needs heavier packages (`pantab`, `polars`, `pyarrow`). You have two options:
|
|
172
|
+
|
|
173
|
+
- Install them into the CLI: `pip install tableau-cli[convert]`.
|
|
174
|
+
- Don't install anything: if [uv](https://docs.astral.sh/uv/) is on your `PATH`, conversion transparently falls back to running the step in an ephemeral `uv run --with ...` environment, so those packages never land in your host Python. The first run provisions the environment (a few seconds); later runs use uv's cache. This is the recommended path when the CLI itself was installed via `uv tool install tableau-cli`.
|
|
161
175
|
|
|
162
176
|
For most use cases, `ds download --to parquet` (or `--to csv`) is simpler — it downloads and converts in one step. The `convert` command is useful when you already have a `.tdsx` or `.hyper` file on disk.
|
|
163
177
|
|
|
@@ -177,14 +191,23 @@ tableau-cli convert extract.hyper --to csv -o ./output/result.csv
|
|
|
177
191
|
### Workbooks
|
|
178
192
|
|
|
179
193
|
```bash
|
|
180
|
-
# List workbooks
|
|
194
|
+
# List workbooks (includes upstream datasources via Metadata API enrichment)
|
|
181
195
|
tableau-cli workbooks list
|
|
182
196
|
tableau-cli wb list --filter "name:eq:Finance" --format table
|
|
183
197
|
|
|
184
|
-
# Get workbook details (includes views with usage statistics
|
|
198
|
+
# Get workbook details (includes views with usage statistics,
|
|
199
|
+
# upstream datasources, and the default view's web URL)
|
|
185
200
|
tableau-cli workbooks get <workbookId>
|
|
186
201
|
```
|
|
187
202
|
|
|
203
|
+
### Projects
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# List projects
|
|
207
|
+
tableau-cli projects list
|
|
208
|
+
tableau-cli projects list --filter "topLevelProject:eq:true" --format table
|
|
209
|
+
```
|
|
210
|
+
|
|
188
211
|
### Config
|
|
189
212
|
|
|
190
213
|
```bash
|
|
@@ -237,10 +260,11 @@ Error types include: `authentication-error`, `feature-disabled`, `tableau-api-er
|
|
|
237
260
|
| Area | APIs Used |
|
|
238
261
|
|------|-----------|
|
|
239
262
|
| Authentication | REST API v3.24 — PAT sign-in / sign-out |
|
|
240
|
-
| Datasources | REST API (list, download) + VizQL Data Service (metadata, query) + Metadata API (GraphQL enrichment) |
|
|
241
|
-
| Views | REST API (list, data, image) |
|
|
242
|
-
| Workbooks | REST API (list, get with view enrichment) |
|
|
243
|
-
|
|
|
263
|
+
| Datasources | REST API (list, download) + VizQL Data Service (metadata, model, query) + Metadata API (GraphQL enrichment) |
|
|
264
|
+
| Views | REST API (list, get, data, image with view filters) + Metadata API (lineage enrichment) |
|
|
265
|
+
| Workbooks | REST API (list, get with view enrichment) + Metadata API (lineage enrichment) |
|
|
266
|
+
| Projects | REST API (list) |
|
|
267
|
+
| Search | Content Exploration API + Metadata API (lineage enrichment) |
|
|
244
268
|
| Convert | Local file conversion: TDSX/HYPER → Parquet/CSV (optional dependencies) |
|
|
245
269
|
|
|
246
270
|
## Development
|
|
@@ -102,7 +102,9 @@ tableau-cli datasources download <datasourceId> -o ./data/
|
|
|
102
102
|
tableau-cli ds download <datasourceId> -o ./data/ --to parquet
|
|
103
103
|
tableau-cli ds download <datasourceId> -o ./data/ --to csv
|
|
104
104
|
|
|
105
|
-
# Get field metadata (VizQL Data Service + Metadata API enrichment)
|
|
105
|
+
# Get field metadata (VizQL Data Service + Metadata API enrichment).
|
|
106
|
+
# On Tableau >= 2025.3 the result also includes the datasource model:
|
|
107
|
+
# logical tables and their relationships.
|
|
106
108
|
tableau-cli datasources metadata <luid>
|
|
107
109
|
|
|
108
110
|
# Query datasource data (VizQL Data Service)
|
|
@@ -113,21 +115,33 @@ tableau-cli ds query <luid> --query '{"fields": [...]}' --limit 100
|
|
|
113
115
|
### Views
|
|
114
116
|
|
|
115
117
|
```bash
|
|
116
|
-
# List views
|
|
118
|
+
# List views (includes upstream datasources via Metadata API enrichment)
|
|
117
119
|
tableau-cli views list
|
|
118
120
|
tableau-cli views list --filter "name:has:Dashboard" --format table
|
|
119
121
|
|
|
122
|
+
# Get view details: upstream datasources, workbook/project/owner info, and web URL
|
|
123
|
+
tableau-cli views get <viewId>
|
|
124
|
+
|
|
120
125
|
# Get view data as CSV
|
|
121
126
|
tableau-cli views data <viewId>
|
|
122
127
|
|
|
128
|
+
# Get view data with view filters applied
|
|
129
|
+
tableau-cli views data <viewId> --vf "Region=West" --vf "Category=Technology"
|
|
130
|
+
|
|
123
131
|
# Download view image
|
|
124
132
|
tableau-cli views image <viewId> -o dashboard.png
|
|
125
133
|
tableau-cli views image <viewId> --width 1200 --height 800 --img-format SVG -o dashboard.svg
|
|
134
|
+
tableau-cli views image <viewId> --vf "Region=West" -o west.png
|
|
126
135
|
```
|
|
127
136
|
|
|
128
137
|
### Convert
|
|
129
138
|
|
|
130
|
-
Convert local TDSX/HYPER files to Parquet or CSV.
|
|
139
|
+
Convert local TDSX/HYPER files to Parquet or CSV.
|
|
140
|
+
|
|
141
|
+
Conversion needs heavier packages (`pantab`, `polars`, `pyarrow`). You have two options:
|
|
142
|
+
|
|
143
|
+
- Install them into the CLI: `pip install tableau-cli[convert]`.
|
|
144
|
+
- Don't install anything: if [uv](https://docs.astral.sh/uv/) is on your `PATH`, conversion transparently falls back to running the step in an ephemeral `uv run --with ...` environment, so those packages never land in your host Python. The first run provisions the environment (a few seconds); later runs use uv's cache. This is the recommended path when the CLI itself was installed via `uv tool install tableau-cli`.
|
|
131
145
|
|
|
132
146
|
For most use cases, `ds download --to parquet` (or `--to csv`) is simpler — it downloads and converts in one step. The `convert` command is useful when you already have a `.tdsx` or `.hyper` file on disk.
|
|
133
147
|
|
|
@@ -147,14 +161,23 @@ tableau-cli convert extract.hyper --to csv -o ./output/result.csv
|
|
|
147
161
|
### Workbooks
|
|
148
162
|
|
|
149
163
|
```bash
|
|
150
|
-
# List workbooks
|
|
164
|
+
# List workbooks (includes upstream datasources via Metadata API enrichment)
|
|
151
165
|
tableau-cli workbooks list
|
|
152
166
|
tableau-cli wb list --filter "name:eq:Finance" --format table
|
|
153
167
|
|
|
154
|
-
# Get workbook details (includes views with usage statistics
|
|
168
|
+
# Get workbook details (includes views with usage statistics,
|
|
169
|
+
# upstream datasources, and the default view's web URL)
|
|
155
170
|
tableau-cli workbooks get <workbookId>
|
|
156
171
|
```
|
|
157
172
|
|
|
173
|
+
### Projects
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# List projects
|
|
177
|
+
tableau-cli projects list
|
|
178
|
+
tableau-cli projects list --filter "topLevelProject:eq:true" --format table
|
|
179
|
+
```
|
|
180
|
+
|
|
158
181
|
### Config
|
|
159
182
|
|
|
160
183
|
```bash
|
|
@@ -207,10 +230,11 @@ Error types include: `authentication-error`, `feature-disabled`, `tableau-api-er
|
|
|
207
230
|
| Area | APIs Used |
|
|
208
231
|
|------|-----------|
|
|
209
232
|
| Authentication | REST API v3.24 — PAT sign-in / sign-out |
|
|
210
|
-
| Datasources | REST API (list, download) + VizQL Data Service (metadata, query) + Metadata API (GraphQL enrichment) |
|
|
211
|
-
| Views | REST API (list, data, image) |
|
|
212
|
-
| Workbooks | REST API (list, get with view enrichment) |
|
|
213
|
-
|
|
|
233
|
+
| Datasources | REST API (list, download) + VizQL Data Service (metadata, model, query) + Metadata API (GraphQL enrichment) |
|
|
234
|
+
| Views | REST API (list, get, data, image with view filters) + Metadata API (lineage enrichment) |
|
|
235
|
+
| Workbooks | REST API (list, get with view enrichment) + Metadata API (lineage enrichment) |
|
|
236
|
+
| Projects | REST API (list) |
|
|
237
|
+
| Search | Content Exploration API + Metadata API (lineage enrichment) |
|
|
214
238
|
| Convert | Local file conversion: TDSX/HYPER → Parquet/CSV (optional dependencies) |
|
|
215
239
|
|
|
216
240
|
## Development
|
|
@@ -33,13 +33,14 @@ Before running any `tableau-cli` command, load [references/cli.md](references/cl
|
|
|
33
33
|
The following requests can be handled directly with CLI commands. Load [references/cli.md](references/cli.md) and execute:
|
|
34
34
|
|
|
35
35
|
- Search content across types — `search`
|
|
36
|
-
- List/filter datasources, views, or
|
|
36
|
+
- List/filter datasources, views, workbooks, or projects — `ds list` / `views list` / `wb list` / `projects list`
|
|
37
37
|
- Download a datasource file — `ds download` (supports `--to parquet` / `--to csv` for direct conversion)
|
|
38
|
-
- Inspect datasource field metadata — `ds metadata`
|
|
38
|
+
- Inspect datasource field metadata and table relationships — `ds metadata`
|
|
39
39
|
- Query data from a datasource — `ds query`
|
|
40
|
-
-
|
|
41
|
-
- Export view as
|
|
42
|
-
-
|
|
40
|
+
- View details of a single view, incl. its web URL and upstream datasources — `views get`
|
|
41
|
+
- Export view data as CSV — `views data` (supports `--vf "Field=Value"` view filters)
|
|
42
|
+
- Export view as image — `views image` (supports `--vf "Field=Value"` view filters)
|
|
43
|
+
- View workbook details, its views, and its web URL — `wb get`
|
|
43
44
|
- Convert TDSX/HYPER to Parquet/CSV — `convert`
|
|
44
45
|
|
|
45
46
|
Common combined workflows:
|
|
@@ -49,3 +50,5 @@ Common combined workflows:
|
|
|
49
50
|
**Quick data extraction (no file download)**: `ds metadata` → `ds query` (requires VizQL Data Service; fall back to download + convert if unavailable)
|
|
50
51
|
|
|
51
52
|
**Export dashboard screenshot**: `views list` → `views image`
|
|
53
|
+
|
|
54
|
+
**From dashboard to data**: `search` or `views get` → `upstreamDatasources[].luid` → `ds metadata` → `ds query`
|
|
@@ -40,6 +40,8 @@ tableau-cli search --format table
|
|
|
40
40
|
|
|
41
41
|
View results include `parentWorkbookName` (parent workbook) and `totalViewCount` (total views).
|
|
42
42
|
|
|
43
|
+
Workbook and view results include `upstreamDatasources[]` (`{luid, name}`) — the published datasources they are built on. Use this to jump from a dashboard to `ds metadata` / `ds query` without a separate lookup.
|
|
44
|
+
|
|
43
45
|
---
|
|
44
46
|
|
|
45
47
|
## datasources (alias: ds)
|
|
@@ -94,10 +96,11 @@ tableau-cli ds metadata <luid> --format table
|
|
|
94
96
|
|
|
95
97
|
Returns:
|
|
96
98
|
- `datasourceDescription` — datasource description
|
|
99
|
+
- `datasourceModel` — logical tables and their relationships (join fields and operators); only present on Tableau >= 2025.3
|
|
97
100
|
- `fieldGroups[].fields[]` — field list (name, dataType, role, formula, description, etc.)
|
|
98
101
|
- `parameters[]` — parameter list (name, parameterType, dataType, value, etc.)
|
|
99
102
|
|
|
100
|
-
Attempts to enrich via GraphQL Metadata API; falls back to base VizQL metadata if unavailable.
|
|
103
|
+
Attempts to enrich via GraphQL Metadata API; falls back to base VizQL metadata if unavailable. Use `datasourceModel` to understand multi-table datasources before constructing `ds query` requests.
|
|
101
104
|
|
|
102
105
|
### ds query
|
|
103
106
|
|
|
@@ -137,7 +140,22 @@ tableau-cli views list --filter "name:has:keyword" --format table
|
|
|
137
140
|
| `--limit` | int | none | Max total results |
|
|
138
141
|
| `--format` | json / table | json | Output format |
|
|
139
142
|
|
|
140
|
-
Results include `
|
|
143
|
+
Results include the parent `workbook` (with name), `project`, `owner`, `upstreamDatasources[]` (published datasources the view is built on), and `totalViewCount`.
|
|
144
|
+
|
|
145
|
+
### views get
|
|
146
|
+
|
|
147
|
+
View details for a single view, including its web URL.
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
tableau-cli views get <view-id>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Option | Type | Default | Description |
|
|
154
|
+
|--------|------|---------|-------------|
|
|
155
|
+
| `view_id` | string (positional) | required | View LUID |
|
|
156
|
+
| `--format` | json / table | json | Output format |
|
|
157
|
+
|
|
158
|
+
Returns the view with `upstreamDatasources[]`, parent `workbook` / `project` / `owner` info, usage statistics, and `url` — the web URL to open the view in a browser.
|
|
141
159
|
|
|
142
160
|
### views data
|
|
143
161
|
|
|
@@ -145,11 +163,13 @@ Export view data as CSV.
|
|
|
145
163
|
|
|
146
164
|
```bash
|
|
147
165
|
tableau-cli views data <view-id>
|
|
166
|
+
tableau-cli views data <view-id> --vf "Region=West" --vf "Category=Technology"
|
|
148
167
|
```
|
|
149
168
|
|
|
150
169
|
| Option | Type | Description |
|
|
151
170
|
|--------|------|-------------|
|
|
152
171
|
| `view_id` | string (positional) | View LUID |
|
|
172
|
+
| `--vf` | `Field=Value` (repeatable) | View filter: filter the view by field values before export |
|
|
153
173
|
|
|
154
174
|
Output: CSV text written directly to stdout (not JSON).
|
|
155
175
|
|
|
@@ -161,6 +181,7 @@ Export view as image.
|
|
|
161
181
|
tableau-cli views image <view-id> -o output.png
|
|
162
182
|
tableau-cli views image <view-id> --width 1200 --height 800 -o output.png
|
|
163
183
|
tableau-cli views image <view-id> --img-format SVG -o output.svg
|
|
184
|
+
tableau-cli views image <view-id> --vf "Region=West" -o west.png
|
|
164
185
|
```
|
|
165
186
|
|
|
166
187
|
| Option | Type | Default | Description |
|
|
@@ -169,6 +190,7 @@ tableau-cli views image <view-id> --img-format SVG -o output.svg
|
|
|
169
190
|
| `--width` | int | none | Image width in pixels |
|
|
170
191
|
| `--height` | int | none | Image height in pixels |
|
|
171
192
|
| `--img-format` | PNG / SVG | PNG | Image format |
|
|
193
|
+
| `--vf` | `Field=Value` (repeatable) | none | View filter: filter the view by field values before rendering |
|
|
172
194
|
| `-o, --output` | path | none | Output file path |
|
|
173
195
|
|
|
174
196
|
With `-o`: outputs `{"filePath": "<absolute path>"}`. Without `-o`: base64-encoded image to stdout.
|
|
@@ -191,6 +213,8 @@ tableau-cli wb list --filter "name:has:keyword" --format table
|
|
|
191
213
|
| `--limit` | int | none | Max total results |
|
|
192
214
|
| `--format` | json / table | json | Output format |
|
|
193
215
|
|
|
216
|
+
Results include `upstreamDatasources[]` (`{luid, name}`) — the published datasources each workbook is built on.
|
|
217
|
+
|
|
194
218
|
### wb get
|
|
195
219
|
|
|
196
220
|
View workbook details, including all views with usage statistics.
|
|
@@ -205,6 +229,29 @@ tableau-cli wb get <workbook-id> --format table
|
|
|
205
229
|
| `workbook_id` | string (positional) | required | Workbook LUID |
|
|
206
230
|
| `--format` | json / table | json | Output format |
|
|
207
231
|
|
|
232
|
+
Returns the workbook with `upstreamDatasources[]` and `url` — the web URL to open the workbook's default view in a browser.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## projects
|
|
237
|
+
|
|
238
|
+
### projects list
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
tableau-cli projects list
|
|
242
|
+
tableau-cli projects list --filter "topLevelProject:eq:true" --format table
|
|
243
|
+
tableau-cli projects list --filter "parentProjectId:eq:<project-id>"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
| Option | Type | Default | Description |
|
|
247
|
+
|--------|------|---------|-------------|
|
|
248
|
+
| `--filter` | string | none | Server-side filter (fields: name, parentProjectId, topLevelProject, ownerName, createdAt, updatedAt) |
|
|
249
|
+
| `--page-size` | int | none | Items per page |
|
|
250
|
+
| `--limit` | int | none | Max total results |
|
|
251
|
+
| `--format` | json / table | json | Output format |
|
|
252
|
+
|
|
253
|
+
Results include name, description, parent project, content permissions, owner, and timestamps.
|
|
254
|
+
|
|
208
255
|
---
|
|
209
256
|
|
|
210
257
|
## convert
|
|
@@ -274,9 +321,14 @@ The `--filter` option uses `field:operator:value` format:
|
|
|
274
321
|
| Inspect datasource fields | `ds metadata <luid>` |
|
|
275
322
|
| Query data from a datasource | `ds query <luid> --query '{...}'` |
|
|
276
323
|
| Find a dashboard/view | `views list --filter "name:has:..."` |
|
|
324
|
+
| See view details / get view link | `views get <view-id>` |
|
|
325
|
+
| Find the datasource behind a dashboard | `views get <view-id>` → `upstreamDatasources` |
|
|
277
326
|
| Export view data | `views data <view-id>` |
|
|
327
|
+
| Export filtered view data | `views data <view-id> --vf "Field=Value"` |
|
|
278
328
|
| Export view screenshot | `views image <view-id> -o file.png` |
|
|
329
|
+
| Export filtered view screenshot | `views image <view-id> --vf "Field=Value" -o file.png` |
|
|
279
330
|
| Find workbooks | `wb list --filter "name:has:..."` |
|
|
280
|
-
| See views in a workbook | `wb get <workbook-id>` |
|
|
331
|
+
| See views in a workbook / get workbook link | `wb get <workbook-id>` |
|
|
332
|
+
| Browse projects / site structure | `projects list` |
|
|
281
333
|
| Convert local TDSX to Parquet | `convert file.tdsx -o dir/` |
|
|
282
334
|
| Download and convert for analysis | `ds download <id> --to parquet` |
|
|
@@ -89,6 +89,14 @@ class RestApi:
|
|
|
89
89
|
self._client.post(url, headers=self._auth_headers())
|
|
90
90
|
self._creds = None
|
|
91
91
|
|
|
92
|
+
# ── Server (REST API) ───────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
def get_server_info(self) -> dict[str, Any]:
|
|
95
|
+
url = f"{self._base_url}/serverinfo"
|
|
96
|
+
resp = self._client.get(url, headers=self._auth_headers())
|
|
97
|
+
resp.raise_for_status()
|
|
98
|
+
return resp.json()["serverInfo"]
|
|
99
|
+
|
|
92
100
|
# ── Datasources (REST API) ──────────────────────────────────────
|
|
93
101
|
|
|
94
102
|
def list_datasources(
|
|
@@ -167,9 +175,17 @@ class RestApi:
|
|
|
167
175
|
"views": [_shape_view(v) for v in raw_list],
|
|
168
176
|
}
|
|
169
177
|
|
|
170
|
-
def
|
|
178
|
+
def get_view(self, *, view_id: str, site_id: str) -> dict[str, Any]:
|
|
179
|
+
url = f"{self._base_url}/sites/{site_id}/views/{view_id}"
|
|
180
|
+
params = {"includeUsageStatistics": "true"}
|
|
181
|
+
resp = self._client.get(url, params=params, headers=self._auth_headers())
|
|
182
|
+
resp.raise_for_status()
|
|
183
|
+
return _shape_view(resp.json()["view"])
|
|
184
|
+
|
|
185
|
+
def query_view_data(self, *, view_id: str, site_id: str, view_filters: dict[str, str] | None = None) -> str:
|
|
171
186
|
url = f"{self._base_url}/sites/{site_id}/views/{view_id}/data"
|
|
172
|
-
|
|
187
|
+
params = _view_filter_params(view_filters)
|
|
188
|
+
resp = self._client.get(url, params=params, headers=self._auth_headers())
|
|
173
189
|
resp.raise_for_status()
|
|
174
190
|
return resp.text
|
|
175
191
|
|
|
@@ -181,6 +197,7 @@ class RestApi:
|
|
|
181
197
|
width: int | None = None,
|
|
182
198
|
height: int | None = None,
|
|
183
199
|
format_: str = "PNG",
|
|
200
|
+
view_filters: dict[str, str] | None = None,
|
|
184
201
|
) -> bytes:
|
|
185
202
|
url = f"{self._base_url}/sites/{site_id}/views/{view_id}/image"
|
|
186
203
|
params: dict[str, Any] = {"resolution": "high"}
|
|
@@ -190,6 +207,7 @@ class RestApi:
|
|
|
190
207
|
params["vizHeight"] = height
|
|
191
208
|
if format_:
|
|
192
209
|
params["format"] = format_
|
|
210
|
+
params.update(_view_filter_params(view_filters))
|
|
193
211
|
resp = self._client.get(url, params=params, headers=self._auth_headers())
|
|
194
212
|
if resp.status_code >= 400:
|
|
195
213
|
error_data: dict[str, Any] = {}
|
|
@@ -264,6 +282,33 @@ class RestApi:
|
|
|
264
282
|
"workbooks": [_shape_workbook(w) for w in raw_list],
|
|
265
283
|
}
|
|
266
284
|
|
|
285
|
+
# ── Projects (REST API) ─────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
def query_projects(
|
|
288
|
+
self,
|
|
289
|
+
*,
|
|
290
|
+
site_id: str,
|
|
291
|
+
filter_: str = "",
|
|
292
|
+
page_size: int | None = None,
|
|
293
|
+
page_number: int | None = None,
|
|
294
|
+
) -> dict[str, Any]:
|
|
295
|
+
url = f"{self._base_url}/sites/{site_id}/projects"
|
|
296
|
+
params: dict[str, Any] = {}
|
|
297
|
+
if filter_:
|
|
298
|
+
params["filter"] = filter_
|
|
299
|
+
if page_size is not None:
|
|
300
|
+
params["pageSize"] = page_size
|
|
301
|
+
if page_number is not None:
|
|
302
|
+
params["pageNumber"] = page_number
|
|
303
|
+
resp = self._client.get(url, params=params, headers=self._auth_headers())
|
|
304
|
+
resp.raise_for_status()
|
|
305
|
+
data = resp.json()
|
|
306
|
+
raw_list = data.get("projects", {}).get("project") or []
|
|
307
|
+
return {
|
|
308
|
+
"pagination": _parse_pagination(data["pagination"]),
|
|
309
|
+
"projects": [_shape_project_detail(p) for p in raw_list],
|
|
310
|
+
}
|
|
311
|
+
|
|
267
312
|
# ── Content Exploration (Search) ────────────────────────────────
|
|
268
313
|
|
|
269
314
|
def search_content(
|
|
@@ -305,6 +350,18 @@ class RestApi:
|
|
|
305
350
|
resp.raise_for_status()
|
|
306
351
|
return resp.json()
|
|
307
352
|
|
|
353
|
+
def get_datasource_model(self, *, datasource_luid: str) -> dict[str, Any]:
|
|
354
|
+
url = f"{self._host}/api/v1/vizql-data-service/get-datasource-model"
|
|
355
|
+
body = {"datasource": {"datasourceLuid": datasource_luid}}
|
|
356
|
+
try:
|
|
357
|
+
resp = self._client.post(url, json=body, headers=self._auth_headers())
|
|
358
|
+
except httpx.TransportError as exc:
|
|
359
|
+
raise FeatureDisabledError(VDS_DISABLED_MESSAGE) from exc
|
|
360
|
+
if resp.status_code == 404:
|
|
361
|
+
raise FeatureDisabledError(VDS_DISABLED_MESSAGE)
|
|
362
|
+
resp.raise_for_status()
|
|
363
|
+
return resp.json()
|
|
364
|
+
|
|
308
365
|
def query_datasource(self, *, datasource_luid: str, query: dict, options: dict | None = None) -> dict[str, Any]:
|
|
309
366
|
url = f"{self._host}/api/v1/vizql-data-service/query-datasource"
|
|
310
367
|
body: dict[str, Any] = {
|
|
@@ -350,6 +407,13 @@ def _parse_pagination(p: dict[str, Any]) -> dict[str, int]:
|
|
|
350
407
|
}
|
|
351
408
|
|
|
352
409
|
|
|
410
|
+
def _view_filter_params(view_filters: dict[str, str] | None) -> dict[str, str]:
|
|
411
|
+
"""Map view filter field names to `vf_`-prefixed query parameters."""
|
|
412
|
+
if not view_filters:
|
|
413
|
+
return {}
|
|
414
|
+
return {(key if key.startswith("vf_") else f"vf_{key}"): value for key, value in view_filters.items()}
|
|
415
|
+
|
|
416
|
+
|
|
353
417
|
# ── Response shaping (equivalent to Zod schemas in TS version) ──────────
|
|
354
418
|
|
|
355
419
|
|
|
@@ -359,6 +423,31 @@ def _shape_project(raw: Any) -> dict[str, str]:
|
|
|
359
423
|
return {"name": raw.get("name", ""), "id": raw.get("id", "")}
|
|
360
424
|
|
|
361
425
|
|
|
426
|
+
def _shape_project_detail(raw: dict[str, Any]) -> dict[str, Any]:
|
|
427
|
+
"""Match TS projectSchema: {id, name, description?, parentProjectId?, contentPermissions?, ...}"""
|
|
428
|
+
result: dict[str, Any] = {
|
|
429
|
+
"id": raw.get("id", ""),
|
|
430
|
+
"name": raw.get("name", ""),
|
|
431
|
+
}
|
|
432
|
+
for key in (
|
|
433
|
+
"description",
|
|
434
|
+
"parentProjectId",
|
|
435
|
+
"contentPermissions",
|
|
436
|
+
"controllingPermissionsProjectId",
|
|
437
|
+
"createdAt",
|
|
438
|
+
"updatedAt",
|
|
439
|
+
):
|
|
440
|
+
if key in raw:
|
|
441
|
+
result[key] = raw[key]
|
|
442
|
+
# z.coerce.boolean() — convert string "true"/"false" to bool
|
|
443
|
+
if "topLevelProject" in raw:
|
|
444
|
+
top_level = raw["topLevelProject"]
|
|
445
|
+
result["topLevelProject"] = top_level.lower() == "true" if isinstance(top_level, str) else bool(top_level)
|
|
446
|
+
if isinstance(raw.get("owner"), dict) and raw["owner"].get("id"):
|
|
447
|
+
result["owner"] = {"id": raw["owner"]["id"]}
|
|
448
|
+
return result
|
|
449
|
+
|
|
450
|
+
|
|
362
451
|
def _shape_tags(raw: Any) -> dict[str, Any]:
|
|
363
452
|
if not isinstance(raw, dict):
|
|
364
453
|
return {}
|
|
@@ -382,10 +471,11 @@ def _shape_datasource(raw: dict[str, Any]) -> dict[str, Any]:
|
|
|
382
471
|
|
|
383
472
|
|
|
384
473
|
def _shape_view(raw: dict[str, Any]) -> dict[str, Any]:
|
|
385
|
-
"""Match TS viewSchema: {id, name, createdAt, updatedAt, workbook?, owner?, project?, tags, usage?}"""
|
|
474
|
+
"""Match TS viewSchema: {id, name, contentUrl, createdAt, updatedAt, workbook?, owner?, project?, tags, usage?}"""
|
|
386
475
|
result: dict[str, Any] = {
|
|
387
476
|
"id": raw.get("id", ""),
|
|
388
477
|
"name": raw.get("name", ""),
|
|
478
|
+
"contentUrl": raw.get("contentUrl", ""),
|
|
389
479
|
"createdAt": raw.get("createdAt", ""),
|
|
390
480
|
"updatedAt": raw.get("updatedAt", ""),
|
|
391
481
|
}
|
|
@@ -87,10 +87,13 @@ def _check_for_update() -> str | None:
|
|
|
87
87
|
resp = httpx.get("https://pypi.org/pypi/tableau-cli/json", timeout=3)
|
|
88
88
|
resp.raise_for_status()
|
|
89
89
|
latest = resp.json()["info"]["version"]
|
|
90
|
-
|
|
90
|
+
|
|
91
|
+
def version_key(version: str) -> tuple[int, ...]:
|
|
92
|
+
return tuple(int(part) for part in version.split("."))
|
|
93
|
+
|
|
94
|
+
if version_key(latest) > version_key(current):
|
|
91
95
|
return (
|
|
92
|
-
f
|
|
93
|
-
'Run `pip install --upgrade "tableau-cli[convert]"` to update.'
|
|
96
|
+
f'Update available: {current} → {latest}. Run `pip install --upgrade "tableau-cli[convert]"` to update.'
|
|
94
97
|
)
|
|
95
98
|
except Exception:
|
|
96
99
|
pass
|
|
@@ -120,7 +123,7 @@ class TableauCli(click.Group):
|
|
|
120
123
|
|
|
121
124
|
|
|
122
125
|
@click.group(cls=TableauCli)
|
|
123
|
-
@click.version_option("0.1.
|
|
126
|
+
@click.version_option("0.1.4")
|
|
124
127
|
def cli():
|
|
125
128
|
"""CLI tool for interacting with Tableau Server/Cloud."""
|
|
126
129
|
|
|
@@ -129,6 +132,7 @@ def main() -> None:
|
|
|
129
132
|
from .commands.config_cmd import config_group
|
|
130
133
|
from .commands.convert_cmd import convert_command
|
|
131
134
|
from .commands.datasources_cmd import datasources_group
|
|
135
|
+
from .commands.projects_cmd import projects_group
|
|
132
136
|
from .commands.search_cmd import search_command
|
|
133
137
|
from .commands.views_cmd import views_group
|
|
134
138
|
from .commands.workbooks_cmd import workbooks_group
|
|
@@ -137,6 +141,7 @@ def main() -> None:
|
|
|
137
141
|
cli.add_command(convert_command)
|
|
138
142
|
cli.add_command(datasources_group)
|
|
139
143
|
cli.add_command(datasources_group, name="ds") # alias
|
|
144
|
+
cli.add_command(projects_group)
|
|
140
145
|
cli.add_command(views_group)
|
|
141
146
|
cli.add_command(workbooks_group)
|
|
142
147
|
cli.add_command(workbooks_group, name="wb") # alias
|
|
@@ -11,10 +11,8 @@ from ..errors.cli_error import CliError
|
|
|
11
11
|
from ..output.format import output
|
|
12
12
|
from ..utils.convert import (
|
|
13
13
|
SUPPORTED_FORMATS,
|
|
14
|
-
check_convert_deps,
|
|
15
14
|
extract_hyper_from_tdsx,
|
|
16
|
-
|
|
17
|
-
write_df,
|
|
15
|
+
run_conversion,
|
|
18
16
|
)
|
|
19
17
|
|
|
20
18
|
|
|
@@ -26,8 +24,6 @@ from ..utils.convert import (
|
|
|
26
24
|
@click.option("-o", "--output", "output_path", default=None, help="Output file or directory (default: same as input)")
|
|
27
25
|
def convert_command(input_path, to_fmt, output_path):
|
|
28
26
|
"""Convert TDSX/HYPER files to Parquet or CSV format."""
|
|
29
|
-
check_convert_deps()
|
|
30
|
-
|
|
31
27
|
input_p = Path(input_path)
|
|
32
28
|
suffix = input_p.suffix.lower()
|
|
33
29
|
|
|
@@ -47,15 +43,13 @@ def convert_command(input_path, to_fmt, output_path):
|
|
|
47
43
|
else:
|
|
48
44
|
out_p = Path(output_path)
|
|
49
45
|
|
|
50
|
-
#
|
|
46
|
+
# Convert (in-process if deps present, else via an ephemeral uv environment)
|
|
51
47
|
if suffix == ".hyper":
|
|
52
|
-
|
|
48
|
+
run_conversion(input_p, out_p, to_fmt)
|
|
53
49
|
else:
|
|
54
50
|
with TemporaryDirectory() as td:
|
|
55
51
|
hyper_path = extract_hyper_from_tdsx(input_p, Path(td))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
write_df(df, out_p, to_fmt)
|
|
52
|
+
run_conversion(hyper_path, out_p, to_fmt)
|
|
59
53
|
|
|
60
54
|
abs_path = os.path.abspath(out_p)
|
|
61
55
|
sys.stderr.write(f"Converted to {abs_path}\n")
|
|
@@ -65,9 +65,9 @@ def datasources_download(datasource_id, output_path, to_fmt):
|
|
|
65
65
|
config = resolve_config()
|
|
66
66
|
|
|
67
67
|
if to_fmt != "tdsx":
|
|
68
|
-
from ..utils.convert import
|
|
68
|
+
from ..utils.convert import ensure_convert_available
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
ensure_convert_available()
|
|
71
71
|
|
|
72
72
|
data, filename = with_auth(
|
|
73
73
|
config,
|
|
@@ -104,17 +104,25 @@ def datasources_metadata(luid, fmt):
|
|
|
104
104
|
get_graphql_query,
|
|
105
105
|
simplify_read_metadata_result,
|
|
106
106
|
)
|
|
107
|
+
from ..utils.tableau_version import is_tableau_version_at_least
|
|
107
108
|
|
|
108
109
|
read_metadata_result = api.read_metadata(datasource_luid=luid)
|
|
109
110
|
|
|
111
|
+
# The datasource model (logical tables and their relationships) comes from
|
|
112
|
+
# a dedicated VizQL Data Service endpoint that requires Tableau >= 2025.3.
|
|
113
|
+
datasource_model_result = None
|
|
114
|
+
product_version = api.get_server_info().get("productVersion", {}).get("value", "")
|
|
115
|
+
if is_tableau_version_at_least(product_version, "2025.3.0"):
|
|
116
|
+
datasource_model_result = api.get_datasource_model(datasource_luid=luid)
|
|
117
|
+
|
|
110
118
|
# Try to enrich with Metadata API (GraphQL)
|
|
111
119
|
try:
|
|
112
120
|
graphql_result = api.graphql(get_graphql_query(luid))
|
|
113
121
|
except Exception:
|
|
114
122
|
# Metadata API may not be available
|
|
115
|
-
return simplify_read_metadata_result(read_metadata_result)
|
|
123
|
+
return simplify_read_metadata_result(read_metadata_result, datasource_model_result)
|
|
116
124
|
|
|
117
|
-
return combine_fields(read_metadata_result, graphql_result)
|
|
125
|
+
return combine_fields(read_metadata_result, graphql_result, datasource_model_result)
|
|
118
126
|
|
|
119
127
|
result = with_auth(config, fn)
|
|
120
128
|
output(result, fmt)
|