cloudcat 0.6.1__tar.gz → 0.6.2__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.
- {cloudcat-0.6.1 → cloudcat-0.6.2}/PKG-INFO +5 -1
- {cloudcat-0.6.1 → cloudcat-0.6.2}/README.md +4 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/__init__.py +1 -1
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/cli.py +104 -1
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/completion.py +51 -2
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat.egg-info/PKG-INFO +5 -1
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat.egg-info/SOURCES.txt +1 -0
- cloudcat-0.6.2/tests/test_v062_fixes.py +155 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/LICENSE +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/compression.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/config.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/data/SKILL.md +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/filtering.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/formatters.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/progress.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/readers/__init__.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/readers/avro.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/readers/csv.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/readers/json.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/readers/orc.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/readers/parquet.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/readers/text.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/storage/__init__.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/storage/azure.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/storage/base.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/storage/gcs.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/storage/local.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/storage/s3.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/streaming/__init__.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/streaming/filesystems.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/streaming/stats.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/streaming/tracking.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/tables/__init__.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/tables/delta.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/tables/iceberg.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/tables/pushdown.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat/user_config.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat.egg-info/dependency_links.txt +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat.egg-info/entry_points.txt +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat.egg-info/requires.txt +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/cloudcat.egg-info/top_level.txt +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/pyproject.toml +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/setup.cfg +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_bugfix_round2.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_cli.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_cli_features.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_completion.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_compression.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_config.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_config_stats_completion.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_data_reading.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_file_filtering.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_filtering.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_format_detection.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_import_lightness.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_integration.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_output_formatting.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_path_parsing.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_read_data_streaming.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_readers_roundtrip.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_regressions.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_skill_install.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_storage_listing.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_streaming_stats.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_tables.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_tracking.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_v05_features.py +0 -0
- {cloudcat-0.6.1 → cloudcat-0.6.2}/tests/test_visual_output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudcat
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Preview and analyze data files in Google Cloud Storage, AWS S3, and Azure Data Lake Storage Gen2 from your terminal
|
|
5
5
|
Author: Jonathan Sudhakar
|
|
6
6
|
License: MIT
|
|
@@ -554,6 +554,10 @@ Options:
|
|
|
554
554
|
|
|
555
555
|
--config-profile TEXT Named profile from ~/.config/cloudcat/config.toml
|
|
556
556
|
|
|
557
|
+
--install-completion [bash|zsh|fish]
|
|
558
|
+
Enable completion with one command (adds one
|
|
559
|
+
line to your shell rc file; idempotent)
|
|
560
|
+
|
|
557
561
|
--completion [bash|zsh|fish] Print the shell completion script and exit.
|
|
558
562
|
Once enabled, TAB completes PATH too: local
|
|
559
563
|
files natively, s3://<TAB> lists buckets,
|
|
@@ -472,6 +472,10 @@ Options:
|
|
|
472
472
|
|
|
473
473
|
--config-profile TEXT Named profile from ~/.config/cloudcat/config.toml
|
|
474
474
|
|
|
475
|
+
--install-completion [bash|zsh|fish]
|
|
476
|
+
Enable completion with one command (adds one
|
|
477
|
+
line to your shell rc file; idempotent)
|
|
478
|
+
|
|
475
479
|
--completion [bash|zsh|fish] Print the shell completion script and exit.
|
|
476
480
|
Once enabled, TAB completes PATH too: local
|
|
477
481
|
files natively, s3://<TAB> lists buckets,
|
|
@@ -202,6 +202,35 @@ def get_files_for_multiread(
|
|
|
202
202
|
return selected_files
|
|
203
203
|
|
|
204
204
|
|
|
205
|
+
_ICEBERG_NESTED_RE = re.compile(r'(.*)/metadata/(?:v\d+|\d+-[^/]*)\.metadata\.json$')
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _find_nested_tables(files: List[Tuple[str, int]]) -> dict:
|
|
209
|
+
"""Find lakehouse table roots nested inside a directory listing.
|
|
210
|
+
|
|
211
|
+
Returns {root_path: format}. Used to guide users who point cloudcat at
|
|
212
|
+
a folder OF tables rather than at a table — merging data files across
|
|
213
|
+
different tables would silently produce wrong results.
|
|
214
|
+
"""
|
|
215
|
+
tables = {}
|
|
216
|
+
for name, _size in files:
|
|
217
|
+
if '/_delta_log/' in f'/{name}':
|
|
218
|
+
root = name.split('/_delta_log/')[0]
|
|
219
|
+
if root: # empty means the listing prefix IS the table root
|
|
220
|
+
tables.setdefault(root, 'delta')
|
|
221
|
+
continue
|
|
222
|
+
match = _ICEBERG_NESTED_RE.match(name)
|
|
223
|
+
if match and match.group(1):
|
|
224
|
+
tables.setdefault(match.group(1), 'iceberg')
|
|
225
|
+
return tables
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _has_known_extension(name: str) -> bool:
|
|
229
|
+
"""True if the filename matches any recognized data-format extension."""
|
|
230
|
+
return any(re.search(regex, name, re.IGNORECASE)
|
|
231
|
+
for regex in FORMAT_EXTENSION_MAP.values())
|
|
232
|
+
|
|
233
|
+
|
|
205
234
|
def find_first_non_empty_file(
|
|
206
235
|
service: str,
|
|
207
236
|
bucket: str,
|
|
@@ -239,7 +268,40 @@ def find_first_non_empty_file(
|
|
|
239
268
|
info(Fore.YELLOW + f"No files matching format '{input_format}' found in "
|
|
240
269
|
f"{service}://{bucket}/{prefix}. Using first available file." + Style.RESET_ALL)
|
|
241
270
|
|
|
242
|
-
#
|
|
271
|
+
# No explicit format: the selection drives format INFERENCE, so only
|
|
272
|
+
# consider files whose extension we can actually infer from.
|
|
273
|
+
if not input_format:
|
|
274
|
+
nested_tables = _find_nested_tables(non_empty_files)
|
|
275
|
+
if nested_tables:
|
|
276
|
+
listing = '\n'.join(
|
|
277
|
+
f" {fmt}: {root}/" for root, fmt in sorted(nested_tables.items())
|
|
278
|
+
)
|
|
279
|
+
first_root = sorted(nested_tables)[0]
|
|
280
|
+
raise ValueError(
|
|
281
|
+
"this directory contains lakehouse table(s), not a single dataset:\n"
|
|
282
|
+
f"{listing}\n"
|
|
283
|
+
f"Point at a table root, e.g.: cloudcat {first_root}/ "
|
|
284
|
+
"(or pass --input-format to force reading loose files)"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
for file_name, file_size in non_empty_files:
|
|
288
|
+
if any(re.search(pattern, file_name) for pattern in SKIP_PATTERNS):
|
|
289
|
+
continue
|
|
290
|
+
if not _has_known_extension(file_name):
|
|
291
|
+
continue # e.g. catalog.db, .lock — can't infer a format from it
|
|
292
|
+
if not quiet:
|
|
293
|
+
info(Fore.BLUE + f"Selected file: {file_name} ({file_size} bytes)" + Style.RESET_ALL)
|
|
294
|
+
return file_name, file_size
|
|
295
|
+
|
|
296
|
+
sample = ', '.join(name.rsplit('/', 1)[-1] for name, _ in non_empty_files[:5])
|
|
297
|
+
raise ValueError(
|
|
298
|
+
f"no files with a recognized data format in {service}://{bucket}/{prefix} "
|
|
299
|
+
f"(found: {sample}). Formats are inferred from extensions "
|
|
300
|
+
"(.csv/.json/.parquet/.avro/.orc/.txt, optionally compressed); "
|
|
301
|
+
"pass --input-format to force one."
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Explicit format requested but unmatched: first non-metadata file.
|
|
243
305
|
for file_name, file_size in non_empty_files:
|
|
244
306
|
if not any(re.search(pattern, file_name) for pattern in SKIP_PATTERNS):
|
|
245
307
|
if not quiet:
|
|
@@ -850,6 +912,44 @@ def _print_completion(ctx, param, value):
|
|
|
850
912
|
ctx.exit()
|
|
851
913
|
|
|
852
914
|
|
|
915
|
+
_COMPLETION_SNIPPETS = {
|
|
916
|
+
'zsh': ('~/.zshrc', 'eval "$(cloudcat --completion zsh)"'),
|
|
917
|
+
'bash': ('~/.bashrc',
|
|
918
|
+
'COMP_WORDBREAKS=${COMP_WORDBREAKS//:/} # let URLs like s3:// complete\n'
|
|
919
|
+
'eval "$(cloudcat --completion bash)"'),
|
|
920
|
+
'fish': ('~/.config/fish/config.fish', 'cloudcat --completion fish | source'),
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
_COMPLETION_MARKER = '# added by cloudcat --install-completion'
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def _install_completion(ctx, param, value):
|
|
927
|
+
"""Eager callback: enable shell completion in the user's rc file.
|
|
928
|
+
|
|
929
|
+
Idempotent — a marker comment prevents duplicate blocks on re-runs.
|
|
930
|
+
"""
|
|
931
|
+
if not value or ctx.resilient_parsing:
|
|
932
|
+
return
|
|
933
|
+
rc_path, snippet = _COMPLETION_SNIPPETS[value]
|
|
934
|
+
rc_file = os.path.expanduser(rc_path)
|
|
935
|
+
|
|
936
|
+
existing = ''
|
|
937
|
+
if os.path.exists(rc_file):
|
|
938
|
+
with open(rc_file, 'r', encoding='utf-8', errors='replace') as f:
|
|
939
|
+
existing = f.read()
|
|
940
|
+
if _COMPLETION_MARKER in existing:
|
|
941
|
+
click.echo(f"Completion is already set up in {rc_file}")
|
|
942
|
+
ctx.exit()
|
|
943
|
+
|
|
944
|
+
os.makedirs(os.path.dirname(rc_file), exist_ok=True)
|
|
945
|
+
block = f"\n{_COMPLETION_MARKER}\n{snippet}\n"
|
|
946
|
+
with open(rc_file, 'a', encoding='utf-8') as f:
|
|
947
|
+
f.write(block)
|
|
948
|
+
click.echo(f"Added cloudcat completion to {rc_file} — restart your shell "
|
|
949
|
+
f"(or run: source {rc_path}) and try: cloudcat s3://<TAB>")
|
|
950
|
+
ctx.exit()
|
|
951
|
+
|
|
952
|
+
|
|
853
953
|
# Where each agent expects personal skills (name -> path builder).
|
|
854
954
|
_SKILL_DESTINATIONS = {
|
|
855
955
|
'claude': lambda: os.path.join(os.path.expanduser('~'), '.claude', 'skills', 'cloudcat', 'SKILL.md'),
|
|
@@ -962,6 +1062,9 @@ def _render_data(df, output_format: str) -> str:
|
|
|
962
1062
|
@click.option('--completion', type=click.Choice(['bash', 'zsh', 'fish']), is_eager=True,
|
|
963
1063
|
expose_value=False, callback=_print_completion,
|
|
964
1064
|
help='Print the shell completion script and exit (e.g. eval "$(cloudcat --completion zsh)")')
|
|
1065
|
+
@click.option('--install-completion', type=click.Choice(['bash', 'zsh', 'fish']), is_eager=True,
|
|
1066
|
+
expose_value=False, callback=_install_completion,
|
|
1067
|
+
help='Enable shell completion by adding one line to your shell rc file (idempotent)')
|
|
965
1068
|
@click.option('--install-skill', type=click.Choice(['claude', 'claude-project', 'codex', 'print']),
|
|
966
1069
|
is_eager=True, expose_value=False, callback=_install_skill,
|
|
967
1070
|
help='Install the bundled AI-agent skill: claude (~/.claude/skills), '
|
|
@@ -15,7 +15,51 @@ Behavior mirrors `ls` completion, scheme-aware:
|
|
|
15
15
|
|
|
16
16
|
from typing import List, Optional, Tuple
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
from click.shell_completion import CompletionItem, ZshComplete, add_completion_class
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _debug(message: str) -> None:
|
|
24
|
+
"""Optionally record why a completion produced nothing.
|
|
25
|
+
|
|
26
|
+
Completion swallows every error so it can never break the shell — which
|
|
27
|
+
makes "TAB does nothing" undiagnosable. Set CLOUDCAT_COMPLETE_DEBUG=1 and
|
|
28
|
+
failures are appended to ~/.cache/cloudcat/completion.log instead.
|
|
29
|
+
"""
|
|
30
|
+
if not os.environ.get('CLOUDCAT_COMPLETE_DEBUG'):
|
|
31
|
+
return
|
|
32
|
+
try:
|
|
33
|
+
import datetime
|
|
34
|
+
cache_dir = os.path.join(
|
|
35
|
+
os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache')), 'cloudcat')
|
|
36
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
37
|
+
with open(os.path.join(cache_dir, 'completion.log'), 'a', encoding='utf-8') as f:
|
|
38
|
+
f.write(f"{datetime.datetime.now().isoformat()} {message}\n")
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CloudcatZshComplete(ZshComplete):
|
|
44
|
+
"""Click's zsh completer, hardened for real-world .zshrc files.
|
|
45
|
+
|
|
46
|
+
The stock script calls compdef unconditionally; if the user evals it
|
|
47
|
+
before compinit has run, registration fails silently and TAB "does
|
|
48
|
+
nothing". This variant bootstraps compinit when needed.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
name = 'zsh'
|
|
52
|
+
source_template = ZshComplete.source_template.replace(
|
|
53
|
+
" compdef %(complete_func)s %(prog_name)s",
|
|
54
|
+
""" if ! typeset -f compdef >/dev/null 2>&1; then
|
|
55
|
+
autoload -Uz compinit && compinit -u
|
|
56
|
+
fi
|
|
57
|
+
compdef %(complete_func)s %(prog_name)s""",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Replace the registered zsh completer with the hardened variant.
|
|
62
|
+
add_completion_class(CloudcatZshComplete)
|
|
19
63
|
|
|
20
64
|
# Never return more candidates than a shell menu can usefully show.
|
|
21
65
|
LIMIT = 100
|
|
@@ -36,7 +80,10 @@ def _s3_client(profile: Optional[str]):
|
|
|
36
80
|
config = Config(connect_timeout=CONNECT_TIMEOUT, read_timeout=READ_TIMEOUT,
|
|
37
81
|
retries={'max_attempts': 0})
|
|
38
82
|
session = boto3.Session(profile_name=profile) if profile else boto3.Session()
|
|
39
|
-
|
|
83
|
+
# Without a configured region some boto3 versions raise NoRegionError at
|
|
84
|
+
# client creation; bucket listing is region-agnostic, so default it.
|
|
85
|
+
region = session.region_name or 'us-east-1'
|
|
86
|
+
return session.client('s3', region_name=region, config=config)
|
|
40
87
|
|
|
41
88
|
|
|
42
89
|
def _list_s3_buckets(profile: Optional[str]) -> List[str]:
|
|
@@ -196,4 +243,6 @@ def complete_path(ctx, param, incomplete: str) -> List[CompletionItem]:
|
|
|
196
243
|
return items
|
|
197
244
|
except Exception:
|
|
198
245
|
# Completion must never break the shell: no candidates beats a stack.
|
|
246
|
+
import traceback
|
|
247
|
+
_debug(f"complete_path({incomplete!r}) failed:\n{traceback.format_exc()}")
|
|
199
248
|
return []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudcat
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Preview and analyze data files in Google Cloud Storage, AWS S3, and Azure Data Lake Storage Gen2 from your terminal
|
|
5
5
|
Author: Jonathan Sudhakar
|
|
6
6
|
License: MIT
|
|
@@ -554,6 +554,10 @@ Options:
|
|
|
554
554
|
|
|
555
555
|
--config-profile TEXT Named profile from ~/.config/cloudcat/config.toml
|
|
556
556
|
|
|
557
|
+
--install-completion [bash|zsh|fish]
|
|
558
|
+
Enable completion with one command (adds one
|
|
559
|
+
line to your shell rc file; idempotent)
|
|
560
|
+
|
|
557
561
|
--completion [bash|zsh|fish] Print the shell completion script and exit.
|
|
558
562
|
Once enabled, TAB completes PATH too: local
|
|
559
563
|
files natively, s3://<TAB> lists buckets,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Tests for the 0.6.2 fixes: mixed-directory inference and completion setup."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from click.testing import CliRunner
|
|
8
|
+
|
|
9
|
+
from cloudcat.cli import main, _find_nested_tables
|
|
10
|
+
import cloudcat.completion as completion
|
|
11
|
+
|
|
12
|
+
runner = CliRunner()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# --------------------------------------------------------------------------
|
|
16
|
+
# directory inference (bug 1)
|
|
17
|
+
# --------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
class TestNestedTableDetection:
|
|
20
|
+
def test_finds_delta_and_iceberg_roots(self):
|
|
21
|
+
files = [
|
|
22
|
+
("/lake/orders_delta/_delta_log/000.json", 10),
|
|
23
|
+
("/lake/orders_delta/part-0.parquet", 10),
|
|
24
|
+
("/lake/shop/orders/metadata/00001-abc.metadata.json", 10),
|
|
25
|
+
("/lake/shop/orders/data/f.parquet", 10),
|
|
26
|
+
("/lake/catalog.db", 10),
|
|
27
|
+
]
|
|
28
|
+
tables = _find_nested_tables(files)
|
|
29
|
+
assert tables == {
|
|
30
|
+
"/lake/orders_delta": "delta",
|
|
31
|
+
"/lake/shop/orders": "iceberg",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def test_hadoop_style_iceberg_metadata(self):
|
|
35
|
+
files = [("/w/t/metadata/v3.metadata.json", 5)]
|
|
36
|
+
assert _find_nested_tables(files) == {"/w/t": "iceberg"}
|
|
37
|
+
|
|
38
|
+
def test_plain_files_are_not_tables(self):
|
|
39
|
+
files = [("/data/a.parquet", 5), ("/data/metadata/notes.txt", 5)]
|
|
40
|
+
assert _find_nested_tables(files) == {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestMixedDirectoryCli:
|
|
44
|
+
def test_directory_of_tables_gets_guidance(self, tmp_path):
|
|
45
|
+
deltalake = pytest.importorskip("deltalake")
|
|
46
|
+
import pandas as pd
|
|
47
|
+
from deltalake import write_deltalake
|
|
48
|
+
write_deltalake(str(tmp_path / "orders_delta"),
|
|
49
|
+
pd.DataFrame({"a": [1, 2]}))
|
|
50
|
+
# a second, fake iceberg table (detection only needs the marker file)
|
|
51
|
+
meta = tmp_path / "shop" / "orders" / "metadata"
|
|
52
|
+
meta.mkdir(parents=True)
|
|
53
|
+
(meta / "00001-abc.metadata.json").write_text("{}")
|
|
54
|
+
(tmp_path / "catalog.db").write_text("sqlite")
|
|
55
|
+
|
|
56
|
+
res = runner.invoke(main, [str(tmp_path) + "/", "--no-color"])
|
|
57
|
+
assert res.exit_code == 1
|
|
58
|
+
assert "contains lakehouse table(s)" in res.output
|
|
59
|
+
assert "orders_delta" in res.output
|
|
60
|
+
assert "shop/orders" in res.output
|
|
61
|
+
assert "Point at a table root" in res.output
|
|
62
|
+
|
|
63
|
+
def test_unknown_extensions_are_skipped_for_inference(self, tmp_path):
|
|
64
|
+
(tmp_path / "catalog.db").write_text("junk")
|
|
65
|
+
(tmp_path / "data.csv").write_text("a\n1\n2\n")
|
|
66
|
+
res = runner.invoke(main, [str(tmp_path) + "/", "--no-color",
|
|
67
|
+
"-s", "dont_show", "-o", "csv"])
|
|
68
|
+
assert res.exit_code == 0
|
|
69
|
+
assert "1" in res.stdout and "2" in res.stdout
|
|
70
|
+
|
|
71
|
+
def test_only_unknown_extensions_gives_clear_error(self, tmp_path):
|
|
72
|
+
(tmp_path / "catalog.db").write_text("junk")
|
|
73
|
+
(tmp_path / "state.lock").write_text("x")
|
|
74
|
+
res = runner.invoke(main, [str(tmp_path) + "/", "--no-color"])
|
|
75
|
+
assert res.exit_code == 1
|
|
76
|
+
assert "no files with a recognized data format" in res.output
|
|
77
|
+
assert "catalog.db" in res.output
|
|
78
|
+
assert "--input-format" in res.output
|
|
79
|
+
|
|
80
|
+
def test_explicit_format_still_forces_reading(self, tmp_path):
|
|
81
|
+
(tmp_path / "catalog.db").write_text("junk")
|
|
82
|
+
(tmp_path / "data.csv").write_text("a\n1\n")
|
|
83
|
+
res = runner.invoke(main, [str(tmp_path) + "/", "--no-color", "-i", "csv",
|
|
84
|
+
"-s", "dont_show", "-o", "csv"])
|
|
85
|
+
assert res.exit_code == 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# --------------------------------------------------------------------------
|
|
89
|
+
# completion hardening (bug 2)
|
|
90
|
+
# --------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
class TestS3RegionFallback:
|
|
93
|
+
def test_client_gets_a_region_when_none_configured(self, monkeypatch, tmp_path):
|
|
94
|
+
for var in ("AWS_DEFAULT_REGION", "AWS_REGION", "AWS_PROFILE"):
|
|
95
|
+
monkeypatch.delenv(var, raising=False)
|
|
96
|
+
monkeypatch.setenv("AWS_CONFIG_FILE", str(tmp_path / "none"))
|
|
97
|
+
monkeypatch.setenv("AWS_SHARED_CREDENTIALS_FILE", str(tmp_path / "none"))
|
|
98
|
+
client = completion._s3_client(None)
|
|
99
|
+
assert client.meta.region_name == "us-east-1"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TestZshHardening:
|
|
103
|
+
def test_emitted_script_bootstraps_compinit(self):
|
|
104
|
+
res = runner.invoke(main, ["--completion", "zsh"])
|
|
105
|
+
assert res.exit_code == 0
|
|
106
|
+
assert "typeset -f compdef" in res.stdout
|
|
107
|
+
assert "compinit" in res.stdout
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestInstallCompletion:
|
|
111
|
+
def test_zsh_install_is_idempotent(self, tmp_path, monkeypatch):
|
|
112
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
113
|
+
first = runner.invoke(main, ["--install-completion", "zsh"])
|
|
114
|
+
second = runner.invoke(main, ["--install-completion", "zsh"])
|
|
115
|
+
assert first.exit_code == 0 and second.exit_code == 0
|
|
116
|
+
content = (tmp_path / ".zshrc").read_text()
|
|
117
|
+
assert content.count('eval "$(cloudcat --completion zsh)"') == 1
|
|
118
|
+
assert "already set up" in second.output
|
|
119
|
+
|
|
120
|
+
def test_bash_install_includes_wordbreaks_fix(self, tmp_path, monkeypatch):
|
|
121
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
122
|
+
res = runner.invoke(main, ["--install-completion", "bash"])
|
|
123
|
+
assert res.exit_code == 0
|
|
124
|
+
content = (tmp_path / ".bashrc").read_text()
|
|
125
|
+
assert "COMP_WORDBREAKS" in content
|
|
126
|
+
assert 'eval "$(cloudcat --completion bash)"' in content
|
|
127
|
+
|
|
128
|
+
def test_fish_install_creates_config_dir(self, tmp_path, monkeypatch):
|
|
129
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
130
|
+
res = runner.invoke(main, ["--install-completion", "fish"])
|
|
131
|
+
assert res.exit_code == 0
|
|
132
|
+
assert "cloudcat --completion fish | source" in \
|
|
133
|
+
(tmp_path / ".config" / "fish" / "config.fish").read_text()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestCompletionDebugLog:
|
|
137
|
+
def test_failures_are_logged_when_enabled(self, tmp_path, monkeypatch):
|
|
138
|
+
monkeypatch.setenv("CLOUDCAT_COMPLETE_DEBUG", "1")
|
|
139
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
140
|
+
from click.shell_completion import ShellComplete
|
|
141
|
+
with patch.object(completion, "_list_s3_buckets", side_effect=RuntimeError("boom")):
|
|
142
|
+
sc = ShellComplete(main, {}, "cloudcat", "_CLOUDCAT_COMPLETE")
|
|
143
|
+
assert sc.get_completions([], "s3://") == []
|
|
144
|
+
log = tmp_path / "cloudcat" / "completion.log"
|
|
145
|
+
assert log.exists()
|
|
146
|
+
assert "boom" in log.read_text()
|
|
147
|
+
|
|
148
|
+
def test_silent_by_default(self, tmp_path, monkeypatch):
|
|
149
|
+
monkeypatch.delenv("CLOUDCAT_COMPLETE_DEBUG", raising=False)
|
|
150
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
|
151
|
+
from click.shell_completion import ShellComplete
|
|
152
|
+
with patch.object(completion, "_list_s3_buckets", side_effect=RuntimeError("boom")):
|
|
153
|
+
sc = ShellComplete(main, {}, "cloudcat", "_CLOUDCAT_COMPLETE")
|
|
154
|
+
sc.get_completions([], "s3://")
|
|
155
|
+
assert not (tmp_path / "cloudcat" / "completion.log").exists()
|
|
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
|
|
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
|