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.
Files changed (47) hide show
  1. tableau_cli-0.1.4/.claude/settings.local.json +8 -0
  2. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/PKG-INFO +34 -10
  3. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/README.md +33 -9
  4. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/pyproject.toml +1 -1
  5. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/skills/SKILL.md +8 -5
  6. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/skills/references/cli.md +55 -3
  7. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/api/client.py +93 -3
  8. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/cli.py +9 -4
  9. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/convert_cmd.py +4 -10
  10. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/datasources_cmd.py +12 -4
  11. tableau_cli-0.1.4/src/tableau_cli/commands/projects_cmd.py +38 -0
  12. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/search_cmd.py +2 -1
  13. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/views_cmd.py +54 -4
  14. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/workbooks_cmd.py +10 -1
  15. tableau_cli-0.1.4/src/tableau_cli/utils/_convert_worker.py +55 -0
  16. tableau_cli-0.1.4/src/tableau_cli/utils/convert.py +177 -0
  17. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/utils/datasource_metadata_utils.py +16 -0
  18. tableau_cli-0.1.4/src/tableau_cli/utils/lineage_utils.py +216 -0
  19. tableau_cli-0.1.4/src/tableau_cli/utils/tableau_version.py +21 -0
  20. tableau_cli-0.1.4/src/tableau_cli/utils/web_url.py +33 -0
  21. tableau_cli-0.1.2/.claude/settings.local.json +0 -20
  22. tableau_cli-0.1.2/.claude/skills/tableau-cli +0 -1
  23. tableau_cli-0.1.2/skills-lock.json +0 -10
  24. tableau_cli-0.1.2/src/tableau_cli/utils/convert.py +0 -99
  25. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/.gitignore +0 -0
  26. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/LICENSE +0 -0
  27. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/NOTICE +0 -0
  28. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/skills/references/installation.md +0 -0
  29. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/__init__.py +0 -0
  30. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/api/__init__.py +0 -0
  31. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/auth/__init__.py +0 -0
  32. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/auth/with_auth.py +0 -0
  33. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/__init__.py +0 -0
  34. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/commands/config_cmd.py +0 -0
  35. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/config/__init__.py +0 -0
  36. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/config/store.py +0 -0
  37. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/config/types.py +0 -0
  38. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/errors/__init__.py +0 -0
  39. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/errors/cli_error.py +0 -0
  40. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/errors/vds_error_handler.py +0 -0
  41. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/output/__init__.py +0 -0
  42. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/output/format.py +0 -0
  43. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/output/json_output.py +0 -0
  44. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/output/table_output.py +0 -0
  45. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/utils/__init__.py +0 -0
  46. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/utils/paginate.py +0 -0
  47. {tableau_cli-0.1.2 → tableau_cli-0.1.4}/src/tableau_cli/utils/search_content_utils.py +0 -0
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(command -v uv)",
5
+ "Bash(uv --version)"
6
+ ]
7
+ }
8
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tableau-cli
3
- Version: 0.1.2
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. Requires `pip install tableau-cli[convert]`.
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
- | Search | Content Exploration API |
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. Requires `pip install tableau-cli[convert]`.
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
- | Search | Content Exploration API |
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tableau-cli"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "CLI for Tableau Server/Cloud, designed for AI agent integration"
9
9
  license = "Apache-2.0"
10
10
  requires-python = ">=3.10"
@@ -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 workbooks — `ds list` / `views list` / `wb list`
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
- - Export view data as CSV — `views data`
41
- - Export view as image — `views image`
42
- - View workbook details and its views — `wb get`
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 `workbookId` to trace back to the parent workbook.
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 query_view_data(self, *, view_id: str, site_id: str) -> str:
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
- resp = self._client.get(url, headers=self._auth_headers())
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
- if latest != current:
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"Update available: {current} → {latest}. "
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.2")
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
- read_hyper,
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
- # Read hyper data
46
+ # Convert (in-process if deps present, else via an ephemeral uv environment)
51
47
  if suffix == ".hyper":
52
- df = read_hyper(input_p)
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
- df = read_hyper(hyper_path)
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 check_convert_deps
68
+ from ..utils.convert import ensure_convert_available
69
69
 
70
- check_convert_deps()
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)