tableau-cli 0.1.3__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tableau_cli-0.1.4/.claude/settings.local.json +8 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/PKG-INFO +7 -2
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/README.md +6 -1
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/pyproject.toml +1 -1
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/cli.py +1 -1
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/convert_cmd.py +4 -10
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/datasources_cmd.py +2 -2
- tableau_cli-0.1.4/src/tableau_cli/utils/_convert_worker.py +55 -0
- tableau_cli-0.1.4/src/tableau_cli/utils/convert.py +177 -0
- tableau_cli-0.1.3/src/tableau_cli/utils/convert.py +0 -99
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/.gitignore +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/LICENSE +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/NOTICE +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/skills/SKILL.md +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/skills/references/cli.md +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/skills/references/installation.md +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/__init__.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/api/__init__.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/api/client.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/auth/__init__.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/auth/with_auth.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/__init__.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/config_cmd.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/projects_cmd.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/search_cmd.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/views_cmd.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/workbooks_cmd.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/config/__init__.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/config/store.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/config/types.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/errors/__init__.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/errors/cli_error.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/errors/vds_error_handler.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/output/__init__.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/output/format.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/output/json_output.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/output/table_output.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/__init__.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/datasource_metadata_utils.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/lineage_utils.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/paginate.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/search_content_utils.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/tableau_version.py +0 -0
- {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/web_url.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tableau-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: CLI for Tableau Server/Cloud, designed for AI agent integration
|
|
5
5
|
Project-URL: Repository, https://github.com/i-richardwang/tableau-cli
|
|
6
6
|
Author: Richard Wang
|
|
@@ -166,7 +166,12 @@ tableau-cli views image <viewId> --vf "Region=West" -o west.png
|
|
|
166
166
|
|
|
167
167
|
### Convert
|
|
168
168
|
|
|
169
|
-
Convert local TDSX/HYPER files to Parquet or CSV.
|
|
169
|
+
Convert local TDSX/HYPER files to Parquet or CSV.
|
|
170
|
+
|
|
171
|
+
Conversion needs heavier packages (`pantab`, `polars`, `pyarrow`). You have two options:
|
|
172
|
+
|
|
173
|
+
- Install them into the CLI: `pip install tableau-cli[convert]`.
|
|
174
|
+
- Don't install anything: if [uv](https://docs.astral.sh/uv/) is on your `PATH`, conversion transparently falls back to running the step in an ephemeral `uv run --with ...` environment, so those packages never land in your host Python. The first run provisions the environment (a few seconds); later runs use uv's cache. This is the recommended path when the CLI itself was installed via `uv tool install tableau-cli`.
|
|
170
175
|
|
|
171
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.
|
|
172
177
|
|
|
@@ -136,7 +136,12 @@ tableau-cli views image <viewId> --vf "Region=West" -o west.png
|
|
|
136
136
|
|
|
137
137
|
### Convert
|
|
138
138
|
|
|
139
|
-
Convert local TDSX/HYPER files to Parquet or CSV.
|
|
139
|
+
Convert local TDSX/HYPER files to Parquet or CSV.
|
|
140
|
+
|
|
141
|
+
Conversion needs heavier packages (`pantab`, `polars`, `pyarrow`). You have two options:
|
|
142
|
+
|
|
143
|
+
- Install them into the CLI: `pip install tableau-cli[convert]`.
|
|
144
|
+
- Don't install anything: if [uv](https://docs.astral.sh/uv/) is on your `PATH`, conversion transparently falls back to running the step in an ephemeral `uv run --with ...` environment, so those packages never land in your host Python. The first run provisions the environment (a few seconds); later runs use uv's cache. This is the recommended path when the CLI itself was installed via `uv tool install tableau-cli`.
|
|
140
145
|
|
|
141
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.
|
|
142
147
|
|
|
@@ -11,10 +11,8 @@ from ..errors.cli_error import CliError
|
|
|
11
11
|
from ..output.format import output
|
|
12
12
|
from ..utils.convert import (
|
|
13
13
|
SUPPORTED_FORMATS,
|
|
14
|
-
check_convert_deps,
|
|
15
14
|
extract_hyper_from_tdsx,
|
|
16
|
-
|
|
17
|
-
write_df,
|
|
15
|
+
run_conversion,
|
|
18
16
|
)
|
|
19
17
|
|
|
20
18
|
|
|
@@ -26,8 +24,6 @@ from ..utils.convert import (
|
|
|
26
24
|
@click.option("-o", "--output", "output_path", default=None, help="Output file or directory (default: same as input)")
|
|
27
25
|
def convert_command(input_path, to_fmt, output_path):
|
|
28
26
|
"""Convert TDSX/HYPER files to Parquet or CSV format."""
|
|
29
|
-
check_convert_deps()
|
|
30
|
-
|
|
31
27
|
input_p = Path(input_path)
|
|
32
28
|
suffix = input_p.suffix.lower()
|
|
33
29
|
|
|
@@ -47,15 +43,13 @@ def convert_command(input_path, to_fmt, output_path):
|
|
|
47
43
|
else:
|
|
48
44
|
out_p = Path(output_path)
|
|
49
45
|
|
|
50
|
-
#
|
|
46
|
+
# Convert (in-process if deps present, else via an ephemeral uv environment)
|
|
51
47
|
if suffix == ".hyper":
|
|
52
|
-
|
|
48
|
+
run_conversion(input_p, out_p, to_fmt)
|
|
53
49
|
else:
|
|
54
50
|
with TemporaryDirectory() as td:
|
|
55
51
|
hyper_path = extract_hyper_from_tdsx(input_p, Path(td))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
write_df(df, out_p, to_fmt)
|
|
52
|
+
run_conversion(hyper_path, out_p, to_fmt)
|
|
59
53
|
|
|
60
54
|
abs_path = os.path.abspath(out_p)
|
|
61
55
|
sys.stderr.write(f"Converted to {abs_path}\n")
|
|
@@ -65,9 +65,9 @@ def datasources_download(datasource_id, output_path, to_fmt):
|
|
|
65
65
|
config = resolve_config()
|
|
66
66
|
|
|
67
67
|
if to_fmt != "tdsx":
|
|
68
|
-
from ..utils.convert import
|
|
68
|
+
from ..utils.convert import ensure_convert_available
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
ensure_convert_available()
|
|
71
71
|
|
|
72
72
|
data, filename = with_auth(
|
|
73
73
|
config,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Standalone hyper -> parquet/csv worker, run in an ephemeral uv environment.
|
|
2
|
+
|
|
3
|
+
Invoked as: python _convert_worker.py <hyper_path> <output_path> <parquet|csv>
|
|
4
|
+
|
|
5
|
+
Must stay self-contained: only pantab / polars + stdlib, no `tableau_cli` imports,
|
|
6
|
+
because it runs in a fresh `uv run --with ...` interpreter where this package is
|
|
7
|
+
not installed. Known errors are reported as a single JSON line on stdout plus a
|
|
8
|
+
non-zero exit; the parent process (utils/convert.py) turns them into CliError.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _fail(error_type: str, message: str, hint: str = "") -> None:
|
|
19
|
+
payload = {"error_type": error_type, "message": message}
|
|
20
|
+
if hint:
|
|
21
|
+
payload["hint"] = hint
|
|
22
|
+
print(json.dumps(payload))
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> None:
|
|
27
|
+
if len(sys.argv) != 4:
|
|
28
|
+
_fail("convert-error", "worker expects <hyper_path> <output_path> <format>")
|
|
29
|
+
|
|
30
|
+
hyper_path, output_path, to_fmt = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
31
|
+
name = os.path.basename(hyper_path)
|
|
32
|
+
|
|
33
|
+
import pantab
|
|
34
|
+
|
|
35
|
+
frames = pantab.frames_from_hyper(hyper_path, return_type="polars")
|
|
36
|
+
if not frames:
|
|
37
|
+
_fail("convert-error", f"No tables found in {name}")
|
|
38
|
+
if len(frames) != 1:
|
|
39
|
+
_fail(
|
|
40
|
+
"convert-error",
|
|
41
|
+
f"Found {len(frames)} tables in {name}, expected 1",
|
|
42
|
+
"Multi-table hyper files are not supported yet.",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
df = next(iter(frames.values()))
|
|
46
|
+
if to_fmt == "parquet":
|
|
47
|
+
df.write_parquet(output_path)
|
|
48
|
+
elif to_fmt == "csv":
|
|
49
|
+
df.write_csv(output_path)
|
|
50
|
+
else:
|
|
51
|
+
_fail("convert-error", f"Unsupported format: {to_fmt}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Shared helpers for converting TDSX / HYPER files to Parquet or CSV.
|
|
2
|
+
|
|
3
|
+
The heavy conversion dependencies (pantab / polars / pyarrow) are optional. When
|
|
4
|
+
they are not importable in the current interpreter — e.g. the CLI was installed
|
|
5
|
+
via `uv tool install` without the `[convert]` extra — conversion transparently
|
|
6
|
+
falls back to running a self-contained worker in an ephemeral `uv run --with ...`
|
|
7
|
+
environment, so the heavy packages never land in the host Python.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from tempfile import TemporaryDirectory
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
from zipfile import ZipFile
|
|
19
|
+
|
|
20
|
+
from ..errors.cli_error import CliError
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
import polars as pl
|
|
24
|
+
|
|
25
|
+
# Import names used to detect in-process availability.
|
|
26
|
+
CONVERT_DEPS = ("pantab", "polars", "pyarrow")
|
|
27
|
+
# PEP 508 requirements handed to `uv run --with` (keep in sync with pyproject [convert]).
|
|
28
|
+
CONVERT_REQUIREMENTS = ("pantab>=4.0", "polars>=1.0", "pyarrow>=15.0")
|
|
29
|
+
SUPPORTED_FORMATS = ("parquet", "csv")
|
|
30
|
+
|
|
31
|
+
_WORKER = Path(__file__).parent / "_convert_worker.py"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_importable(name: str) -> bool:
|
|
35
|
+
try:
|
|
36
|
+
__import__(name)
|
|
37
|
+
return True
|
|
38
|
+
except ImportError:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _deps_available() -> bool:
|
|
43
|
+
return all(_is_importable(pkg) for pkg in CONVERT_DEPS)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _uv_available() -> bool:
|
|
47
|
+
return shutil.which("uv") is not None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ensure_convert_available() -> None:
|
|
51
|
+
"""Fail fast if conversion can run neither in-process nor via uv.
|
|
52
|
+
|
|
53
|
+
Used by callers that do expensive work (e.g. downloading a datasource) before
|
|
54
|
+
converting, so they can bail out before spending that effort.
|
|
55
|
+
"""
|
|
56
|
+
if _deps_available() or _uv_available():
|
|
57
|
+
return
|
|
58
|
+
raise CliError(
|
|
59
|
+
error_type="missing-dependencies",
|
|
60
|
+
message=f"Convert requires packages not installed: {', '.join(CONVERT_DEPS)}",
|
|
61
|
+
hint=(
|
|
62
|
+
"Install uv (https://docs.astral.sh/uv/) to convert without installing these packages, "
|
|
63
|
+
"or run `pip install tableau-cli[convert]`."
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_hyper_from_tdsx(tdsx_path: Path, target_dir: Path) -> Path:
|
|
69
|
+
"""Extract the .hyper file from a TDSX archive."""
|
|
70
|
+
with ZipFile(tdsx_path) as zf:
|
|
71
|
+
members = [m for m in zf.namelist() if m.endswith(".hyper")]
|
|
72
|
+
if len(members) == 0:
|
|
73
|
+
raise CliError(
|
|
74
|
+
error_type="convert-error",
|
|
75
|
+
message=f"No .hyper file found in {tdsx_path.name}",
|
|
76
|
+
hint="This datasource may be a live connection with no embedded data.",
|
|
77
|
+
)
|
|
78
|
+
if len(members) != 1:
|
|
79
|
+
raise CliError(
|
|
80
|
+
error_type="convert-error",
|
|
81
|
+
message=f"Found {len(members)} .hyper files in {tdsx_path.name}, expected 1",
|
|
82
|
+
)
|
|
83
|
+
member = members[0]
|
|
84
|
+
zf.extract(member, path=target_dir)
|
|
85
|
+
extracted = target_dir / member
|
|
86
|
+
if extracted.parent != target_dir:
|
|
87
|
+
final_path = target_dir / extracted.name
|
|
88
|
+
extracted.replace(final_path)
|
|
89
|
+
return final_path
|
|
90
|
+
return extracted
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def read_hyper(hyper_path: Path) -> pl.DataFrame:
|
|
94
|
+
"""Read .hyper file and return as Polars DataFrame (in-process path)."""
|
|
95
|
+
import pantab
|
|
96
|
+
|
|
97
|
+
frames = pantab.frames_from_hyper(str(hyper_path), return_type="polars")
|
|
98
|
+
if not frames:
|
|
99
|
+
raise CliError(
|
|
100
|
+
error_type="convert-error",
|
|
101
|
+
message=f"No tables found in {hyper_path.name}",
|
|
102
|
+
)
|
|
103
|
+
if len(frames) != 1:
|
|
104
|
+
raise CliError(
|
|
105
|
+
error_type="convert-error",
|
|
106
|
+
message=f"Found {len(frames)} tables in {hyper_path.name}, expected 1",
|
|
107
|
+
hint="Multi-table hyper files are not supported yet.",
|
|
108
|
+
)
|
|
109
|
+
return next(iter(frames.values()))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def write_df(df: pl.DataFrame, output_path: Path, to: str) -> None:
|
|
113
|
+
"""Write DataFrame to the specified format (in-process path)."""
|
|
114
|
+
if to == "parquet":
|
|
115
|
+
df.write_parquet(output_path)
|
|
116
|
+
elif to == "csv":
|
|
117
|
+
df.write_csv(output_path)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _convert_via_uv(hyper_path: Path, output_path: Path, to_fmt: str) -> None:
|
|
121
|
+
"""Convert by running the worker in an ephemeral uv environment.
|
|
122
|
+
|
|
123
|
+
uv's own progress (provisioning the environment on first run) is streamed to
|
|
124
|
+
stderr so long first runs are visible; only the worker's stdout is captured,
|
|
125
|
+
where it emits a structured JSON error on failure.
|
|
126
|
+
"""
|
|
127
|
+
cmd = ["uv", "run"]
|
|
128
|
+
for req in CONVERT_REQUIREMENTS:
|
|
129
|
+
cmd += ["--with", req]
|
|
130
|
+
cmd += ["python", str(_WORKER), str(hyper_path), str(output_path), to_fmt]
|
|
131
|
+
|
|
132
|
+
proc = subprocess.run(cmd, stdout=subprocess.PIPE, text=True)
|
|
133
|
+
if proc.returncode == 0:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
payload = None
|
|
137
|
+
out = (proc.stdout or "").strip()
|
|
138
|
+
if out:
|
|
139
|
+
try:
|
|
140
|
+
payload = json.loads(out.splitlines()[-1])
|
|
141
|
+
except (ValueError, IndexError):
|
|
142
|
+
payload = None
|
|
143
|
+
|
|
144
|
+
if isinstance(payload, dict) and payload.get("error_type"):
|
|
145
|
+
raise CliError(
|
|
146
|
+
error_type=payload["error_type"],
|
|
147
|
+
message=payload.get("message", "Conversion failed"),
|
|
148
|
+
hint=payload.get("hint", ""),
|
|
149
|
+
)
|
|
150
|
+
raise CliError(
|
|
151
|
+
error_type="convert-error",
|
|
152
|
+
message="Conversion via uv failed. See the uv output above for details.",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def run_conversion(hyper_path: Path, output_path: Path, to_fmt: str) -> None:
|
|
157
|
+
"""Convert a .hyper file to parquet/csv, in-process if deps are present,
|
|
158
|
+
otherwise via an ephemeral uv environment."""
|
|
159
|
+
if _deps_available():
|
|
160
|
+
df = read_hyper(hyper_path)
|
|
161
|
+
write_df(df, output_path, to_fmt)
|
|
162
|
+
return
|
|
163
|
+
if _uv_available():
|
|
164
|
+
_convert_via_uv(hyper_path, output_path, to_fmt)
|
|
165
|
+
return
|
|
166
|
+
ensure_convert_available() # raises with an actionable hint
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def convert_tdsx_bytes(data: bytes, output_path: Path, to_fmt: str) -> None:
|
|
170
|
+
"""Convert raw TDSX bytes directly to parquet/csv without persisting the intermediate file."""
|
|
171
|
+
with TemporaryDirectory() as td:
|
|
172
|
+
tmp_dir = Path(td)
|
|
173
|
+
tdsx_path = tmp_dir / "datasource.tdsx"
|
|
174
|
+
tdsx_path.write_bytes(data)
|
|
175
|
+
hyper_path = extract_hyper_from_tdsx(tdsx_path, tmp_dir)
|
|
176
|
+
# Keep the temp dir alive through conversion: the uv worker reads the file from disk.
|
|
177
|
+
run_conversion(hyper_path, output_path, to_fmt)
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"""Shared helpers for converting TDSX / HYPER files to Parquet or CSV."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from tempfile import TemporaryDirectory
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
8
|
-
from zipfile import ZipFile
|
|
9
|
-
|
|
10
|
-
from ..errors.cli_error import CliError
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
import polars as pl
|
|
14
|
-
|
|
15
|
-
CONVERT_DEPS = ("pantab", "polars", "pyarrow")
|
|
16
|
-
SUPPORTED_FORMATS = ("parquet", "csv")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _is_importable(name: str) -> bool:
|
|
20
|
-
try:
|
|
21
|
-
__import__(name)
|
|
22
|
-
return True
|
|
23
|
-
except ImportError:
|
|
24
|
-
return False
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def check_convert_deps() -> None:
|
|
28
|
-
"""Check that optional convert dependencies are installed."""
|
|
29
|
-
missing = [pkg for pkg in CONVERT_DEPS if not _is_importable(pkg)]
|
|
30
|
-
if missing:
|
|
31
|
-
raise CliError(
|
|
32
|
-
error_type="missing-dependencies",
|
|
33
|
-
message=f"Convert requires packages not installed: {', '.join(missing)}",
|
|
34
|
-
hint="Run `pip install tableau-cli[convert]` to install conversion dependencies.",
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def extract_hyper_from_tdsx(tdsx_path: Path, target_dir: Path) -> Path:
|
|
39
|
-
"""Extract the .hyper file from a TDSX archive."""
|
|
40
|
-
with ZipFile(tdsx_path) as zf:
|
|
41
|
-
members = [m for m in zf.namelist() if m.endswith(".hyper")]
|
|
42
|
-
if len(members) == 0:
|
|
43
|
-
raise CliError(
|
|
44
|
-
error_type="convert-error",
|
|
45
|
-
message=f"No .hyper file found in {tdsx_path.name}",
|
|
46
|
-
hint="This datasource may be a live connection with no embedded data.",
|
|
47
|
-
)
|
|
48
|
-
if len(members) != 1:
|
|
49
|
-
raise CliError(
|
|
50
|
-
error_type="convert-error",
|
|
51
|
-
message=f"Found {len(members)} .hyper files in {tdsx_path.name}, expected 1",
|
|
52
|
-
)
|
|
53
|
-
member = members[0]
|
|
54
|
-
zf.extract(member, path=target_dir)
|
|
55
|
-
extracted = target_dir / member
|
|
56
|
-
if extracted.parent != target_dir:
|
|
57
|
-
final_path = target_dir / extracted.name
|
|
58
|
-
extracted.replace(final_path)
|
|
59
|
-
return final_path
|
|
60
|
-
return extracted
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def read_hyper(hyper_path: Path) -> pl.DataFrame:
|
|
64
|
-
"""Read .hyper file and return as Polars DataFrame."""
|
|
65
|
-
import pantab
|
|
66
|
-
import polars as pl
|
|
67
|
-
|
|
68
|
-
frames = pantab.frames_from_hyper(str(hyper_path))
|
|
69
|
-
if not frames:
|
|
70
|
-
raise CliError(
|
|
71
|
-
error_type="convert-error",
|
|
72
|
-
message=f"No tables found in {hyper_path.name}",
|
|
73
|
-
)
|
|
74
|
-
if len(frames) != 1:
|
|
75
|
-
raise CliError(
|
|
76
|
-
error_type="convert-error",
|
|
77
|
-
message=f"Found {len(frames)} tables in {hyper_path.name}, expected 1",
|
|
78
|
-
hint="Multi-table hyper files are not supported yet.",
|
|
79
|
-
)
|
|
80
|
-
return pl.from_pandas(next(iter(frames.values())))
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def write_df(df: pl.DataFrame, output_path: Path, to: str) -> None:
|
|
84
|
-
"""Write DataFrame to the specified format."""
|
|
85
|
-
if to == "parquet":
|
|
86
|
-
df.write_parquet(output_path)
|
|
87
|
-
elif to == "csv":
|
|
88
|
-
df.write_csv(output_path)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def convert_tdsx_bytes(data: bytes, output_path: Path, to_fmt: str) -> None:
|
|
92
|
-
"""Convert raw TDSX bytes directly to parquet/csv without persisting the intermediate file."""
|
|
93
|
-
with TemporaryDirectory() as td:
|
|
94
|
-
tmp_dir = Path(td)
|
|
95
|
-
tdsx_path = tmp_dir / "datasource.tdsx"
|
|
96
|
-
tdsx_path.write_bytes(data)
|
|
97
|
-
hyper_path = extract_hyper_from_tdsx(tdsx_path, tmp_dir)
|
|
98
|
-
df = read_hyper(hyper_path)
|
|
99
|
-
write_df(df, output_path, to_fmt)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|