cloudcat 0.3.2__tar.gz → 0.3.3__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 (48) hide show
  1. {cloudcat-0.3.2 → cloudcat-0.3.3}/PKG-INFO +4 -4
  2. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/__init__.py +1 -1
  3. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/cli.py +61 -30
  4. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/config.py +3 -1
  5. cloudcat-0.3.3/cloudcat/progress.py +102 -0
  6. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/storage/__init__.py +3 -2
  7. cloudcat-0.3.3/cloudcat/storage/azure.py +133 -0
  8. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/storage/base.py +1 -1
  9. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/streaming/filesystems.py +13 -14
  10. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat.egg-info/PKG-INFO +4 -4
  11. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat.egg-info/SOURCES.txt +1 -0
  12. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat.egg-info/requires.txt +1 -1
  13. {cloudcat-0.3.2 → cloudcat-0.3.3}/setup.py +3 -3
  14. cloudcat-0.3.2/cloudcat/storage/azure.py +0 -125
  15. {cloudcat-0.3.2 → cloudcat-0.3.3}/LICENSE +0 -0
  16. {cloudcat-0.3.2 → cloudcat-0.3.3}/README.md +0 -0
  17. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/compression.py +0 -0
  18. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/filtering.py +0 -0
  19. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/formatters.py +0 -0
  20. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/readers/__init__.py +0 -0
  21. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/readers/avro.py +0 -0
  22. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/readers/csv.py +0 -0
  23. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/readers/json.py +0 -0
  24. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/readers/orc.py +0 -0
  25. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/readers/parquet.py +0 -0
  26. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/readers/text.py +0 -0
  27. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/storage/gcs.py +0 -0
  28. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/storage/s3.py +0 -0
  29. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/streaming/__init__.py +0 -0
  30. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/streaming/stats.py +0 -0
  31. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat/streaming/tracking.py +0 -0
  32. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat.egg-info/dependency_links.txt +0 -0
  33. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat.egg-info/entry_points.txt +0 -0
  34. {cloudcat-0.3.2 → cloudcat-0.3.3}/cloudcat.egg-info/top_level.txt +0 -0
  35. {cloudcat-0.3.2 → cloudcat-0.3.3}/pyproject.toml +0 -0
  36. {cloudcat-0.3.2 → cloudcat-0.3.3}/setup.cfg +0 -0
  37. {cloudcat-0.3.2 → cloudcat-0.3.3}/tests/__init__.py +0 -0
  38. {cloudcat-0.3.2 → cloudcat-0.3.3}/tests/conftest.py +0 -0
  39. {cloudcat-0.3.2 → cloudcat-0.3.3}/tests/test_cli.py +0 -0
  40. {cloudcat-0.3.2 → cloudcat-0.3.3}/tests/test_compression.py +0 -0
  41. {cloudcat-0.3.2 → cloudcat-0.3.3}/tests/test_config.py +0 -0
  42. {cloudcat-0.3.2 → cloudcat-0.3.3}/tests/test_data_reading.py +0 -0
  43. {cloudcat-0.3.2 → cloudcat-0.3.3}/tests/test_file_filtering.py +0 -0
  44. {cloudcat-0.3.2 → cloudcat-0.3.3}/tests/test_filtering.py +0 -0
  45. {cloudcat-0.3.2 → cloudcat-0.3.3}/tests/test_format_detection.py +0 -0
  46. {cloudcat-0.3.2 → cloudcat-0.3.3}/tests/test_integration.py +0 -0
  47. {cloudcat-0.3.2 → cloudcat-0.3.3}/tests/test_output_formatting.py +0 -0
  48. {cloudcat-0.3.2 → cloudcat-0.3.3}/tests/test_path_parsing.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudcat
3
- Version: 0.3.2
4
- Summary: Preview and analyze data files in Google Cloud Storage, AWS S3, and Azure Blob Storage from your terminal
3
+ Version: 0.3.3
4
+ Summary: Preview and analyze data files in Google Cloud Storage, AWS S3, and Azure Data Lake Storage Gen2 from your terminal
5
5
  Home-page: https://github.com/jonathansudhakar1/cloudcat
6
6
  Author: Jonathan Sudhakar
7
7
  Author-email: jonathan@example.com
@@ -9,7 +9,7 @@ Project-URL: Homepage, https://github.com/jonathansudhakar1/cloudcat
9
9
  Project-URL: Documentation, https://cloudcatcli.com
10
10
  Project-URL: Bug Reports, https://github.com/jonathansudhakar1/cloudcat/issues
11
11
  Project-URL: Source, https://github.com/jonathansudhakar1/cloudcat
12
- Keywords: cloud,gcs,s3,azure,cli,storage,data,parquet,csv,json,avro,orc,google-cloud,aws,azure-blob,data-engineering,etl,spark,bigquery,databricks,kafka,hive
12
+ Keywords: cloud,gcs,s3,azure,cli,storage,data,parquet,csv,json,avro,orc,google-cloud,aws,adls,datalake,data-engineering,etl,spark,bigquery,databricks,kafka,hive
13
13
  Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Intended Audience :: System Administrators
@@ -38,7 +38,7 @@ Requires-Dist: tabulate>=0.8.9
38
38
  Requires-Dist: colorama>=0.4.4
