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.
Files changed (46) hide show
  1. {cloudcat-0.3.0 → cloudcat-0.3.1}/PKG-INFO +1 -1
  2. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/__init__.py +1 -1
  3. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/cli.py +64 -7
  4. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/compression.py +21 -0
  5. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat.egg-info/PKG-INFO +1 -1
  6. {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_compression.py +24 -1
  7. {cloudcat-0.3.0 → cloudcat-0.3.1}/LICENSE +0 -0
  8. {cloudcat-0.3.0 → cloudcat-0.3.1}/README.md +0 -0
  9. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/config.py +0 -0
  10. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/filtering.py +0 -0
  11. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/formatters.py +0 -0
  12. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/__init__.py +0 -0
  13. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/avro.py +0 -0
  14. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/csv.py +0 -0
  15. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/json.py +0 -0
  16. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/orc.py +0 -0
  17. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/parquet.py +0 -0
  18. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/readers/text.py +0 -0
  19. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/storage/__init__.py +0 -0
  20. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/storage/azure.py +0 -0
  21. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/storage/base.py +0 -0
  22. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/storage/gcs.py +0 -0
  23. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/storage/s3.py +0 -0
  24. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/streaming/__init__.py +0 -0
  25. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/streaming/filesystems.py +0 -0
  26. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/streaming/stats.py +0 -0
  27. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat/streaming/tracking.py +0 -0
  28. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat.egg-info/SOURCES.txt +0 -0
  29. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat.egg-info/dependency_links.txt +0 -0
  30. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat.egg-info/entry_points.txt +0 -0
  31. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat.egg-info/requires.txt +0 -0
  32. {cloudcat-0.3.0 → cloudcat-0.3.1}/cloudcat.egg-info/top_level.txt +0 -0
  33. {cloudcat-0.3.0 → cloudcat-0.3.1}/pyproject.toml +0 -0
  34. {cloudcat-0.3.0 → cloudcat-0.3.1}/setup.cfg +0 -0
  35. {cloudcat-0.3.0 → cloudcat-0.3.1}/setup.py +0 -0
  36. {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/__init__.py +0 -0
  37. {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/conftest.py +0 -0
  38. {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_cli.py +0 -0
  39. {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_config.py +0 -0
  40. {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_data_reading.py +0 -0
  41. {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_file_filtering.py +0 -0
  42. {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_filtering.py +0 -0
  43. {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_format_detection.py +0 -0
  44. {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_integration.py +0 -0
  45. {cloudcat-0.3.0 → cloudcat-0.3.1}/tests/test_output_formatting.py +0 -0
  46. {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.0
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
@@ -2,4 +2,4 @@
2
2
  #!/usr/bin/env python
3
3
  """cloudcat - A CLI utility to read and display files from cloud storage."""
4
4
 
5
- __version__ = "0.3.0"
5
+ __version__ = "0.3.1"
@@ -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
- click.echo(Fore.YELLOW + "Counting records (this might take a while for large files)..." + Style.RESET_ALL)
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, total_record_count = read_data_from_multiple_files(
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
- total_record_count = get_record_count(service, bucket, object_path, input_format, delimiter)
864
- click.echo(Fore.CYAN + f"Total records: {total_record_count}" + Style.RESET_ALL)
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
- total_record_count = get_record_count(service, bucket, object_path, input_format, delimiter)
888
- click.echo(Fore.CYAN + f"\nTotal records: {total_record_count}" + Style.RESET_ALL)
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.0
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("path/to/file.PARQUET.SNAPPY") == "snappy"
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