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.
Files changed (44) hide show
  1. tableau_cli-0.1.4/.claude/settings.local.json +8 -0
  2. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/PKG-INFO +7 -2
  3. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/README.md +6 -1
  4. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/pyproject.toml +1 -1
  5. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/cli.py +1 -1
  6. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/convert_cmd.py +4 -10
  7. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/datasources_cmd.py +2 -2
  8. tableau_cli-0.1.4/src/tableau_cli/utils/_convert_worker.py +55 -0
  9. tableau_cli-0.1.4/src/tableau_cli/utils/convert.py +177 -0
  10. tableau_cli-0.1.3/src/tableau_cli/utils/convert.py +0 -99
  11. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/.gitignore +0 -0
  12. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/LICENSE +0 -0
  13. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/NOTICE +0 -0
  14. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/skills/SKILL.md +0 -0
  15. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/skills/references/cli.md +0 -0
  16. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/skills/references/installation.md +0 -0
  17. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/__init__.py +0 -0
  18. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/api/__init__.py +0 -0
  19. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/api/client.py +0 -0
  20. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/auth/__init__.py +0 -0
  21. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/auth/with_auth.py +0 -0
  22. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/__init__.py +0 -0
  23. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/config_cmd.py +0 -0
  24. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/projects_cmd.py +0 -0
  25. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/search_cmd.py +0 -0
  26. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/views_cmd.py +0 -0
  27. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/commands/workbooks_cmd.py +0 -0
  28. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/config/__init__.py +0 -0
  29. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/config/store.py +0 -0
  30. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/config/types.py +0 -0
  31. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/errors/__init__.py +0 -0
  32. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/errors/cli_error.py +0 -0
  33. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/errors/vds_error_handler.py +0 -0
  34. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/output/__init__.py +0 -0
  35. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/output/format.py +0 -0
  36. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/output/json_output.py +0 -0
  37. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/output/table_output.py +0 -0
  38. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/__init__.py +0 -0
  39. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/datasource_metadata_utils.py +0 -0
  40. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/lineage_utils.py +0 -0
  41. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/paginate.py +0 -0
  42. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/search_content_utils.py +0 -0
  43. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/tableau_version.py +0 -0
  44. {tableau_cli-0.1.3 → tableau_cli-0.1.4}/src/tableau_cli/utils/web_url.py +0 -0
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(command -v uv)",
5
+ "Bash(uv --version)"
6
+ ]
7
+ }
8
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tableau-cli
3
- Version: 0.1.3
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. Requires `pip install tableau-cli[convert]`.
169
+ Convert local TDSX/HYPER files to Parquet or CSV.
170
+
171
+ Conversion needs heavier packages (`pantab`, `polars`, `pyarrow`). You have two options:
172
+
173
+ - Install them into the CLI: `pip install tableau-cli[convert]`.
174
+ - Don't install anything: if [uv](https://docs.astral.sh/uv/) is on your `PATH`, conversion transparently falls back to running the step in an ephemeral `uv run --with ...` environment, so those packages never land in your host Python. The first run provisions the environment (a few seconds); later runs use uv's cache. This is the recommended path when the CLI itself was installed via `uv tool install tableau-cli`.
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. Requires `pip install tableau-cli[convert]`.
139
+ Convert local TDSX/HYPER files to Parquet or CSV.
140
+
141
+ Conversion needs heavier packages (`pantab`, `polars`, `pyarrow`). You have two options:
142
+
143
+ - Install them into the CLI: `pip install tableau-cli[convert]`.
144
+ - Don't install anything: if [uv](https://docs.astral.sh/uv/) is on your `PATH`, conversion transparently falls back to running the step in an ephemeral `uv run --with ...` environment, so those packages never land in your host Python. The first run provisions the environment (a few seconds); later runs use uv's cache. This is the recommended path when the CLI itself was installed via `uv tool install tableau-cli`.
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tableau-cli"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "CLI for Tableau Server/Cloud, designed for AI agent integration"
9
9
  license = "Apache-2.0"
10
10
  requires-python = ">=3.10"
@@ -123,7 +123,7 @@ class TableauCli(click.Group):
123
123
 
124
124
 
125
125
  @click.group(cls=TableauCli)
126
- @click.version_option("0.1.3")
126
+ @click.version_option("0.1.4")
127
127
  def cli():
128
128
  """CLI tool for interacting with Tableau Server/Cloud."""
129
129
 
@@ -11,10 +11,8 @@ from ..errors.cli_error import CliError
11
11
  from ..output.format import output
12
12
  from ..utils.convert import (
13
13
  SUPPORTED_FORMATS,
14
- check_convert_deps,
15
14
  extract_hyper_from_tdsx,
16
- read_hyper,
17
- write_df,
15
+ run_conversion,
18
16
  )
19
17
 
20
18
 
@@ -26,8 +24,6 @@ from ..utils.convert import (
26
24
  @click.option("-o", "--output", "output_path", default=None, help="Output file or directory (default: same as input)")
27
25
  def convert_command(input_path, to_fmt, output_path):
28
26
  """Convert TDSX/HYPER files to Parquet or CSV format."""
29
- check_convert_deps()
30
-
31
27
  input_p = Path(input_path)
32
28
  suffix = input_p.suffix.lower()
33
29
 
@@ -47,15 +43,13 @@ def convert_command(input_path, to_fmt, output_path):
47
43
  else:
48
44
  out_p = Path(output_path)
49
45
 
50
- # Read hyper data
46
+ # Convert (in-process if deps present, else via an ephemeral uv environment)
51
47
  if suffix == ".hyper":
52
- df = read_hyper(input_p)
48
+ run_conversion(input_p, out_p, to_fmt)
53
49
  else:
54
50
  with TemporaryDirectory() as td:
55
51
  hyper_path = extract_hyper_from_tdsx(input_p, Path(td))
56
- df = read_hyper(hyper_path)
57
-
58
- write_df(df, out_p, to_fmt)
52
+ run_conversion(hyper_path, out_p, to_fmt)
59
53
 
60
54
  abs_path = os.path.abspath(out_p)
61
55
  sys.stderr.write(f"Converted to {abs_path}\n")
@@ -65,9 +65,9 @@ def datasources_download(datasource_id, output_path, to_fmt):
65
65
  config = resolve_config()
66
66
 
67
67
  if to_fmt != "tdsx":
68
- from ..utils.convert import check_convert_deps
68
+ from ..utils.convert import ensure_convert_available
69
69
 
70
- check_convert_deps()
70
+ ensure_convert_available()
71
71
 
72
72
  data, filename = with_auth(
73
73
  config,
@@ -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