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.
Files changed (64) hide show
  1. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/AGENTS.md +7 -1
  2. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/MANIFEST.in +1 -0
  3. py_tbparse-0.2.0/PKG-INFO +150 -0
  4. py_tbparse-0.2.0/README.md +114 -0
  5. py_tbparse-0.2.0/docs/gui-overview.png +0 -0
  6. py_tbparse-0.2.0/py_tbparse.egg-info/PKG-INFO +150 -0
  7. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/py_tbparse.egg-info/SOURCES.txt +2 -0
  8. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/pyproject.toml +6 -1
  9. py_tbparse-0.2.0/tests/conftest.py +56 -0
  10. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_batch.py +12 -0
  11. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_calculated_fields.py +15 -0
  12. py_tbparse-0.2.0/tests/test_clean.py +52 -0
  13. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_datasources.py +14 -0
  14. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_gui_browser.py +190 -14
  15. py_tbparse-0.2.0/tests/test_joins.py +191 -0
  16. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_parser.py +26 -1
  17. py_tbparse-0.2.0/tests/test_relationships.py +156 -0
  18. py_tbparse-0.2.0/tests/test_version.py +25 -0
  19. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_webgui.py +172 -0
  20. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/__init__.py +1 -1
  21. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/_clean.py +32 -0
  22. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/_xml.py +35 -9
  23. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/batch.py +1 -1
  24. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/calculated_fields.py +8 -1
  25. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/cli.py +1 -1
  26. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/datasources.py +1 -1
  27. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/joins.py +36 -10
  28. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/parser.py +11 -8
  29. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/relationships.py +39 -12
  30. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/validators.py +3 -3
  31. py_tbparse-0.2.0/twbparser_py/webgui.py +928 -0
  32. py_tbparse-0.1.1a0/PKG-INFO +0 -168
  33. py_tbparse-0.1.1a0/README.md +0 -135
  34. py_tbparse-0.1.1a0/py_tbparse.egg-info/PKG-INFO +0 -168
  35. py_tbparse-0.1.1a0/tests/conftest.py +0 -25
  36. py_tbparse-0.1.1a0/tests/test_clean.py +0 -31
  37. py_tbparse-0.1.1a0/tests/test_joins.py +0 -74
  38. py_tbparse-0.1.1a0/tests/test_relationships.py +0 -62
  39. py_tbparse-0.1.1a0/twbparser_py/webgui.py +0 -398
  40. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/LICENSE +0 -0
  41. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/py_tbparse.egg-info/dependency_links.txt +0 -0
  42. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/py_tbparse.egg-info/entry_points.txt +0 -0
  43. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/py_tbparse.egg-info/requires.txt +0 -0
  44. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/py_tbparse.egg-info/top_level.txt +0 -0
  45. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/setup.cfg +0 -0
  46. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/fixtures/test_for_wenjie.twb +0 -0
  47. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/fixtures/test_for_zip.twbx +0 -0
  48. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_cli.py +0 -0
  49. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_dashboards.py +0 -0
  50. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_diff.py +0 -0
  51. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_fields.py +0 -0
  52. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_graph.py +0 -0
  53. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_published.py +0 -0
  54. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_sql.py +0 -0
  55. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_validators.py +0 -0
  56. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/tests/test_xml.py +0 -0
  57. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/_tables.py +0 -0
  58. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/dashboards.py +0 -0
  59. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/diff.py +0 -0
  60. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/fields.py +0 -0
  61. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/graph.py +0 -0
  62. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/published.py +0 -0
  63. {py_tbparse-0.1.1a0 → py_tbparse-0.2.0}/twbparser_py/py.typed +0 -0
  64. {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. Don't add
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.
@@ -4,3 +4,4 @@ include AGENTS.md
4
4
  recursive-include tests *.py
5
5
  include tests/fixtures/*.twb
6
6
  include tests/fixtures/*.twbx
7
+ recursive-include docs *.png
@@ -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
+ ![py-tbparse GUI, overview of a loaded workbook](https://raw.githubusercontent.com/DDSNA/py-tbparse/main/docs/gui-overview.png)
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
+ ![py-tbparse GUI, overview of a loaded workbook](https://raw.githubusercontent.com/DDSNA/py-tbparse/main/docs/gui-overview.png)
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
+ ![py-tbparse GUI, overview of a loaded workbook](https://raw.githubusercontent.com/DDSNA/py-tbparse/main/docs/gui-overview.png)
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.1.1a"
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="&quot;East&quot;">
42
+ <calculation class="tableau" formula="&quot;East&quot;"/>
43
+ <members>
44
+ <member value="&quot;East&quot;"/>
45
+ <member value="&quot;West&quot;"/>
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