39
39
  Requires-Dist: google-cloud-storage>=2.0.0
40
40
  Requires-Dist: boto3>=1.18.0
41
- Requires-Dist: azure-storage-blob>=12.0.0
41
+ Requires-Dist: azure-storage-file-datalake>=12.0.0
42
42
  Requires-Dist: azure-identity>=1.0.0
43
43
  Provides-Extra: parquet
44
44
  Requires-Dist: pyarrow>=5.0.0; extra == "parquet"
@@ -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.2"
5
+ __version__ = "0.3.3"
@@ -29,6 +29,7 @@ from .compression import (
29
29
  )
30
30
  from .filtering import parse_where_clause, apply_where_filter
31
31
  from .formatters import colorize_json, format_table_with_colored_header
32
+ from .progress import start_progress, update_progress, stop_progress
32
33
  from .storage import (
33
34
  parse_cloud_path,
34
35
  get_stream,
@@ -139,12 +140,14 @@ def get_files_for_multiread(
139
140
  total_size = 0
140
141
 
141
142
  for file_name, file_size in filtered_files:
143
+ # Check if adding this file would exceed the limit
144
+ # Always include at least the first file even if it exceeds the limit
145
+ if selected_files and total_size + file_size > max_size_bytes:
146
+ break
147
+
142
148
  selected_files.append((file_name, file_size))
143
149
  total_size += file_size
144
150
 
145
- if total_size >= max_size_bytes:
146
- break
147
-
148
151
  if not selected_files:
149
152
  raise ValueError(f"No suitable files found in {service}://{bucket}/{prefix}")
150
153
 
@@ -255,7 +258,8 @@ def read_data_from_multiple_files(
255
258
  num_rows: int,
256
259
  columns: Optional[str] = None,
257
260
  delimiter: Optional[str] = None,
258
- offset: int = 0
261
+ offset: int = 0,
262
+ quiet: bool = False
259
263
  ) -> Tuple[pd.DataFrame, pd.Series, int]:
260
264
  """Read data from multiple files and concatenate the results.
261
265
 
@@ -268,6 +272,7 @@ def read_data_from_multiple_files(
268
272
  columns: Columns to select.
269
273
  delimiter: CSV delimiter.
270
274
  offset: Rows to skip.
275
+ quiet: Suppress progress messages.
271
276
 
272
277
  Returns:
273
278
  Tuple of (DataFrame, schema, total_rows).
@@ -278,16 +283,22 @@ def read_data_from_multiple_files(
278
283
  rows_skipped = 0
279
284
  total_rows = 0
280
285
 
281
- def process_file(file_info, remaining_to_skip, remaining_to_read):
286
+ def process_file(file_info, remaining_to_skip, remaining_to_read, file_index, total_files):
282
287
  file_name, file_size = file_info
283
- click.echo(Fore.BLUE + f"Reading file: {file_name} ({file_size/1024:.1f} KB)" + Style.RESET_ALL)
288
+ if not quiet:
289
+ click.echo(Fore.BLUE + f"Reading file: {file_name} ({file_size/1024:.1f} KB)" + Style.RESET_ALL)
290
+ else:
291
+ # Update progress indicator with current file
292
+ short_name = file_name.split('/')[-1]
293
+ update_progress(f"Reading file {file_index + 1}/{total_files}: {short_name}")
284
294
 
285
295
  stream = get_stream(service, bucket, file_name)
286
296
 
287
297
  # Check for compression and decompress if needed
288
298
  compression = detect_compression(file_name)
289
299
  if compression:
290
- click.echo(Fore.BLUE + f"Detected {compression} compression, decompressing..." + Style.RESET_ALL)
300
+ if not quiet:
301
+ click.echo(Fore.BLUE + f"Detected {compression} compression, decompressing..." + Style.RESET_ALL)
291
302
  stream = decompress_stream(stream, compression)
292
303
 
293
304
  # Calculate how many rows to read from this file
@@ -314,10 +325,11 @@ def read_data_from_multiple_files(
314
325
  # Process files in order until we have enough rows
315
326
  remaining_offset = offset
316
327
  remaining_rows = num_rows if num_rows > 0 else float('inf')
328
+ total_files = len(file_list)
317
329
 
318
- for file_info in file_list:
330
+ for file_index, file_info in enumerate(file_list):
319
331
  try:
320
- df, schema, file_rows = process_file(file_info, remaining_offset, remaining_rows if remaining_rows != float('inf') else 0)
332
+ df, schema, file_rows = process_file(file_info, remaining_offset, remaining_rows if remaining_rows != float('inf') else 0, file_index, total_files)
321
333
 
322
334
  if not df.empty:
323
335
  total_rows += len(df)
@@ -460,7 +472,8 @@ def read_data_streaming(
460
472
  aws_profile=cloud_config.aws_profile,
461
473
  gcp_project=cloud_config.gcp_project,
462
474
  gcp_credentials=cloud_config.gcp_credentials,
463
- azure_account=cloud_config.azure_account
475
+ azure_account=cloud_config.azure_account,
476
+ azure_access_key=cloud_config.azure_access_key
464
477
  )
465
478
  pyarrow_path = f"{bucket}/{object_path}"
466
479
 
@@ -709,7 +722,7 @@ def get_record_count_multiple_files(
709
722
 
710
723
  @click.command()
711
724
  @click.version_option(version=__version__, prog_name='cloudcat')
712
- @click.option('--path', '-p', required=True, help='Path to the file or directory (gcs://, s3://, or az://)')
725
+ @click.option('--path', '-p', required=True, help='Path to the file or directory (gcs://, s3://, or abfss://)')
713
726
  @click.option('--output-format', '-o', type=click.Choice(['json', 'jsonp', 'csv', 'table']), default='table',
714
727
  help='Output format (default: table)')
715
728
  @click.option('--input-format', '-i', type=click.Choice(['json', 'csv', 'parquet', 'avro', 'orc', 'text']),
@@ -729,11 +742,11 @@ def get_record_count_multiple_files(
729
742
  @click.option('--profile', help='AWS profile name (for S3 access)')
730
743
  @click.option('--project', help='GCP project ID (for GCS access)')
731
744
  @click.option('--credentials', help='Path to GCP service account JSON file')
732
- @click.option('--account', help='Azure storage account name')
745
+ @click.option('--az-access-key', help='Azure storage account access key')
733
746
  @click.option('--yes', '-y', is_flag=True, help='Skip confirmation prompts (for scripting)')
734
747
  def main(path, output_format, input_format, columns, num_rows, offset, where, schema, count,
735
- multi_file_mode, max_size_mb, delimiter, profile, project, credentials, account, yes):
736
- """Display data from files in Google Cloud Storage, AWS S3, or Azure Blob Storage.
748
+ multi_file_mode, max_size_mb, delimiter, profile, project, credentials, az_access_key, yes):
749
+ """Display data from files in Google Cloud Storage, AWS S3, or Azure Data Lake Storage Gen2.
737
750
 
738
751
  Supported formats: CSV, JSON, Parquet, Avro, ORC, and plain text.
739
752
  Supports compressed files: .gz, .zst, .lz4, .snappy, .bz2
@@ -749,8 +762,8 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
749
762
  cloudcat --path s3://my-bucket/data.parquet --columns id,name,value
750
763
 
751
764
  \b
752
- # Read from Azure Blob Storage
753
- cloudcat --path az://my-container/data.json --output-format jsonp
765
+ # Read from Azure Data Lake Storage Gen2
766
+ cloudcat --path abfss://container@account.dfs.core.windows.net/data.json --output-format jsonp
754
767
 
755
768
  \b
756
769
  # Read Avro files from Kafka exports
@@ -762,7 +775,7 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
762
775
 
763
776
  \b
764
777
  # Read log files as plain text
765
- cloudcat --path az://logs/app.log --input-format text
778
+ cloudcat --path abfss://logs@account.dfs.core.windows.net/app.log --input-format text
766
779
 
767
780
  \b
768
781
  # Read from a directory (reads first non-empty data file)
@@ -804,8 +817,8 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
804
817
  cloudcat --path gcs://bucket/data.csv --credentials /path/to/service-account.json
805
818
 
806
819
  \b
807
- # Use specific Azure storage account
808
- cloudcat --path az://container/data.csv --account mystorageaccount
820
+ # Use Azure storage account access key
821
+ cloudcat --path abfss://container@account.dfs.core.windows.net/data.csv --az-access-key YOUR_KEY
809
822
  """
810
823
  try:
811
824
  # Configure cloud credentials from CLI options
@@ -815,8 +828,8 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
815
828
  cloud_config.gcp_project = project
816
829
  if credentials:
817
830
  cloud_config.gcp_credentials = credentials
818
- if account:
819
- cloud_config.azure_account = account
831
+ if az_access_key:
832
+ cloud_config.azure_access_key = az_access_key
820
833
 
821
834
  # Handle special characters in delimiter
822
835
  if delimiter == "\\t":
@@ -834,37 +847,44 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
834
847
 
835
848
  # Handle directory paths based on multi-file-mode
836
849
  if is_directory:
837
- click.echo(Fore.BLUE + f"Path is a directory" + Style.RESET_ALL)
838
-
839
850
  if multi_file_mode == 'first' or (multi_file_mode == 'auto' and max_size_mb <= 0):
840
851
  # Use a single file
841
- click.echo(Fore.BLUE + f"Looking for first suitable file..." + Style.RESET_ALL)
852
+ start_progress("Listing files...")
842
853
  object_path = find_first_non_empty_file(service, bucket, object_path, input_format)
843
854
 
844
855
  # Determine input format if not specified
845
856
  if not input_format:
846
857
  input_format = detect_format_from_path(object_path)
847
- click.echo(Fore.BLUE + f"Inferred input format: {input_format}" + Style.RESET_ALL)
858
+
859
+ # Get file name for display
860
+ file_name = object_path.split('/')[-1]
861
+ update_progress(f"Reading {file_name}...")
848
862
 
849
863
  # Read the data from the single file with streaming
850
864
  df, full_schema, streaming_stats = read_data_streaming(service, bucket, object_path, input_format, num_rows, columns, delimiter, offset)
851
865
  total_record_count = None # Will be computed later if needed
866
+
867
+ # Stop progress and show format info
868
+ stop_progress()
869
+ click.echo(Fore.BLUE + f"Inferred input format: {input_format}" + Style.RESET_ALL)
852
870
  else:
853
871
  # Read from multiple files
854
- click.echo(Fore.BLUE + f"Reading multiple files (up to {max_size_mb}MB)..." + Style.RESET_ALL)
872
+ start_progress("Listing files...")
855
873
 
856
874
  # Determine input format if not specified (use the first file to infer)
857
875
  if not input_format:
858
876
  first_file = find_first_non_empty_file(service, bucket, object_path)
859
877
  input_format = detect_format_from_path(first_file)
860
- click.echo(Fore.BLUE + f"Inferred input format from first file: {input_format}" + Style.RESET_ALL)
878
+
879
+ update_progress(f"Selecting {input_format} files...")
861
880
 
862
881
  # Get files to read for preview (limited by max_size_mb)
863
- file_list = get_files_for_multiread(service, bucket, object_path, input_format, max_size_mb)
882
+ file_list = get_files_for_multiread(service, bucket, object_path, input_format, max_size_mb, quiet=True)
864
883
 
865
- # Read data from multiple files
884
+ # Read data from multiple files with progress updates
885
+ update_progress(f"Reading {len(file_list)} files...")
866
886
  df, full_schema, rows_in_files = read_data_from_multiple_files(
867
- service, bucket, file_list, input_format, num_rows, columns, delimiter, offset
887
+ service, bucket, file_list, input_format, num_rows, columns, delimiter, offset, quiet=True
868
888
  )
869
889
 
870
890
  # Calculate total size for stats
@@ -874,6 +894,7 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
874
894
  # For --count, get ALL files (not limited by max_size_mb)
875
895
  # so we can count records across the entire directory
876
896
  if count:
897
+ stop_progress()
877
898
  all_files = get_files_for_multiread(service, bucket, object_path, input_format, max_size_mb=999999, quiet=True)
878
899
  all_files_size = sum(f[1] for f in all_files)
879
900
  all_files_size_mb = all_files_size / (1024 * 1024)
@@ -889,8 +910,12 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
889
910
  # Update stats to reflect all files
890
911
  streaming_stats.file_size = all_files_size
891
912
  else:
913
+ stop_progress()
892
914
  multi_file_list = file_list
893
915
 
916
+ # Show format info
917
+ click.echo(Fore.BLUE + f"Inferred input format: {input_format}" + Style.RESET_ALL)
918
+
894
919
  # total_record_count will be computed later if --count is specified
895
920
  total_record_count = None
896
921
 
@@ -904,10 +929,16 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
904
929
  input_format = detect_format_from_path(object_path)
905
930
  click.echo(Fore.BLUE + f"Inferred input format: {input_format}" + Style.RESET_ALL)
906
931
 
932
+ # Get file name for display
933
+ file_name = object_path.split('/')[-1]
934
+ start_progress(f"Reading {file_name}...")
935
+
907
936
  # Read the data with streaming
908
937
  df, full_schema, streaming_stats = read_data_streaming(service, bucket, object_path, input_format, num_rows, columns, delimiter, offset)
909
938
  total_record_count = None # Will be computed later if needed
910
939
 
940
+ stop_progress()
941
+
911
942
  # Apply WHERE filter if specified
912
943
  if where and not df.empty:
913
944
  original_count = len(df)
@@ -14,7 +14,8 @@ class CloudConfig:
14
14
  self.aws_profile: Optional[str] = None
15
15
  self.gcp_project: Optional[str] = None
16
16
  self.gcp_credentials: Optional[str] = None # Path to service account JSON
17
- self.azure_account: Optional[str] = None
17
+ self.azure_account: Optional[str] = None # Extracted from abfss:// URL
18
+ self.azure_access_key: Optional[str] = None # Storage account access key
18
19
 
19
20
  def reset(self) -> None:
20
21
  """Reset all configuration to defaults. Useful for testing."""
@@ -22,6 +23,7 @@ class CloudConfig:
22
23
  self.gcp_project = None
23
24
  self.gcp_credentials = None
24
25
  self.azure_account = None
26
+ self.azure_access_key = None
25
27
 
26
28
 
27
29
  # Global configuration instance
@@ -0,0 +1,102 @@
1
+ """Progress indicator for CLI operations."""
2
+
3
+ import sys
4
+ import threading
5
+ import time
6
+ from typing import Optional
7
+ from colorama import Fore, Style
8
+
9
+
10
+ class ProgressIndicator:
11
+ """A simple animated progress indicator that updates in place.
12
+
13
+ Shows a spinner animation with a status message that updates on a single line.
14
+ """
15
+
16
+ SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
17
+
18
+ def __init__(self):
19
+ self._message: str = ""
20
+ self._running: bool = False
21
+ self._thread: Optional[threading.Thread] = None
22
+ self._frame_index: int = 0
23
+ self._lock = threading.Lock()
24
+
25
+ def start(self, message: str = "Working...") -> None:
26
+ """Start the progress indicator with an initial message."""
27
+ with self._lock:
28
+ self._message = message
29
+ self._running = True
30
+ self._frame_index = 0
31
+
32
+ self._thread = threading.Thread(target=self._animate, daemon=True)
33
+ self._thread.start()
34
+
35
+ def update(self, message: str) -> None:
36
+ """Update the progress message."""
37
+ with self._lock:
38
+ self._message = message
39
+
40
+ def stop(self, final_message: Optional[str] = None) -> None:
41
+ """Stop the progress indicator and optionally show a final message."""
42
+ with self._lock:
43
+ self._running = False
44
+
45
+ if self._thread:
46
+ self._thread.join(timeout=0.5)
47
+ self._thread = None
48
+
49
+ # Clear the current line
50
+ sys.stderr.write('\r' + ' ' * 80 + '\r')
51
+ sys.stderr.flush()
52
+
53
+ # Show final message if provided
54
+ if final_message:
55
+ sys.stderr.write(final_message + '\n')
56
+ sys.stderr.flush()
57
+
58
+ def _animate(self) -> None:
59
+ """Animation loop running in a separate thread."""
60
+ while True:
61
+ with self._lock:
62
+ if not self._running:
63
+ break
64
+ message = self._message
65
+ frame = self.SPINNER_FRAMES[self._frame_index]
66
+ self._frame_index = (self._frame_index + 1) % len(self.SPINNER_FRAMES)
67
+
68
+ # Write spinner and message, overwriting the line
69
+ output = f'\r{Fore.CYAN}{frame}{Style.RESET_ALL} {message}'
70
+ # Pad to clear any leftover characters from longer previous messages
71
+ output = output.ljust(80)
72
+ sys.stderr.write(output)
73
+ sys.stderr.flush()
74
+
75
+ time.sleep(0.08)
76
+
77
+
78
+ # Global instance for easy access
79
+ _progress: Optional[ProgressIndicator] = None
80
+
81
+
82
+ def start_progress(message: str = "Working...") -> None:
83
+ """Start a global progress indicator."""
84
+ global _progress
85
+ if _progress is None:
86
+ _progress = ProgressIndicator()
87
+ _progress.start(message)
88
+
89
+
90
+ def update_progress(message: str) -> None:
91
+ """Update the global progress indicator message."""
92
+ global _progress
93
+ if _progress:
94
+ _progress.update(message)
95
+
96
+
97
+ def stop_progress(final_message: Optional[str] = None) -> None:
98
+ """Stop the global progress indicator."""
99
+ global _progress
100
+ if _progress:
101
+ _progress.stop(final_message)
102
+ _progress = None
@@ -3,7 +3,7 @@
3
3
  from .base import parse_cloud_path, get_stream, list_directory, get_file_size
4
4
  from .gcs import get_gcs_client, get_gcs_stream, list_gcs_directory, HAS_GCS
5
5
  from .s3 import get_s3_client, get_s3_stream, list_s3_directory, HAS_S3
6
- from .azure import get_azure_blob_service_client, get_azure_stream, list_azure_directory, HAS_AZURE
6
+ from .azure import get_azure_datalake_service_client, get_azure_blob_service_client, get_azure_stream, list_azure_directory, HAS_AZURE
7
7
 
8
8
  __all__ = [
9
9
  'parse_cloud_path',
@@ -18,7 +18,8 @@ __all__ = [
18
18
  'get_s3_stream',
19
19
  'list_s3_directory',
20
20
  'HAS_S3',
21
- 'get_azure_blob_service_client',
21
+ 'get_azure_datalake_service_client',
22
+ 'get_azure_blob_service_client', # Alias for backwards compatibility
22
23
  'get_azure_stream',
23
24
  'list_azure_directory',
24
25
  'HAS_AZURE',
@@ -0,0 +1,133 @@
1
+ """Azure Data Lake Storage Gen2 (ADLS Gen2) client and operations."""
2
+
3
+ import io
4
+ import os
5
+ import sys
6
+ from typing import List, Tuple
7
+
8
+ from colorama import Fore, Style
9
+
10
+ from ..config import cloud_config
11
+
12
+ # Try to import Azure Data Lake client
13
+ try:
14
+ from azure.storage.filedatalake import DataLakeServiceClient
15
+ HAS_AZURE = True
16
+ except ImportError:
17
+ DataLakeServiceClient = None
18
+ HAS_AZURE = False
19
+
20
+
21
+ def get_azure_datalake_service_client():
22
+ """Get an Azure DataLakeServiceClient with optional account configuration.
23
+
24
+ Authentication priority:
25
+ 1. Access key (--az-access-key or AZURE_STORAGE_ACCESS_KEY env var)
26
+ 2. DefaultAzureCredential (az login, managed identity, etc.)
27
+
28
+ Returns:
29
+ azure.storage.filedatalake.DataLakeServiceClient instance.
30
+
31
+ Raises:
32
+ SystemExit: If azure-storage-file-datalake is not installed.
33
+ ValueError: If Azure credentials are not configured.
34
+ """
35
+ if not HAS_AZURE:
36
+ sys.stderr.write(
37
+ Fore.RED + "Error: azure-storage-file-datalake package is required for Azure access.\n" +
38
+ "Install it with: pip install azure-storage-file-datalake\n" + Style.RESET_ALL
39
+ )
40
+ sys.exit(1)
41
+
42
+ # Get account name (set by abfss:// URL parsing)
43
+ account_name = cloud_config.azure_account
44
+ if not account_name:
45
+ raise ValueError(
46
+ "Azure storage account not found. Use abfss:// URL format: "
47
+ "abfss://container@account.dfs.core.windows.net/path"
48
+ )
49
+
50
+ account_url = f"https://{account_name}.dfs.core.windows.net"
51
+
52
+ # Check for access key (CLI option or environment variable)
53
+ access_key = cloud_config.azure_access_key or os.environ.get('AZURE_STORAGE_ACCESS_KEY')
54
+
55
+ if access_key:
56
+ # Use access key authentication
57
+ return DataLakeServiceClient(account_url=account_url, credential=access_key)
58
+ else:
59
+ # Fall back to DefaultAzureCredential (az login, managed identity, etc.)
60
+ from azure.identity import DefaultAzureCredential
61
+ credential = DefaultAzureCredential()
62
+ return DataLakeServiceClient(account_url=account_url, credential=credential)
63
+
64
+
65
+ # Keep old function name as alias for backwards compatibility
66
+ get_azure_blob_service_client = get_azure_datalake_service_client
67
+
68
+
69
+ def get_azure_stream(container_name: str, file_path: str) -> io.BytesIO:
70
+ """Get a file stream from Azure Data Lake Storage Gen2.
71
+
72
+ Args:
73
+ container_name: Azure filesystem (container) name.
74
+ file_path: File path within the filesystem.
75
+
76
+ Returns:
77
+ BytesIO buffer containing the file content.
78
+ """
79
+ datalake_service_client = get_azure_datalake_service_client()
80
+ file_system_client = datalake_service_client.get_file_system_client(file_system=container_name)
81
+ file_client = file_system_client.get_file_client(file_path)
82
+
83
+ # Download file to a BytesIO buffer
84
+ buffer = io.BytesIO()
85
+ download = file_client.download_file()
86
+ buffer.write(download.readall())
87
+ buffer.seek(0)
88
+
89
+ return buffer
90
+
91
+
92
+ def get_azure_file_size(container_name: str, file_path: str) -> int:
93
+ """Get the size of an Azure Data Lake file without downloading it.
94
+
95
+ Args:
96
+ container_name: Azure filesystem (container) name.
97
+ file_path: File path within the filesystem.
98
+
99
+ Returns:
100
+ File size in bytes.
101
+ """
102
+ datalake_service_client = get_azure_datalake_service_client()
103
+ file_system_client = datalake_service_client.get_file_system_client(file_system=container_name)
104
+ file_client = file_system_client.get_file_client(file_path)
105
+ properties = file_client.get_file_properties()
106
+ return properties.size
107
+
108
+
109
+ def list_azure_directory(container_name: str, prefix: str) -> List[Tuple[str, int]]:
110
+ """List files in an Azure Data Lake Storage directory.
111
+
112
+ Args:
113
+ container_name: Azure filesystem (container) name.
114
+ prefix: Directory prefix.
115
+
116
+ Returns:
117
+ List of (filename, size) tuples.
118
+ """
119
+ datalake_service_client = get_azure_datalake_service_client()
120
+ file_system_client = datalake_service_client.get_file_system_client(file_system=container_name)
121
+
122
+ # Ensure prefix ends with / to indicate a directory
123
+ if prefix and not prefix.endswith('/'):
124
+ prefix = prefix + '/'
125
+
126
+ # List paths with the prefix
127
+ file_list = []
128
+ paths = file_system_client.get_paths(path=prefix.rstrip('/'))
129
+ for path in paths:
130
+ if not path.is_directory:
131
+ file_list.append((path.name, path.content_length))
132
+
133
+ return file_list
@@ -9,7 +9,7 @@ def parse_cloud_path(path: str) -> Tuple[str, str, str]:
9
9
  """Parse a cloud storage path into service, bucket/container, and object components.
10
10
 
11
11
  Args:
12
- path: Cloud storage URL (gcs://, s3://, az://, or abfss://).
12
+ path: Cloud storage URL (gcs://, s3://, or abfss://).
13
13
 
14
14
  Returns:
15
15
  Tuple of (service, bucket, object_path).
@@ -29,7 +29,8 @@ def get_pyarrow_filesystem(
29
29
  aws_profile: Optional[str] = None,
30
30
  gcp_project: Optional[str] = None,
31
31
  gcp_credentials: Optional[str] = None,
32
- azure_account: Optional[str] = None
32
+ azure_account: Optional[str] = None,
33
+ azure_access_key: Optional[str] = None
33
34
  ) -> Tuple[Any, str]:
34
35
  """Create a PyArrow filesystem for native cloud access.
35
36
 
@@ -42,6 +43,7 @@ def get_pyarrow_filesystem(
42
43
  gcp_project: GCP project ID for GCS.
43
44
  gcp_credentials: Path to GCP service account JSON for GCS.
44
45
  azure_account: Azure storage account name.
46
+ azure_access_key: Azure storage account access key.
45
47
 
46
48
  Returns:
47
49
  Tuple of (filesystem, path_prefix) where path_prefix is used
@@ -62,7 +64,7 @@ def get_pyarrow_filesystem(
62
64
  elif service == 'gcs':
63
65
  return _get_gcs_filesystem(gcp_project, gcp_credentials)
64
66
  elif service == 'azure':
65
- return _get_azure_filesystem(azure_account)
67
+ return _get_azure_filesystem(azure_account, azure_access_key)
66
68
  else:
67
69
  raise ValueError(f"Unsupported service for PyArrow filesystem: {service}")
68
70
 
@@ -140,12 +142,14 @@ def _get_gcs_filesystem(
140
142
 
141
143
 
142
144
  def _get_azure_filesystem(
143
- azure_account: Optional[str] = None
145
+ azure_account: Optional[str] = None,
146
+ azure_access_key: Optional[str] = None
144
147
  ) -> Tuple[Any, str]:
145
- """Create PyArrow AzureFileSystem.
148
+ """Create PyArrow AzureFileSystem for ADLS Gen2.
146
149
 
147
150
  Args:
148
151
  azure_account: Azure storage account name.
152
+ azure_access_key: Azure storage account access key.
149
153
 
150
154
  Returns:
151
155
  Tuple of (AzureFileSystem, path_prefix).
@@ -156,16 +160,11 @@ def _get_azure_filesystem(
156
160
 
157
161
  if azure_account:
158
162
  kwargs['account_name'] = azure_account
159
- else:
160
- # Try to get from environment
161
- account_url = os.environ.get('AZURE_STORAGE_ACCOUNT_URL')
162
- if account_url:
163
- # Extract account name from URL
164
- # Format: https://<account>.blob.core.windows.net
165
- import re
166
- match = re.match(r'https://([^.]+)\.blob\.core\.windows\.net', account_url)
167
- if match:
168
- kwargs['account_name'] = match.group(1)
163
+
164
+ # Check for access key (parameter or environment variable)
165
+ access_key = azure_access_key or os.environ.get('AZURE_STORAGE_ACCESS_KEY')
166
+ if access_key:
167
+ kwargs['account_key'] = access_key
169
168
 
170
169
  filesystem = pa_fs.AzureFileSystem(**kwargs)
171
170
  return filesystem, ''
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudcat
3
- Version: 0.3.2
4
- Summary: Preview and analyze data files in Google Cloud Storage, AWS S3, and Azure Blob Storage from your terminal
3
+ Version: 0.3.3
4
+ Summary: Preview and analyze data files in Google Cloud Storage, AWS S3, and Azure Data Lake Storage Gen2 from your terminal
5
5
  Home-page: https://github.com/jonathansudhakar1/cloudcat
6
6
  Author: Jonathan Sudhakar
7
7
  Author-email: jonathan@example.com
@@ -9,7 +9,7 @@ Project-URL: Homepage, https://github.com/jonathansudhakar1/cloudcat
9
9
  Project-URL: Documentation, https://cloudcatcli.com
10
10
  Project-URL: Bug Reports, https://github.com/jonathansudhakar1/cloudcat/issues
11
11
  Project-URL: Source, https://github.com/jonathansudhakar1/cloudcat
12
- Keywords: cloud,gcs,s3,azure,cli,storage,data,parquet,csv,json,avro,orc,google-cloud,aws,azure-blob,data-engineering,etl,spark,bigquery,databricks,kafka,hive
12
+ Keywords: cloud,gcs,s3,azure,cli,storage,data,parquet,csv,json,avro,orc,google-cloud,aws,adls,datalake,data-engineering,etl,spark,bigquery,databricks,kafka,hive
13
13
  Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Intended Audience :: System Administrators
@@ -38,7 +38,7 @@ Requires-Dist: tabulate>=0.8.9
38
38
  Requires-Dist: colorama>=0.4.4
39
39
  Requires-Dist: google-cloud-storage>=2.0.0
40
40
  Requires-Dist: boto3>=1.18.0
41
- Requires-Dist: azure-storage-blob>=12.0.0
41
+ Requires-Dist: azure-storage-file-datalake>=12.0.0
42
42
  Requires-Dist: azure-identity>=1.0.0
43
43
  Provides-Extra: parquet
44
44
  Requires-Dist: pyarrow>=5.0.0; extra == "parquet"
@@ -8,6 +8,7 @@ cloudcat/compression.py
8
8
  cloudcat/config.py
9
9
  cloudcat/filtering.py
10
10
  cloudcat/formatters.py
11
+ cloudcat/progress.py
11
12
  cloudcat.egg-info/PKG-INFO
12
13
  cloudcat.egg-info/SOURCES.txt
13
14
  cloudcat.egg-info/dependency_links.txt
@@ -4,7 +4,7 @@ tabulate>=0.8.9
4
4
  colorama>=0.4.4
5
5
  google-cloud-storage>=2.0.0
6
6
  boto3>=1.18.0
7
- azure-storage-blob>=12.0.0
7
+ azure-storage-file-datalake>=12.0.0
8
8
  azure-identity>=1.0.0
9
9
 
10
10
  [all]
@@ -30,7 +30,7 @@ setup(
30
30
  # Cloud providers (included by default)
31
31
  "google-cloud-storage>=2.0.0",
32
32
  "boto3>=1.18.0",
33
- "azure-storage-blob>=12.0.0",
33
+ "azure-storage-file-datalake>=12.0.0",
34
34
  "azure-identity>=1.0.0",
35
35
  ],
36
36
  tests_require=[
@@ -60,10 +60,10 @@ setup(
60
60
  },
61
61
  author="Jonathan Sudhakar",
62
62
  author_email="jonathan@example.com",
63
- description="Preview and analyze data files in Google Cloud Storage, AWS S3, and Azure Blob Storage from your terminal",
63
+ description="Preview and analyze data files in Google Cloud Storage, AWS S3, and Azure Data Lake Storage Gen2 from your terminal",
64
64
  long_description=long_description,
65
65
  long_description_content_type="text/markdown",
66
- keywords="cloud, gcs, s3, azure, cli, storage, data, parquet, csv, json, avro, orc, google-cloud, aws, azure-blob, data-engineering, etl, spark, bigquery, databricks, kafka, hive",
66
+ keywords="cloud, gcs, s3, azure, cli, storage, data, parquet, csv, json, avro, orc, google-cloud, aws, adls, datalake, data-engineering, etl, spark, bigquery, databricks, kafka, hive",
67
67
  project_urls={
68
68
  "Homepage": "https://github.com/jonathansudhakar1/cloudcat",
69
69
  "Documentation": "https://cloudcatcli.com",
@@ -1,125 +0,0 @@
1
- """Azure Blob Storage client and operations."""
2
-
3
- import io
4
- import os
5
- import sys
6
- from typing import List, Tuple
7
-
8
- from colorama import Fore, Style
9
-
10
- from ..config import cloud_config
11
-
12
- # Try to import Azure client
13
- try:
14
- from azure.storage.blob import BlobServiceClient, ContainerClient
15
- HAS_AZURE = True
16
- except ImportError:
17
- BlobServiceClient = None
18
- ContainerClient = None
19
- HAS_AZURE = False
20
-
21
-
22
- def get_azure_blob_service_client():
23
- """Get an Azure BlobServiceClient with optional account configuration.
24
-
25
- Returns:
26
- azure.storage.blob.BlobServiceClient instance.
27
-
28
- Raises:
29
- SystemExit: If azure-storage-blob is not installed.
30
- ValueError: If Azure credentials are not configured.
31
- """
32
- if not HAS_AZURE:
33
- sys.stderr.write(
34
- Fore.RED + "Error: azure-storage-blob package is required for Azure access.\n" +
35
- "Install it with: pip install azure-storage-blob\n" + Style.RESET_ALL
36
- )
37
- sys.exit(1)
38
-
39
- # Check for explicit account override
40
- if cloud_config.azure_account:
41
- account_url = f"https://{cloud_config.azure_account}.blob.core.windows.net"
42
- from azure.identity import DefaultAzureCredential
43
- credential = DefaultAzureCredential()
44
- return BlobServiceClient(account_url=account_url, credential=credential)
45
-
46
- # Fall back to environment variables
47
- connection_string = os.environ.get('AZURE_STORAGE_CONNECTION_STRING')
48
- account_url = os.environ.get('AZURE_STORAGE_ACCOUNT_URL')
49
-
50
- if connection_string:
51
- return BlobServiceClient.from_connection_string(connection_string)
52
- elif account_url:
53
- from azure.identity import DefaultAzureCredential
54
- credential = DefaultAzureCredential()
55
- return BlobServiceClient(account_url=account_url, credential=credential)
56
- else:
57
- raise ValueError(
58
- "Azure credentials not found. Use --account, or set "
59
- "AZURE_STORAGE_CONNECTION_STRING or AZURE_STORAGE_ACCOUNT_URL environment variable."
60
- )
61
-
62
-
63
- def get_azure_stream(container_name: str, blob_name: str) -> io.BytesIO:
64
- """Get a file stream from Azure Blob Storage.
65
-
66
- Args:
67
- container_name: Azure container name.
68
- blob_name: Blob path within the container.
69
-
70
- Returns:
71
- BytesIO buffer containing the file content.
72
- """
73
- blob_service_client = get_azure_blob_service_client()
74
- blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_name)
75
-
76
- # Download blob to a BytesIO buffer
77
- buffer = io.BytesIO()
78
- download_stream = blob_client.download_blob()
79
- buffer.write(download_stream.readall())
80
- buffer.seek(0)
81
-
82
- return buffer
83
-
84
-
85
- def get_azure_file_size(container_name: str, blob_name: str) -> int:
86
- """Get the size of an Azure blob without downloading it.
87
-
88
- Args:
89
- container_name: Azure container name.
90
- blob_name: Blob path within the container.
91
-
92
- Returns:
93
- File size in bytes.
94
- """
95
- blob_service_client = get_azure_blob_service_client()
96
- blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_name)
97
- properties = blob_client.get_blob_properties()
98
- return properties.size
99
-
100
-
101
- def list_azure_directory(container_name: str, prefix: str) -> List[Tuple[str, int]]:
102
- """List files in an Azure Blob Storage container directory.
103
-
104
- Args:
105
- container_name: Azure container name.
106
- prefix: Directory prefix.
107
-
108
- Returns:
109
- List of (filename, size) tuples.
110
- """
111
- blob_service_client = get_azure_blob_service_client()
112
- container_client = blob_service_client.get_container_client(container_name)
113
-
114
- # Ensure prefix ends with / to indicate a directory
115
- if prefix and not prefix.endswith('/'):
116
- prefix = prefix + '/'
117
-
118
- # List blobs with the prefix
119
- file_list = []
120
- blobs = container_client.list_blobs(name_starts_with=prefix)
121
- for blob in blobs:
122
- if not blob.name.endswith('/'):
123
- file_list.append((blob.name, blob.size))
124
-
125
- return file_list
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