cloudcat 0.3.0__tar.gz → 0.3.1__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.3.0 → cloudcat-0.3.1}/PKG-INFO +1 -1
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/__init__.py +1 -1
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/cli.py +64 -7
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/compression.py +21 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat.egg-info/PKG-INFO +1 -1
- {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_compression.py +24 -1
- {cloudcat-0.3.0 → cloudcat-0.3.1}/LICENSE +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/README.md +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/config.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/filtering.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/formatters.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/__init__.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/avro.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/csv.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/json.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/orc.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/parquet.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/text.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/storage/__init__.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/storage/azure.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/storage/base.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/storage/gcs.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/storage/s3.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/streaming/__init__.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/streaming/filesystems.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/streaming/stats.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/streaming/tracking.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat.egg-info/SOURCES.txt +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat.egg-info/dependency_links.txt +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat.egg-info/entry_points.txt +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat.egg-info/requires.txt +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat.egg-info/top_level.txt +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/pyproject.toml +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/setup.cfg +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/setup.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/__init__.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/conftest.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_cli.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_config.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_data_reading.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_file_filtering.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_filtering.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_format_detection.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_integration.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_output_formatting.py +0 -0
- {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_path_parsing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudcat
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Preview and analyze data files in Google Cloud Storage, AWS S3, and Azure Blob Storage from your terminal
|
|
5
5
|
Home-page: https://github.com/jonathansudhakar1/cloudcat
|
|
6
6
|
Author: Jonathan Sudhakar
|
|
@@ -538,7 +538,8 @@ def get_record_count(
|
|
|
538
538
|
bucket: str,
|
|
539
539
|
object_path: str,
|
|
540
540
|
input_format: str,
|
|
541
|
-
delimiter: Optional[str] = None
|
|
541
|
+
delimiter: Optional[str] = None,
|
|
542
|
+
quiet: bool = False
|
|
542
543
|
):
|
|
543
544
|
"""Get record count from a file.
|
|
544
545
|
|
|
@@ -548,6 +549,7 @@ def get_record_count(
|
|
|
548
549
|
object_path: Object path.
|
|
549
550
|
input_format: Data format.
|
|
550
551
|
delimiter: CSV delimiter.
|
|
552
|
+
quiet: If True, suppress progress messages.
|
|
551
553
|
|
|
552
554
|
Returns:
|
|
553
555
|
Record count (int) or "Unknown" on failure.
|
|
@@ -582,7 +584,8 @@ def get_record_count(
|
|
|
582
584
|
pass # Ignore cleanup errors
|
|
583
585
|
else:
|
|
584
586
|
# For CSV and JSON, we need to count the rows
|
|
585
|
-
|
|
587
|
+
if not quiet:
|
|
588
|
+
click.echo(Fore.YELLOW + "Counting records (this might take a while for large files)..." + Style.RESET_ALL)
|
|
586
589
|
|
|
587
590
|
stream = get_stream(service, bucket, object_path)
|
|
588
591
|
if compression:
|
|
@@ -665,6 +668,42 @@ def get_record_count(
|
|
|
665
668
|
return "Unknown"
|
|
666
669
|
|
|
667
670
|
|
|
671
|
+
def get_record_count_multiple_files(
|
|
672
|
+
service: str,
|
|
673
|
+
bucket: str,
|
|
674
|
+
file_list: List[Tuple[str, int]],
|
|
675
|
+
input_format: str,
|
|
676
|
+
delimiter: Optional[str] = None
|
|
677
|
+
):
|
|
678
|
+
"""Get total record count across multiple files.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
service: Cloud service identifier.
|
|
682
|
+
bucket: Bucket or container name.
|
|
683
|
+
file_list: List of (filename, size) tuples.
|
|
684
|
+
input_format: Data format.
|
|
685
|
+
delimiter: CSV delimiter.
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
Total record count (int) or "Unknown" on failure.
|
|
689
|
+
"""
|
|
690
|
+
click.echo(Fore.YELLOW + f"Counting records across {len(file_list)} files..." + Style.RESET_ALL)
|
|
691
|
+
total_count = 0
|
|
692
|
+
|
|
693
|
+
for file_name, file_size in file_list:
|
|
694
|
+
try:
|
|
695
|
+
count = get_record_count(service, bucket, file_name, input_format, delimiter, quiet=True)
|
|
696
|
+
if isinstance(count, int):
|
|
697
|
+
total_count += count
|
|
698
|
+
click.echo(Fore.BLUE + f" {file_name}: {count:,} records" + Style.RESET_ALL)
|
|
699
|
+
else:
|
|
700
|
+
click.echo(Fore.YELLOW + f" {file_name}: {count}" + Style.RESET_ALL)
|
|
701
|
+
except Exception as e:
|
|
702
|
+
click.echo(Fore.YELLOW + f" {file_name}: Error - {str(e)}" + Style.RESET_ALL)
|
|
703
|
+
|
|
704
|
+
return total_count
|
|
705
|
+
|
|
706
|
+
|
|
668
707
|
@click.command()
|
|
669
708
|
@click.version_option(version=__version__, prog_name='cloudcat')
|
|
670
709
|
@click.option('--path', '-p', required=True, help='Path to the file or directory (gcs://, s3://, or az://)')
|
|
@@ -787,6 +826,7 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
|
|
|
787
826
|
|
|
788
827
|
# Initialize streaming stats
|
|
789
828
|
streaming_stats = None
|
|
829
|
+
multi_file_list = None # For directory reads with --count
|
|
790
830
|
|
|
791
831
|
# Handle directory paths based on multi-file-mode
|
|
792
832
|
if is_directory:
|
|
@@ -819,7 +859,7 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
|
|
|
819
859
|
file_list = get_files_for_multiread(service, bucket, object_path, input_format, max_size_mb)
|
|
820
860
|
|
|
821
861
|
# Read data from multiple files
|
|
822
|
-
df, full_schema,
|
|
862
|
+
df, full_schema, rows_in_files = read_data_from_multiple_files(
|
|
823
863
|
service, bucket, file_list, input_format, num_rows, columns, delimiter, offset
|
|
824
864
|
)
|
|
825
865
|
|
|
@@ -827,6 +867,11 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
|
|
|
827
867
|
total_size = sum(f[1] for f in file_list)
|
|
828
868
|
streaming_stats = StreamingStats(file_size=total_size, bytes_read=total_size, format_type=input_format)
|
|
829
869
|
|
|
870
|
+
# Store file_list for --count to use later
|
|
871
|
+
multi_file_list = file_list
|
|
872
|
+
# total_record_count will be computed later if --count is specified
|
|
873
|
+
total_record_count = None
|
|
874
|
+
|
|
830
875
|
# Update object_path for display/logging purposes
|
|
831
876
|
object_path = f"{object_path} ({len(file_list)} files)"
|
|
832
877
|
else:
|
|
@@ -860,8 +905,13 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
|
|
|
860
905
|
if count:
|
|
861
906
|
try:
|
|
862
907
|
if total_record_count is None:
|
|
863
|
-
|
|
864
|
-
|
|
908
|
+
if multi_file_list:
|
|
909
|
+
total_record_count = get_record_count_multiple_files(
|
|
910
|
+
service, bucket, multi_file_list, input_format, delimiter
|
|
911
|
+
)
|
|
912
|
+
else:
|
|
913
|
+
total_record_count = get_record_count(service, bucket, object_path, input_format, delimiter)
|
|
914
|
+
click.echo(Fore.CYAN + f"Total records: {total_record_count:,}" + Style.RESET_ALL)
|
|
865
915
|
except Exception as e:
|
|
866
916
|
click.echo(Fore.YELLOW + f"Could not count records: {str(e)}" + Style.RESET_ALL)
|
|
867
917
|
return
|
|
@@ -884,8 +934,15 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
|
|
|
884
934
|
if count:
|
|
885
935
|
try:
|
|
886
936
|
if total_record_count is None:
|
|
887
|
-
|
|
888
|
-
|
|
937
|
+
if multi_file_list:
|
|
938
|
+
# Count across all files in the directory
|
|
939
|
+
total_record_count = get_record_count_multiple_files(
|
|
940
|
+
service, bucket, multi_file_list, input_format, delimiter
|
|
941
|
+
)
|
|
942
|
+
else:
|
|
943
|
+
# Single file count
|
|
944
|
+
total_record_count = get_record_count(service, bucket, object_path, input_format, delimiter)
|
|
945
|
+
click.echo(Fore.CYAN + f"\nTotal records: {total_record_count:,}" + Style.RESET_ALL)
|
|
889
946
|
except Exception as e:
|
|
890
947
|
click.echo(Fore.YELLOW + f"\nCould not count records: {str(e)}" + Style.RESET_ALL)
|
|
891
948
|
|
|
@@ -33,6 +33,10 @@ except ImportError:
|
|
|
33
33
|
def detect_compression(path: str) -> Optional[str]:
|
|
34
34
|
"""Detect compression type from file extension.
|
|
35
35
|
|
|
36
|
+
For Parquet and ORC files, snappy is an internal codec handled by PyArrow,
|
|
37
|
+
not external compression. Files like .parquet.snappy should be read directly
|
|
38
|
+
by PyArrow without external decompression.
|
|
39
|
+
|
|
36
40
|
Args:
|
|
37
41
|
path: File path to check for compression extension.
|
|
38
42
|
|
|
@@ -40,6 +44,12 @@ def detect_compression(path: str) -> Optional[str]:
|
|
|
40
44
|
Compression type string ('gzip', 'zstd', 'lz4', 'snappy', 'bz2') or None.
|
|
41
45
|
"""
|
|
42
46
|
path_lower = path.lower()
|
|
47
|
+
|
|
48
|
+
# For Parquet and ORC, snappy is an internal codec - PyArrow handles it
|
|
49
|
+
# Files like .parquet.snappy or .orc.snappy don't need external decompression
|
|
50
|
+
if path_lower.endswith('.parquet.snappy') or path_lower.endswith('.orc.snappy'):
|
|
51
|
+
return None
|
|
52
|
+
|
|
43
53
|
if path_lower.endswith('.gz') or path_lower.endswith('.gzip'):
|
|
44
54
|
return 'gzip'
|
|
45
55
|
elif path_lower.endswith('.zst') or path_lower.endswith('.zstd'):
|
|
@@ -101,6 +111,9 @@ def decompress_stream(stream: Union[BinaryIO, bytes], compression: str) -> io.By
|
|
|
101
111
|
def strip_compression_extension(path: str) -> str:
|
|
102
112
|
"""Remove compression extension from path to get the actual file extension.
|
|
103
113
|
|
|
114
|
+
For Parquet and ORC files with .snappy extension (e.g., .parquet.snappy),
|
|
115
|
+
snappy is an internal codec, so we strip .snappy to get .parquet.
|
|
116
|
+
|
|
104
117
|
Args:
|
|
105
118
|
path: File path that may have a compression extension.
|
|
106
119
|
|
|
@@ -108,6 +121,14 @@ def strip_compression_extension(path: str) -> str:
|
|
|
108
121
|
Path with compression extension removed.
|
|
109
122
|
"""
|
|
110
123
|
path_lower = path.lower()
|
|
124
|
+
|
|
125
|
+
# For .parquet.snappy and .orc.snappy, strip .snappy to get the format
|
|
126
|
+
# (snappy is internal codec, not external compression)
|
|
127
|
+
if path_lower.endswith('.parquet.snappy'):
|
|
128
|
+
return path[:-len('.snappy')]
|
|
129
|
+
if path_lower.endswith('.orc.snappy'):
|
|
130
|
+
return path[:-len('.snappy')]
|
|
131
|
+
|
|
111
132
|
for ext in COMPRESSION_EXTENSIONS:
|
|
112
133
|
if path_lower.endswith(ext):
|
|
113
134
|
return path[:-len(ext)]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudcat
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Preview and analyze data files in Google Cloud Storage, AWS S3, and Azure Blob Storage from your terminal
|
|
5
5
|
Home-page: https://github.com/jonathansudhakar1/cloudcat
|
|
6
6
|
Author: Jonathan Sudhakar
|
|
@@ -32,7 +32,18 @@ class TestCompressionDetection:
|
|
|
32
32
|
|
|
33
33
|
def test_snappy_detection(self):
|
|
34
34
|
assert detect_compression("data.csv.snappy") == "snappy"
|
|
35
|
-
assert detect_compression("
|
|
35
|
+
assert detect_compression("data.json.snappy") == "snappy"
|
|
36
|
+
|
|
37
|
+
def test_parquet_snappy_is_internal_codec(self):
|
|
38
|
+
# For Parquet files, .snappy is an internal codec handled by PyArrow
|
|
39
|
+
# Not external compression - PyArrow reads these directly
|
|
40
|
+
assert detect_compression("path/to/file.parquet.snappy") is None
|
|
41
|
+
assert detect_compression("data.PARQUET.SNAPPY") is None
|
|
42
|
+
|
|
43
|
+
def test_orc_snappy_is_internal_codec(self):
|
|
44
|
+
# For ORC files, .snappy is an internal codec handled by PyArrow
|
|
45
|
+
assert detect_compression("path/to/file.orc.snappy") is None
|
|
46
|
+
assert detect_compression("data.ORC.SNAPPY") is None
|
|
36
47
|
|
|
37
48
|
def test_bz2_detection(self):
|
|
38
49
|
assert detect_compression("data.csv.bz2") == "bz2"
|
|
@@ -66,6 +77,18 @@ class TestStripCompressionExtension:
|
|
|
66
77
|
|
|
67
78
|
def test_strip_snappy(self):
|
|
68
79
|
assert strip_compression_extension("data.csv.snappy") == "data.csv"
|
|
80
|
+
assert strip_compression_extension("data.json.snappy") == "data.json"
|
|
81
|
+
|
|
82
|
+
def test_strip_parquet_snappy(self):
|
|
83
|
+
# For .parquet.snappy, strip .snappy to get .parquet format
|
|
84
|
+
# (snappy is internal codec, not external compression)
|
|
85
|
+
assert strip_compression_extension("data.parquet.snappy") == "data.parquet"
|
|
86
|
+
assert strip_compression_extension("path/to/file.PARQUET.SNAPPY") == "path/to/file.PARQUET"
|
|
87
|
+
|
|
88
|
+
def test_strip_orc_snappy(self):
|
|
89
|
+
# For .orc.snappy, strip .snappy to get .orc format
|
|
90
|
+
assert strip_compression_extension("data.orc.snappy") == "data.orc"
|
|
91
|
+
assert strip_compression_extension("path/to/file.ORC.SNAPPY") == "path/to/file.ORC"
|
|
69
92
|
|
|
70
93
|
def test_strip_bz2(self):
|
|
71
94
|
assert strip_compression_extension("data.csv.bz2") == "data.csv"
|
|
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
|