py-tbparse 0.1.1a0__tar.gz → 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.
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/AGENTS.md +7 -1
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/MANIFEST.in +1 -0
- py_tbparse-0.2.0/PKG-INFO +150 -0
- py_tbparse-0.2.0/README.md +114 -0
- py_tbparse-0.2.0/docs/gui-overview.png +0 -0
- py_tbparse-0.2.0/py_tbparse.egg-info/PKG-INFO +150 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/py_tbparse.egg-info/SOURCES.txt +2 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/pyproject.toml +6 -1
- py_tbparse-0.2.0/tests/conftest.py +56 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_batch.py +12 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_calculated_fields.py +15 -0
- py_tbparse-0.2.0/tests/test_clean.py +52 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_datasources.py +14 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_gui_browser.py +190 -14
- py_tbparse-0.2.0/tests/test_joins.py +191 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_parser.py +26 -1
- py_tbparse-0.2.0/tests/test_relationships.py +156 -0
- py_tbparse-0.2.0/tests/test_version.py +25 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_webgui.py +172 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/__init__.py +1 -1
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/_clean.py +32 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/_xml.py +35 -9
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/batch.py +1 -1
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/calculated_fields.py +8 -1
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/cli.py +1 -1
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/datasources.py +1 -1
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/joins.py +36 -10
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/parser.py +11 -8
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/relationships.py +39 -12
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/validators.py +3 -3
- py_tbparse-0.2.0/twbparser_py/webgui.py +928 -0
- py_tbparse-0.1.1a0/PKG-INFO +0 -168
- py_tbparse-0.1.1a0/README.md +0 -135
- py_tbparse-0.1.1a0/py_tbparse.egg-info/PKG-INFO +0 -168
- py_tbparse-0.1.1a0/tests/conftest.py +0 -25
- py_tbparse-0.1.1a0/tests/test_clean.py +0 -31
- py_tbparse-0.1.1a0/tests/test_joins.py +0 -74
- py_tbparse-0.1.1a0/tests/test_relationships.py +0 -62
- py_tbparse-0.1.1a0/twbparser_py/webgui.py +0 -398
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/LICENSE +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/py_tbparse.egg-info/dependency_links.txt +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/py_tbparse.egg-info/entry_points.txt +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/py_tbparse.egg-info/requires.txt +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/py_tbparse.egg-info/top_level.txt +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/setup.cfg +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/fixtures/test_for_wenjie.twb +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/fixtures/test_for_zip.twbx +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_cli.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_dashboards.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_diff.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_fields.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_graph.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_published.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_sql.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_validators.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_xml.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/_tables.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/dashboards.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/diff.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/fields.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/graph.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/published.py +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/py.typed +0 -0
- {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/sql.py +0 -0
|
@@ -220,7 +220,13 @@ the test fixture picks that directory up automatically via
|
|
|
220
220
|
`LD_LIBRARY_PATH`. It verifies each download against the checksum
|
|
221
221
|
`apt-get --print-uris` reports for it (SHA256 if offered, MD5Sum as the
|
|
222
222
|
realistic fallback — this environment's apt only emits MD5Sum) before
|
|
223
|
-
extracting; don't remove that step to "simplify" the script.
|
|
223
|
+
extracting; don't remove that step to "simplify" the script. URIs that
|
|
224
|
+
aren't plain http(s) (e.g. `mirror+file:` apt sources, which `wget` can't
|
|
225
|
+
fetch) are downloaded with `apt-get download` instead, still followed by
|
|
226
|
+
the same checksum check. It also unpacks `xkb-data` and a font, and the
|
|
227
|
+
test fixture sets `XKB_CONFIG_ROOT`/`FONTCONFIG_FILE` from them: on a host
|
|
228
|
+
with neither, Chromium silently drops all keyboard input (so `fill()`
|
|
229
|
+
leaves inputs empty and tests time out) and may crash rendering. Don't add
|
|
224
230
|
`pytest-playwright` — it's a pytest plugin that imports playwright at
|
|
225
231
|
startup, which makes collection fail for anyone who doesn't have it
|
|
226
232
|
installed.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-tbparse
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Native Python port of the twbparser R package: parse Tableau .twb/.twbx workbooks into pandas DataFrames.
|
|
5
|
+
Author: DDSNA
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/DDSNA/py-tbparse
|
|
8
|
+
Project-URL: Source, https://github.com/DDSNA/py-tbparse
|
|
9
|
+
Project-URL: Issues, https://github.com/DDSNA/py-tbparse/issues
|
|
10
|
+
Keywords: tableau,twb,twbx,workbook,parser,pandas
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Office/Business
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: lxml>=4.9
|
|
27
|
+
Requires-Dist: pandas>=1.5
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
30
|
+
Provides-Extra: browser
|
|
31
|
+
Requires-Dist: playwright>=1.40; extra == "browser"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
34
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# py-tbparse
|
|
38
|
+
|
|
39
|
+
Reads Tableau workbooks (`.twb` and `.twbx`) and gives you what's in them as pandas DataFrames: datasources, fields, calculated fields, joins, relationships, dashboards, custom SQL. It's plain Python. You don't need Tableau or R installed.
|
|
40
|
+
|
|
41
|
+
It began as a port of PrigasG's R package [twbparser](https://github.com/PrigasG/twbparser). The browser GUI, the command-line tool, workbook diffing and folder scanning are new here.
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/DDSNA/py-tbparse.git
|
|
49
|
+
cd py-tbparse
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The project got renamed but the module and commands didn't, so: import `twbparser_py`, run `twbparser` or `twbparser-gui`.
|
|
54
|
+
|
|
55
|
+
## Using it from Python
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from twbparser_py import TwbParser
|
|
59
|
+
|
|
60
|
+
p = TwbParser("workbook.twb") # or a .twbx
|
|
61
|
+
p.get_overview() # counts of everything
|
|
62
|
+
p.get_calculated_fields() # each calculation and its formula
|
|
63
|
+
p.get_relationships() # how the tables connect
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Every getter returns a DataFrame. The rest are `get_datasources`, `get_parameters`, `get_fields`, `get_raw_fields`, `get_joins`, `get_relations`, `get_inferred_relationships`, `get_dashboards`, `get_dashboard_sheets`, `get_custom_sql`, `get_initial_sql` and `get_published_refs`. `get_relationship_graph_dot()` returns the data model as Graphviz text, and `validate()` looks for problems in the relationships. In Jupyter, a bare `p` shows the overview.
|
|
67
|
+
|
|
68
|
+
Two helpers work across workbooks:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from twbparser_py import diff_workbooks, scan_folder
|
|
72
|
+
|
|
73
|
+
diff_workbooks(TwbParser("v1.twb"), TwbParser("v2.twb"), table="datasources")
|
|
74
|
+
scan_folder("./workbooks", table="datasources") # one table, every workbook in the folder
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `.twbx` files
|
|
78
|
+
|
|
79
|
+
A `.twbx` is read directly from the zip and nothing gets written to disk. That means `p.twbx_dir` is `None`, and `p.path` is a made-up `<file>.twbx/<name>.twb` that you can't open. If you want the files out, extract them yourself:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from twbparser_py import extract_twb_from_twbx, twbx_extract_files
|
|
83
|
+
|
|
84
|
+
extract_twb_from_twbx("workbook.twbx", extract_dir="out/")
|
|
85
|
+
twbx_extract_files("workbook.twbx", exdir="out/") # everything in the package
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Command line
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
twbparser workbook.twb # overview
|
|
92
|
+
twbparser workbook.twb tables # list the tables
|
|
93
|
+
twbparser workbook.twb calculated-fields
|
|
94
|
+
twbparser workbook.twb fields --format csv -o fields.csv
|
|
95
|
+
twbparser workbook.twbx dashboard-sheets --dashboard "Sales Overview"
|
|
96
|
+
twbparser workbook.twb validate # exit code 2 if it finds a problem
|
|
97
|
+
twbparser workbook.twb graph > model.dot # --include-inferred adds the guessed links
|
|
98
|
+
twbparser diff old.twb new.twb datasources
|
|
99
|
+
twbparser batch ./workbooks datasources
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Tables: `overview`, `datasources`, `parameters`, `fields`, `raw-fields`, `calculated-fields`, `joins`, `relations`, `relationships`, `inferred-relationships`, `dashboards`, `dashboard-sheets`, `custom-sql`, `initial-sql`, `published-refs`.
|
|
103
|
+
|
|
104
|
+
`--format` takes `table` (default), `csv` or `json`. `graph` always prints Graphviz text. `diff` and `batch` accept the same table names except `graph`, `validate` and `tables`.
|
|
105
|
+
|
|
106
|
+
## GUI
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
twbparser-gui workbook.twb # opens your browser with it loaded
|
|
110
|
+
twbparser-gui # starts empty, paste a path and hit Load
|
|
111
|
+
twbparser-gui --no-browser --port 8765 # server only, for a machine with no display
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It uses only the standard library, so there's nothing more to install.
|
|
115
|
+
|
|
116
|
+
The sidebar lists every table with its row count. Click a column header to sort, type in the filter box to narrow rows (`/` jumps to it), click a row to see a long formula or SQL statement in full. The tiles on the overview open their tables. The graph view lets you copy or download the DOT text. Everything else exports as CSV. It has a dark theme and works in a narrow window.
|
|
117
|
+
|
|
118
|
+
It's meant to run on your own machine for one person. It refuses requests that come from other websites, but there's no login, so don't put it on a shared network.
|
|
119
|
+
|
|
120
|
+
## Tests
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install -e ".[test]"
|
|
124
|
+
pytest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The sample workbooks in `tests/fixtures/` come from the R package.
|
|
128
|
+
|
|
129
|
+
The GUI tests run the page in headless Chromium through Playwright and fail on any JavaScript error. They skip if the browser isn't installed. To run them:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install -e ".[test,browser]"
|
|
133
|
+
playwright install chromium # add --with-deps if you have root
|
|
134
|
+
./scripts/setup-browser-libs.sh # without root, this unpacks the system libraries locally
|
|
135
|
+
pytest tests/test_gui_browser.py
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Compared with the R package
|
|
139
|
+
|
|
140
|
+
The code follows the R original function by function, and the aim is the same output. Not everything is ported. Missing so far: formatting, tooltips, colors, axes and sorts, dashboard layout and actions, the analytics helpers (calculation complexity, field usage, replication brief) and the Shiny inspector. The GUI covers some of what the inspector did.
|
|
141
|
+
|
|
142
|
+
Where the R version has a bug, this one doesn't copy it. That currently covers joins and relationships on more than one key, nested joins, the include-parameters option, and calculations with brackets inside brackets.
|
|
143
|
+
|
|
144
|
+
## Why a Tableau parser
|
|
145
|
+
|
|
146
|
+
A `.twb` is XML and a `.twbx` is a zip containing one, so reading them from Python isn't hard. Power BI's `.pbix` is a binary format built on a proprietary storage engine, and getting into it from code took reverse-engineering projects like [PBIXRay](https://github.com/Hugoberry/pbixray) and [pbi-tools](https://github.com/pbi-tools/pbi-tools). On the server side it goes the other way: Tableau's own Python tooling ([tableauserverclient](https://pypi.org/project/tableauserverclient/) and `tabcmd`) is more mature and more open than what Microsoft has for the Power BI REST API.
|
|
147
|
+
|
|
148
|
+
## Credit
|
|
149
|
+
|
|
150
|
+
Based on [twbparser](https://github.com/PrigasG/twbparser) by George Arthur, MIT licensed.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# py-tbparse
|
|
2
|
+
|
|
3
|
+
Reads Tableau workbooks (`.twb` and `.twbx`) and gives you what's in them as pandas DataFrames: datasources, fields, calculated fields, joins, relationships, dashboards, custom SQL. It's plain Python. You don't need Tableau or R installed.
|
|
4
|
+
|
|
5
|
+
It began as a port of PrigasG's R package [twbparser](https://github.com/PrigasG/twbparser). The browser GUI, the command-line tool, workbook diffing and folder scanning are new here.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
git clone https://github.com/DDSNA/py-tbparse.git
|
|
13
|
+
cd py-tbparse
|
|
14
|
+
pip install -e .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The project got renamed but the module and commands didn't, so: import `twbparser_py`, run `twbparser` or `twbparser-gui`.
|
|
18
|
+
|
|
19
|
+
## Using it from Python
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from twbparser_py import TwbParser
|
|
23
|
+
|
|
24
|
+
p = TwbParser("workbook.twb") # or a .twbx
|
|
25
|
+
p.get_overview() # counts of everything
|
|
26
|
+
p.get_calculated_fields() # each calculation and its formula
|
|
27
|
+
p.get_relationships() # how the tables connect
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Every getter returns a DataFrame. The rest are `get_datasources`, `get_parameters`, `get_fields`, `get_raw_fields`, `get_joins`, `get_relations`, `get_inferred_relationships`, `get_dashboards`, `get_dashboard_sheets`, `get_custom_sql`, `get_initial_sql` and `get_published_refs`. `get_relationship_graph_dot()` returns the data model as Graphviz text, and `validate()` looks for problems in the relationships. In Jupyter, a bare `p` shows the overview.
|
|
31
|
+
|
|
32
|
+
Two helpers work across workbooks:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from twbparser_py import diff_workbooks, scan_folder
|
|
36
|
+
|
|
37
|
+
diff_workbooks(TwbParser("v1.twb"), TwbParser("v2.twb"), table="datasources")
|
|
38
|
+
scan_folder("./workbooks", table="datasources") # one table, every workbook in the folder
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `.twbx` files
|
|
42
|
+
|
|
43
|
+
A `.twbx` is read directly from the zip and nothing gets written to disk. That means `p.twbx_dir` is `None`, and `p.path` is a made-up `<file>.twbx/<name>.twb` that you can't open. If you want the files out, extract them yourself:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from twbparser_py import extract_twb_from_twbx, twbx_extract_files
|
|
47
|
+
|
|
48
|
+
extract_twb_from_twbx("workbook.twbx", extract_dir="out/")
|
|
49
|
+
twbx_extract_files("workbook.twbx", exdir="out/") # everything in the package
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Command line
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
twbparser workbook.twb # overview
|
|
56
|
+
twbparser workbook.twb tables # list the tables
|
|
57
|
+
twbparser workbook.twb calculated-fields
|
|
58
|
+
twbparser workbook.twb fields --format csv -o fields.csv
|
|
59
|
+
twbparser workbook.twbx dashboard-sheets --dashboard "Sales Overview"
|
|
60
|
+
twbparser workbook.twb validate # exit code 2 if it finds a problem
|
|
61
|
+
twbparser workbook.twb graph > model.dot # --include-inferred adds the guessed links
|
|
62
|
+
twbparser diff old.twb new.twb datasources
|
|
63
|
+
twbparser batch ./workbooks datasources
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Tables: `overview`, `datasources`, `parameters`, `fields`, `raw-fields`, `calculated-fields`, `joins`, `relations`, `relationships`, `inferred-relationships`, `dashboards`, `dashboard-sheets`, `custom-sql`, `initial-sql`, `published-refs`.
|
|
67
|
+
|
|
68
|
+
`--format` takes `table` (default), `csv` or `json`. `graph` always prints Graphviz text. `diff` and `batch` accept the same table names except `graph`, `validate` and `tables`.
|
|
69
|
+
|
|
70
|
+
## GUI
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
twbparser-gui workbook.twb # opens your browser with it loaded
|
|
74
|
+
twbparser-gui # starts empty, paste a path and hit Load
|
|
75
|
+
twbparser-gui --no-browser --port 8765 # server only, for a machine with no display
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
It uses only the standard library, so there's nothing more to install.
|
|
79
|
+
|
|
80
|
+
The sidebar lists every table with its row count. Click a column header to sort, type in the filter box to narrow rows (`/` jumps to it), click a row to see a long formula or SQL statement in full. The tiles on the overview open their tables. The graph view lets you copy or download the DOT text. Everything else exports as CSV. It has a dark theme and works in a narrow window.
|
|
81
|
+
|
|
82
|
+
It's meant to run on your own machine for one person. It refuses requests that come from other websites, but there's no login, so don't put it on a shared network.
|
|
83
|
+
|
|
84
|
+
## Tests
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install -e ".[test]"
|
|
88
|
+
pytest
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The sample workbooks in `tests/fixtures/` come from the R package.
|
|
92
|
+
|
|
93
|
+
The GUI tests run the page in headless Chromium through Playwright and fail on any JavaScript error. They skip if the browser isn't installed. To run them:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install -e ".[test,browser]"
|
|
97
|
+
playwright install chromium # add --with-deps if you have root
|
|
98
|
+
./scripts/setup-browser-libs.sh # without root, this unpacks the system libraries locally
|
|
99
|
+
pytest tests/test_gui_browser.py
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Compared with the R package
|
|
103
|
+
|
|
104
|
+
The code follows the R original function by function, and the aim is the same output. Not everything is ported. Missing so far: formatting, tooltips, colors, axes and sorts, dashboard layout and actions, the analytics helpers (calculation complexity, field usage, replication brief) and the Shiny inspector. The GUI covers some of what the inspector did.
|
|
105
|
+
|
|
106
|
+
Where the R version has a bug, this one doesn't copy it. That currently covers joins and relationships on more than one key, nested joins, the include-parameters option, and calculations with brackets inside brackets.
|
|
107
|
+
|
|
108
|
+
## Why a Tableau parser
|
|
109
|
+
|
|
110
|
+
A `.twb` is XML and a `.twbx` is a zip containing one, so reading them from Python isn't hard. Power BI's `.pbix` is a binary format built on a proprietary storage engine, and getting into it from code took reverse-engineering projects like [PBIXRay](https://github.com/Hugoberry/pbixray) and [pbi-tools](https://github.com/pbi-tools/pbi-tools). On the server side it goes the other way: Tableau's own Python tooling ([tableauserverclient](https://pypi.org/project/tableauserverclient/) and `tabcmd`) is more mature and more open than what Microsoft has for the Power BI REST API.
|
|
111
|
+
|
|
112
|
+
## Credit
|
|
113
|
+
|
|
114
|
+
Based on [twbparser](https://github.com/PrigasG/twbparser) by George Arthur, MIT licensed.
|
|
Binary file
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-tbparse
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Native Python port of the twbparser R package: parse Tableau .twb/.twbx workbooks into pandas DataFrames.
|
|
5
|
+
Author: DDSNA
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/DDSNA/py-tbparse
|
|
8
|
+
Project-URL: Source, https://github.com/DDSNA/py-tbparse
|
|
9
|
+
Project-URL: Issues, https://github.com/DDSNA/py-tbparse/issues
|
|
10
|
+
Keywords: tableau,twb,twbx,workbook,parser,pandas
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Office/Business
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: lxml>=4.9
|
|
27
|
+
Requires-Dist: pandas>=1.5
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
30
|
+
Provides-Extra: browser
|
|
31
|
+
Requires-Dist: playwright>=1.40; extra == "browser"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
34
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# py-tbparse
|
|
38
|
+
|
|
39
|
+
Reads Tableau workbooks (`.twb` and `.twbx`) and gives you what's in them as pandas DataFrames: datasources, fields, calculated fields, joins, relationships, dashboards, custom SQL. It's plain Python. You don't need Tableau or R installed.
|
|
40
|
+
|
|
41
|
+
It began as a port of PrigasG's R package [twbparser](https://github.com/PrigasG/twbparser). The browser GUI, the command-line tool, workbook diffing and folder scanning are new here.
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/DDSNA/py-tbparse.git
|
|
49
|
+
cd py-tbparse
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The project got renamed but the module and commands didn't, so: import `twbparser_py`, run `twbparser` or `twbparser-gui`.
|
|
54
|
+
|
|
55
|
+
## Using it from Python
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from twbparser_py import TwbParser
|
|
59
|
+
|
|
60
|
+
p = TwbParser("workbook.twb") # or a .twbx
|
|
61
|
+
p.get_overview() # counts of everything
|
|
62
|
+
p.get_calculated_fields() # each calculation and its formula
|
|
63
|
+
p.get_relationships() # how the tables connect
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Every getter returns a DataFrame. The rest are `get_datasources`, `get_parameters`, `get_fields`, `get_raw_fields`, `get_joins`, `get_relations`, `get_inferred_relationships`, `get_dashboards`, `get_dashboard_sheets`, `get_custom_sql`, `get_initial_sql` and `get_published_refs`. `get_relationship_graph_dot()` returns the data model as Graphviz text, and `validate()` looks for problems in the relationships. In Jupyter, a bare `p` shows the overview.
|
|
67
|
+
|
|
68
|
+
Two helpers work across workbooks:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from twbparser_py import diff_workbooks, scan_folder
|
|
72
|
+
|
|
73
|
+
diff_workbooks(TwbParser("v1.twb"), TwbParser("v2.twb"), table="datasources")
|
|
74
|
+
scan_folder("./workbooks", table="datasources") # one table, every workbook in the folder
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `.twbx` files
|
|
78
|
+
|
|
79
|
+
A `.twbx` is read directly from the zip and nothing gets written to disk. That means `p.twbx_dir` is `None`, and `p.path` is a made-up `<file>.twbx/<name>.twb` that you can't open. If you want the files out, extract them yourself:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from twbparser_py import extract_twb_from_twbx, twbx_extract_files
|
|
83
|
+
|
|
84
|
+
extract_twb_from_twbx("workbook.twbx", extract_dir="out/")
|
|
85
|
+
twbx_extract_files("workbook.twbx", exdir="out/") # everything in the package
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Command line
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
twbparser workbook.twb # overview
|
|
92
|
+
twbparser workbook.twb tables # list the tables
|
|
93
|
+
twbparser workbook.twb calculated-fields
|
|
94
|
+
twbparser workbook.twb fields --format csv -o fields.csv
|
|
95
|
+
twbparser workbook.twbx dashboard-sheets --dashboard "Sales Overview"
|
|
96
|
+
twbparser workbook.twb validate # exit code 2 if it finds a problem
|
|
97
|
+
twbparser workbook.twb graph > model.dot # --include-inferred adds the guessed links
|
|
98
|
+
twbparser diff old.twb new.twb datasources
|
|
99
|
+
twbparser batch ./workbooks datasources
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Tables: `overview`, `datasources`, `parameters`, `fields`, `raw-fields`, `calculated-fields`, `joins`, `relations`, `relationships`, `inferred-relationships`, `dashboards`, `dashboard-sheets`, `custom-sql`, `initial-sql`, `published-refs`.
|
|
103
|
+
|
|
104
|
+
`--format` takes `table` (default), `csv` or `json`. `graph` always prints Graphviz text. `diff` and `batch` accept the same table names except `graph`, `validate` and `tables`.
|
|
105
|
+
|
|
106
|
+
## GUI
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
twbparser-gui workbook.twb # opens your browser with it loaded
|
|
110
|
+
twbparser-gui # starts empty, paste a path and hit Load
|
|
111
|
+
twbparser-gui --no-browser --port 8765 # server only, for a machine with no display
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It uses only the standard library, so there's nothing more to install.
|
|
115
|
+
|
|
116
|
+
The sidebar lists every table with its row count. Click a column header to sort, type in the filter box to narrow rows (`/` jumps to it), click a row to see a long formula or SQL statement in full. The tiles on the overview open their tables. The graph view lets you copy or download the DOT text. Everything else exports as CSV. It has a dark theme and works in a narrow window.
|
|
117
|
+
|
|
118
|
+
It's meant to run on your own machine for one person. It refuses requests that come from other websites, but there's no login, so don't put it on a shared network.
|
|
119
|
+
|
|
120
|
+
## Tests
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install -e ".[test]"
|
|
124
|
+
pytest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The sample workbooks in `tests/fixtures/` come from the R package.
|
|
128
|
+
|
|
129
|
+
The GUI tests run the page in headless Chromium through Playwright and fail on any JavaScript error. They skip if the browser isn't installed. To run them:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install -e ".[test,browser]"
|
|
133
|
+
playwright install chromium # add --with-deps if you have root
|
|
134
|
+
./scripts/setup-browser-libs.sh # without root, this unpacks the system libraries locally
|
|
135
|
+
pytest tests/test_gui_browser.py
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Compared with the R package
|
|
139
|
+
|
|
140
|
+
The code follows the R original function by function, and the aim is the same output. Not everything is ported. Missing so far: formatting, tooltips, colors, axes and sorts, dashboard layout and actions, the analytics helpers (calculation complexity, field usage, replication brief) and the Shiny inspector. The GUI covers some of what the inspector did.
|
|
141
|
+
|
|
142
|
+
Where the R version has a bug, this one doesn't copy it. That currently covers joins and relationships on more than one key, nested joins, the include-parameters option, and calculations with brackets inside brackets.
|
|
143
|
+
|
|
144
|
+
## Why a Tableau parser
|
|
145
|
+
|
|
146
|
+
A `.twb` is XML and a `.twbx` is a zip containing one, so reading them from Python isn't hard. Power BI's `.pbix` is a binary format built on a proprietary storage engine, and getting into it from code took reverse-engineering projects like [PBIXRay](https://github.com/Hugoberry/pbixray) and [pbi-tools](https://github.com/pbi-tools/pbi-tools). On the server side it goes the other way: Tableau's own Python tooling ([tableauserverclient](https://pypi.org/project/tableauserverclient/) and `tabcmd`) is more mature and more open than what Microsoft has for the Power BI REST API.
|
|
147
|
+
|
|
148
|
+
## Credit
|
|
149
|
+
|
|
150
|
+
Based on [twbparser](https://github.com/PrigasG/twbparser) by George Arthur, MIT licensed.
|
|
@@ -3,6 +3,7 @@ LICENSE
|
|
|
3
3
|
MANIFEST.in
|
|
4
4
|
README.md
|
|
5
5
|
pyproject.toml
|
|
6
|
+
docs/gui-overview.png
|
|
6
7
|
py_tbparse.egg-info/PKG-INFO
|
|
7
8
|
py_tbparse.egg-info/SOURCES.txt
|
|
8
9
|
py_tbparse.egg-info/dependency_links.txt
|
|
@@ -26,6 +27,7 @@ tests/test_published.py
|
|
|
26
27
|
tests/test_relationships.py
|
|
27
28
|
tests/test_sql.py
|
|
28
29
|
tests/test_validators.py
|
|
30
|
+
tests/test_version.py
|
|
29
31
|
tests/test_webgui.py
|
|
30
32
|
tests/test_xml.py
|
|
31
33
|
tests/fixtures/test_for_wenjie.twb
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "py-tbparse"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Native Python port of the twbparser R package: parse Tableau .twb/.twbx workbooks into pandas DataFrames."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -30,6 +30,11 @@ dependencies = [
|
|
|
30
30
|
"pandas>=1.5",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/DDSNA/py-tbparse"
|
|
35
|
+
Source = "https://github.com/DDSNA/py-tbparse"
|
|
36
|
+
Issues = "https://github.com/DDSNA/py-tbparse/issues"
|
|
37
|
+
|
|
33
38
|
[project.optional-dependencies]
|
|
34
39
|
test = ["pytest>=7"]
|
|
35
40
|
browser = ["playwright>=1.40"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from lxml import etree
|
|
5
|
+
|
|
6
|
+
FIXTURES = Path(__file__).parent / "fixtures"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def wenjie_xml():
|
|
11
|
+
return etree.parse(str(FIXTURES / "test_for_wenjie.twb"))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def wenjie_path():
|
|
16
|
+
return str(FIXTURES / "test_for_wenjie.twb")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def zip_twbx_path():
|
|
21
|
+
return str(FIXTURES / "test_for_zip.twbx")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def xml_from_string(s: str):
|
|
25
|
+
return etree.fromstring(s.encode("utf-8"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Real Tableau workbooks give every column in the "Parameters" datasource a
|
|
29
|
+
# @param-domain-type attribute plus a <calculation> holding its value.
|
|
30
|
+
REAL_PARAMS_XML = """
|
|
31
|
+
<workbook>
|
|
32
|
+
<datasources>
|
|
33
|
+
<datasource name="Parameters" hasconnection="false" inline="true">
|
|
34
|
+
<aliases enabled="yes"/>
|
|
35
|
+
<column caption="Top N" datatype="integer" name="[Parameter 1]"
|
|
36
|
+
param-domain-type="range" role="measure" type="quantitative" value="10">
|
|
37
|
+
<calculation class="tableau" formula="10"/>
|
|
38
|
+
<range granularity="1" max="50" min="1"/>
|
|
39
|
+
</column>
|
|
40
|
+
<column caption="Region Pick" datatype="string" name="[Parameter 2]"
|
|
41
|
+
param-domain-type="list" role="measure" type="nominal" value=""East"">
|
|
42
|
+
<calculation class="tableau" formula=""East""/>
|
|
43
|
+
<members>
|
|
44
|
+
<member value=""East""/>
|
|
45
|
+
<member value=""West""/>
|
|
46
|
+
</members>
|
|
47
|
+
</column>
|
|
48
|
+
</datasource>
|
|
49
|
+
<datasource name="Orders">
|
|
50
|
+
<column name="[Amount]" caption="Amount" datatype="real" role="measure">
|
|
51
|
+
<calculation class="tableau" formula="SUM([Sales])"/>
|
|
52
|
+
</column>
|
|
53
|
+
</datasource>
|
|
54
|
+
</datasources>
|
|
55
|
+
</workbook>
|
|
56
|
+
"""
|
|
@@ -37,3 +37,15 @@ def test_scan_folder_datasources_table():
|
|
|
37
37
|
assert "workbook" in df.columns
|
|
38
38
|
assert "datasource" in df.columns
|
|
39
39
|
assert (df["workbook"] == "test_for_wenjie.twb").sum() == 2
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_scan_folder_directory_with_glob_metacharacters(tmp_path):
|
|
43
|
+
import shutil
|
|
44
|
+
import sys
|
|
45
|
+
|
|
46
|
+
# `*` can't appear in a Windows path; `[` and `]` still exercise the fix.
|
|
47
|
+
folder = tmp_path / ("[2024] Q1" if sys.platform == "win32" else "[2024] Q1 *")
|
|
48
|
+
folder.mkdir()
|
|
49
|
+
shutil.copy("tests/fixtures/test_for_wenjie.twb", folder)
|
|
50
|
+
df = scan_folder(str(folder), table="overview")
|
|
51
|
+
assert list(df["workbook"]) == ["test_for_wenjie.twb"]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from conftest import REAL_PARAMS_XML, xml_from_string
|
|
2
|
+
|
|
1
3
|
from twbparser_py import extract_calculated_fields, extract_raw_fields
|
|
2
4
|
|
|
3
5
|
|
|
@@ -18,3 +20,16 @@ def test_extract_raw_fields_excludes_calc_and_params(wenjie_xml):
|
|
|
18
20
|
assert "Calculation_2139209847776120832" not in raw["tableau_internal_name"].apply(
|
|
19
21
|
lambda x: (x or "").strip("[]")
|
|
20
22
|
).values
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_include_parameters_returns_real_parameter_columns():
|
|
26
|
+
xml = xml_from_string(REAL_PARAMS_XML)
|
|
27
|
+
|
|
28
|
+
default = extract_calculated_fields(xml)
|
|
29
|
+
assert list(default["datasource"]) == ["Orders"]
|
|
30
|
+
|
|
31
|
+
with_params = extract_calculated_fields(xml, include_parameters=True)
|
|
32
|
+
params = with_params[with_params["datasource"] == "Parameters"]
|
|
33
|
+
assert sorted(params["tableau_internal_name"]) == ["[Parameter 1]", "[Parameter 2]"]
|
|
34
|
+
assert set(params["name"]) == {"Top N", "Region Pick"}
|
|
35
|
+
assert "Orders" in with_params["datasource"].values
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from twbparser_py._clean import bracket_tokens, clean_field, clean_table, strip_brackets
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_bracket_tokens_qualified_name():
|
|
5
|
+
assert bracket_tokens("[Orders].[Customer ID]") == ["[Orders]", "[Customer ID]"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_bracket_tokens_keeps_nested_calc_whole():
|
|
9
|
+
# The old regex stopped at the first "]" and returned "[LOWER([x]".
|
|
10
|
+
assert bracket_tokens("[LOWER([x])]") == ["[LOWER([x])]"]
|
|
11
|
+
assert bracket_tokens("[IFNULL([a],[b])].[T]") == ["[IFNULL([a],[b])]", "[T]"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_bracket_tokens_escaped_close_bracket():
|
|
15
|
+
assert bracket_tokens("[Sales]]Q1].[Orders]") == ["[Sales]]Q1]", "[Orders]"]
|
|
16
|
+
assert bracket_tokens("[[x]]") == ["[[x]]"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_bracket_tokens_empty_and_unclosed():
|
|
20
|
+
assert bracket_tokens(None) == []
|
|
21
|
+
assert bracket_tokens("no brackets") == []
|
|
22
|
+
assert bracket_tokens("[open") == []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_clean_table_strips_prefix_brackets_and_hex_suffix():
|
|
26
|
+
assert clean_table("[Extract].[Orders_ABCDEF0123456789ABCDEF0123456789]") == "Orders"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_clean_table_plain():
|
|
30
|
+
assert clean_table("[Orders]") == "Orders"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_clean_table_none():
|
|
34
|
+
assert clean_table(None) is None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_clean_field_takes_last_dot_segment():
|
|
38
|
+
assert clean_field("[Orders].[CustomerID]") == "CustomerID"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_clean_field_unwraps_derivation_wrapper():
|
|
42
|
+
assert clean_field("none:Category:nk") == "Category"
|
|
43
|
+
assert clean_field("clct:Geometry:ok") == "Geometry"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_clean_field_plain():
|
|
47
|
+
assert clean_field("CustomerID") == "CustomerID"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_strip_brackets():
|
|
51
|
+
assert strip_brackets("[Calculation_123]") == "Calculation_123"
|
|
52
|
+
assert strip_brackets(None) is None
|
|
@@ -61,3 +61,17 @@ def test_athena_region_extracts_when_pattern_matches():
|
|
|
61
61
|
)
|
|
62
62
|
conns = extract_named_connections(xml_doc)
|
|
63
63
|
assert conns.iloc[0]["region"] == "us-east-1"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_parameters_fallback_keeps_columns(wenjie_xml, monkeypatch):
|
|
67
|
+
# A failing parameter extraction must still honour the empty-input
|
|
68
|
+
# contract: an empty frame with the real columns, not a bare one.
|
|
69
|
+
from twbparser_py import datasources
|
|
70
|
+
|
|
71
|
+
def boom(_xml):
|
|
72
|
+
raise RuntimeError("malformed")
|
|
73
|
+
|
|
74
|
+
monkeypatch.setattr(datasources, "extract_parameters", boom)
|
|
75
|
+
params = extract_datasource_details(wenjie_xml)["parameters"]
|
|
76
|
+
assert params.empty
|
|
77
|
+
assert list(params.columns) == datasources._PARAMETER_COLUMNS
|