cloudcat 0.2.8__tar.gz → 0.3.0__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 (53) hide show
  1. {cloudcat-0.2.8 → cloudcat-0.3.0}/PKG-INFO +38 -15
  2. {cloudcat-0.2.8 → cloudcat-0.3.0}/README.md +37 -14
  3. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat/__init__.py +1 -1
  4. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat/cli.py +155 -27
  5. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat/compression.py +81 -2
  6. cloudcat-0.3.0/cloudcat/readers/__init__.py +26 -0
  7. cloudcat-0.3.0/cloudcat/readers/avro.py +144 -0
  8. cloudcat-0.3.0/cloudcat/readers/csv.py +150 -0
  9. cloudcat-0.3.0/cloudcat/readers/json.py +254 -0
  10. cloudcat-0.3.0/cloudcat/readers/orc.py +188 -0
  11. cloudcat-0.3.0/cloudcat/readers/parquet.py +282 -0
  12. cloudcat-0.3.0/cloudcat/readers/text.py +123 -0
  13. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat/storage/__init__.py +2 -1
  14. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat/storage/azure.py +16 -0
  15. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat/storage/base.py +45 -2
  16. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat/storage/gcs.py +17 -0
  17. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat/storage/s3.py +15 -0
  18. cloudcat-0.3.0/cloudcat/streaming/__init__.py +13 -0
  19. cloudcat-0.3.0/cloudcat/streaming/filesystems.py +186 -0
  20. cloudcat-0.3.0/cloudcat/streaming/stats.py +96 -0
  21. cloudcat-0.3.0/cloudcat/streaming/tracking.py +231 -0
  22. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat.egg-info/PKG-INFO +38 -15
  23. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat.egg-info/SOURCES.txt +4 -0
  24. {cloudcat-0.2.8 → cloudcat-0.3.0}/tests/test_cli.py +73 -59
  25. {cloudcat-0.2.8 → cloudcat-0.3.0}/tests/test_path_parsing.py +21 -1
  26. cloudcat-0.2.8/cloudcat/readers/__init__.py +0 -20
  27. cloudcat-0.2.8/cloudcat/readers/avro.py +0 -77
  28. cloudcat-0.2.8/cloudcat/readers/csv.py +0 -53
  29. cloudcat-0.2.8/cloudcat/readers/json.py +0 -102
  30. cloudcat-0.2.8/cloudcat/readers/orc.py +0 -86
  31. cloudcat-0.2.8/cloudcat/readers/parquet.py +0 -115
  32. cloudcat-0.2.8/cloudcat/readers/text.py +0 -57
  33. {cloudcat-0.2.8 → cloudcat-0.3.0}/LICENSE +0 -0
  34. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat/config.py +0 -0
  35. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat/filtering.py +0 -0
  36. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat/formatters.py +0 -0
  37. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat.egg-info/dependency_links.txt +0 -0
  38. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat.egg-info/entry_points.txt +0 -0
  39. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat.egg-info/requires.txt +0 -0
  40. {cloudcat-0.2.8 → cloudcat-0.3.0}/cloudcat.egg-info/top_level.txt +0 -0
  41. {cloudcat-0.2.8 → cloudcat-0.3.0}/pyproject.toml +0 -0
  42. {cloudcat-0.2.8 → cloudcat-0.3.0}/setup.cfg +0 -0
  43. {cloudcat-0.2.8 → cloudcat-0.3.0}/setup.py +0 -0
  44. {cloudcat-0.2.8 → cloudcat-0.3.0}/tests/__init__.py +0 -0
  45. {cloudcat-0.2.8 → cloudcat-0.3.0}/tests/conftest.py +0 -0
  46. {cloudcat-0.2.8 → cloudcat-0.3.0}/tests/test_compression.py +0 -0
  47. {cloudcat-0.2.8 → cloudcat-0.3.0}/tests/test_config.py +0 -0
  48. {cloudcat-0.2.8 → cloudcat-0.3.0}/tests/test_data_reading.py +0 -0
  49. {cloudcat-0.2.8 → cloudcat-0.3.0}/tests/test_file_filtering.py +0 -0
  50. {cloudcat-0.2.8 → cloudcat-0.3.0}/tests/test_filtering.py +0 -0
  51. {cloudcat-0.2.8 → cloudcat-0.3.0}/tests/test_format_detection.py +0 -0
  52. {cloudcat-0.2.8 → cloudcat-0.3.0}/tests/test_integration.py +0 -0
  53. {cloudcat-0.2.8 → cloudcat-0.3.0}/tests/test_output_formatting.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudcat
3
- Version: 0.2.8
3
+ Version: 0.3.0
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
@@ -89,7 +89,7 @@ Dynamic: summary
89
89
  <p align="center">
90
90
  <a href="https://pypi.org/project/cloudcat/"><img src="https://img.shields.io/pypi/v/cloudcat.svg?style=flat-square&logo=pypi&logoColor=white" alt="PyPI version"></a>
91
91
  <a href="https://pypi.org/project/cloudcat/"><img src="https://img.shields.io/pypi/pyversions/cloudcat.svg?style=flat-square&logo=python&logoColor=white" alt="Python versions"></a>
92
- <a href="https://pypi.org/project/cloudcat/"><img src="https://img.shields.io/pypi/dm/cloudcat.svg?style=flat-square&logo=pypi&logoColor=white" alt="PyPI Downloads"></a>
92
+ <a href="https://pepy.tech/projects/cloudcat"><img src="https://static.pepy.tech/personalized-badge/cloudcat?period=total&units=international_system&left_color=black&right_color=green&left_text=downloads" alt="PyPI Downloads"></a>
93
93
  <a href="https://github.com/jonathansudhakar1/cloudcat/releases"><img src="https://img.shields.io/github/downloads/jonathansudhakar1/cloudcat/total.svg?style=flat-square&logo=homebrew&logoColor=white&label=homebrew" alt="Homebrew Downloads"></a>
94
94
  <a href="https://github.com/jonathansudhakar1/cloudcat/blob/main/LICENSE"><img src="https://img.shields.io/github/license/jonathansudhakar1/cloudcat.svg?style=flat-square" alt="License"></a>
95
95
  </p>
@@ -220,19 +220,42 @@ cloudcat -p gcs://bucket/data.csv --offset 100 -n 10
220
220
  | Google Cloud Storage | `gcs://` or `gs://` | ✅ Supported |
221
221
  | Amazon S3 | `s3://` | ✅ Supported |
222
222
  | Azure Blob Storage | `az://` or `azure://` | ✅ Supported |
223
+ | Azure Data Lake Gen2 | `abfss://` | ✅ Supported |
223
224
 
224
225
  ### File Format Support
225
226
 
226
- | Format | Read | Auto-Detect | Streaming | Use Case |
227
- |--------|------|-------------|-----------|----------|
228
- | CSV | ✅ | ✅ | ✅ | General data files |
229
- | JSON | ✅ | ✅ | ✅ | API responses, configs |
230
- | JSON Lines | ✅ | ✅ | ✅ | Log files, streaming data |
231
- | Parquet | ✅ | ✅ | ✅ | Spark/analytics data |
232
- | Avro | ✅ | ✅ | ✅ | Kafka, data pipelines |
233
- | ORC | ✅ | ✅ | ✅ | Hive, Hadoop ecosystem |
234
- | Text | ✅ | ✅ | ✅ | Log files, plain text |
235
- | TSV | ✅ | Via `--delimiter` | ✅ | Tab-separated data |
227
+ | Format | Auto-Detect | Use Case |
228
+ |--------|-------------|----------|
229
+ | CSV | ✅ | General data files |
230
+ | JSON | ✅ | API responses, configs |
231
+ | JSON Lines | ✅ | Log files, streaming data |
232
+ | Parquet | ✅ | Spark/analytics data |
233
+ | Avro | ✅ | Kafka, data pipelines |
234
+ | ORC | ✅ | Hive, Hadoop ecosystem |
235
+ | Text | ✅ | Log files, plain text |
236
+ | TSV | Via `--delimiter` | Tab-separated data |
237
+
238
+ ### Streaming Efficiency
239
+
240
+ CloudCat uses intelligent streaming to minimize data transfer and egress costs:
241
+
242
+ | Format | Compression | Streams | Column Projection | Early Row Stop |
243
+ |--------|-------------|---------|-------------------|----------------|
244
+ | Parquet | None/Internal | ✅ | ✅ Range requests | ✅ |
245
+ | Parquet | External (.gz) | ❌ | ❌ | ❌ |
246
+ | ORC | None/Internal | ❌ | ❌ | ❌ |
247
+ | ORC | External (.gz) | ❌ | ❌ | ❌ |
248
+ | CSV | None | ✅ | ❌ | ✅ |
249
+ | CSV | gzip/zstd/lz4/bz2 | ✅ | ❌ | ✅ |
250
+ | CSV | snappy | ❌ | ❌ | ❌ |
251
+ | JSON Lines | None/streamable | ✅ | ❌ | ✅ |
252
+ | JSON Array | Any | ❌ | ❌ | ❌ |
253
+ | Avro | Any | ✅ | ✅ Record-level | ✅ |
254
+ | Text | Any streamable | ✅ | N/A | ✅ |
255
+
256
+ - **Streams**: Only reads data as needed, stops early when row limit is reached
257
+ - **Column Projection**: For Parquet, only fetches required column chunks via HTTP range requests
258
+ - **Early Row Stop**: Stops reading when `--num-rows` limit is reached
236
259
 
237
260
  ### Compression Support
238
261
 
@@ -439,7 +462,7 @@ cloudcat -p s3://logs/errors/ -o json | grep "ERROR"
439
462
  cloudcat -p gcs://exports/daily/users.csv -s show
440
463
 
441
464
  # Verify record count
442
- cloudcat -p s3://warehouse/transactions.parquet --no-count
465
+ cloudcat -p s3://warehouse/transactions.parquet --count
443
466
  ```
444
467
 
445
468
  #### Format Conversion
@@ -483,7 +506,7 @@ Options:
483
506
  -s, --schema TEXT Schema display: show, dont_show, schema_only
484
507
  [default: show]
485
508
 
486
- --no-count Disable automatic record counting
509
+ --count Show total record count (scans entire file)
487
510
 
488
511
  -m, --multi-file-mode TEXT Directory handling: auto, first, all
489
512
  [default: auto]
@@ -579,7 +602,7 @@ cloudcat -p az://container/data.csv --account mystorageaccount
579
602
 
580
603
  ## Performance Tips
581
604
 
582
- 1. **Use `--no-count`** for large files when you don't need the total record count
605
+ 1. **Counting is off by default** — use `--count` only when you need the total record count
583
606
  2. **Prefer Parquet** format when possible — record counts are instant from metadata
584
607
  3. **Use `--num-rows`** to limit data transfer for large files
585
608
  4. **Use `--columns`** to select only needed columns (especially effective with Parquet)
@@ -11,7 +11,7 @@
11
11
  <p align="center">
12
12
  <a href="https://pypi.org/project/cloudcat/"><img src="https://img.shields.io/pypi/v/cloudcat.svg?style=flat-square&logo=pypi&logoColor=white" alt="PyPI version"></a>
13
13
  <a href="https://pypi.org/project/cloudcat/"><img src="https://img.shields.io/pypi/pyversions/cloudcat.svg?style=flat-square&logo=python&logoColor=white" alt="Python versions"></a>
14
- <a href="https://pypi.org/project/cloudcat/"><img src="https://img.shields.io/pypi/dm/cloudcat.svg?style=flat-square&logo=pypi&logoColor=white" alt="PyPI Downloads"></a>
14
+ <a href="https://pepy.tech/projects/cloudcat"><img src="https://static.pepy.tech/personalized-badge/cloudcat?period=total&units=international_system&left_color=black&right_color=green&left_text=downloads" alt="PyPI Downloads"></a>
15
15
  <a href="https://github.com/jonathansudhakar1/cloudcat/releases"><img src="https://img.shields.io/github/downloads/jonathansudhakar1/cloudcat/total.svg?style=flat-square&logo=homebrew&logoColor=white&label=homebrew" alt="Homebrew Downloads"></a>
16
16
  <a href="https://github.com/jonathansudhakar1/cloudcat/blob/main/LICENSE"><img src="https://img.shields.io/github/license/jonathansudhakar1/cloudcat.svg?style=flat-square" alt="License"></a>
17
17
  </p>
@@ -142,19 +142,42 @@ cloudcat -p gcs://bucket/data.csv --offset 100 -n 10
142
142
  | Google Cloud Storage | `gcs://` or `gs://` | ✅ Supported |
143
143
  | Amazon S3 | `s3://` | ✅ Supported |
144
144
  | Azure Blob Storage | `az://` or `azure://` | ✅ Supported |
145
+ | Azure Data Lake Gen2 | `abfss://` | ✅ Supported |
145
146
 
146
147
  ### File Format Support
147
148
 
148
- | Format | Read | Auto-Detect | Streaming | Use Case |
149
- |--------|------|-------------|-----------|----------|
150
- | CSV | ✅ | ✅ | ✅ | General data files |
151
- | JSON | ✅ | ✅ | ✅ | API responses, configs |
152
- | JSON Lines | ✅ | ✅ | ✅ | Log files, streaming data |
153
- | Parquet | ✅ | ✅ | ✅ | Spark/analytics data |
154
- | Avro | ✅ | ✅ | ✅ | Kafka, data pipelines |
155
- | ORC | ✅ | ✅ | ✅ | Hive, Hadoop ecosystem |
156
- | Text | ✅ | ✅ | ✅ | Log files, plain text |
157
- | TSV | ✅ | Via `--delimiter` | ✅ | Tab-separated data |
149
+ | Format | Auto-Detect | Use Case |
150
+ |--------|-------------|----------|
151
+ | CSV | ✅ | General data files |
152
+ | JSON | ✅ | API responses, configs |
153
+ | JSON Lines | ✅ | Log files, streaming data |
154
+ | Parquet | ✅ | Spark/analytics data |
155
+ | Avro | ✅ | Kafka, data pipelines |
156
+ | ORC | ✅ | Hive, Hadoop ecosystem |
157
+ | Text | ✅ | Log files, plain text |
158
+ | TSV | Via `--delimiter` | Tab-separated data |
159
+
160
+ ### Streaming Efficiency
161
+
162
+ CloudCat uses intelligent streaming to minimize data transfer and egress costs:
163
+
164
+ | Format | Compression | Streams | Column Projection | Early Row Stop |
165
+ |--------|-------------|---------|-------------------|----------------|
166
+ | Parquet | None/Internal | ✅ | ✅ Range requests | ✅ |
167
+ | Parquet | External (.gz) | ❌ | ❌ | ❌ |
168
+ | ORC | None/Internal | ❌ | ❌ | ❌ |
169
+ | ORC | External (.gz) | ❌ | ❌ | ❌ |
170
+ | CSV | None | ✅ | ❌ | ✅ |
171
+ | CSV | gzip/zstd/lz4/bz2 | ✅ | ❌ | ✅ |
172
+ | CSV | snappy | ❌ | ❌ | ❌ |
173
+ | JSON Lines | None/streamable | ✅ | ❌ | ✅ |
174
+ | JSON Array | Any | ❌ | ❌ | ❌ |
175
+ | Avro | Any | ✅ | ✅ Record-level | ✅ |
176
+ | Text | Any streamable | ✅ | N/A | ✅ |
177
+
178
+ - **Streams**: Only reads data as needed, stops early when row limit is reached
179
+ - **Column Projection**: For Parquet, only fetches required column chunks via HTTP range requests
180
+ - **Early Row Stop**: Stops reading when `--num-rows` limit is reached
158
181
 
159
182
  ### Compression Support
160
183
 
@@ -361,7 +384,7 @@ cloudcat -p s3://logs/errors/ -o json | grep "ERROR"
361
384
  cloudcat -p gcs://exports/daily/users.csv -s show
362
385
 
363
386
  # Verify record count
364
- cloudcat -p s3://warehouse/transactions.parquet --no-count
387
+ cloudcat -p s3://warehouse/transactions.parquet --count
365
388
  ```
366
389
 
367
390
  #### Format Conversion
@@ -405,7 +428,7 @@ Options:
405
428
  -s, --schema TEXT Schema display: show, dont_show, schema_only
406
429
  [default: show]
407
430
 
408
- --no-count Disable automatic record counting
431
+ --count Show total record count (scans entire file)
409
432
 
410
433
  -m, --multi-file-mode TEXT Directory handling: auto, first, all
411
434
  [default: auto]
@@ -501,7 +524,7 @@ cloudcat -p az://container/data.csv --account mystorageaccount
501
524
 
502
525
  ## Performance Tips
503
526
 
504
- 1. **Use `--no-count`** for large files when you don't need the total record count
527
+ 1. **Counting is off by default** — use `--count` only when you need the total record count
505
528
  2. **Prefer Parquet** format when possible — record counts are instant from metadata
506
529
  3. **Use `--num-rows`** to limit data transfer for large files
507
530
  4. **Use `--columns`** to select only needed columns (especially effective with 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.2.8"
5
+ __version__ = "0.3.0"
@@ -20,28 +20,48 @@ from . import __version__
20
20
 
21
21
  # Import from modular components
22
22
  from .config import cloud_config, SKIP_PATTERNS, FORMAT_EXTENSION_MAP
23
- from .compression import detect_compression, decompress_stream, strip_compression_extension
23
+ from .compression import (
24
+ detect_compression,
25
+ decompress_stream,
26
+ strip_compression_extension,
27
+ get_streaming_decompressor,
28
+ supports_streaming_decompression,
29
+ )
24
30
  from .filtering import parse_where_clause, apply_where_filter
25
31
  from .formatters import colorize_json, format_table_with_colored_header
26
32
  from .storage import (
27
33
  parse_cloud_path,
28
34
  get_stream,
29
35
  list_directory,
36
+ get_file_size,
30
37
  )
31
38
  from .storage.gcs import get_gcs_stream, list_gcs_directory
32
39
  from .storage.s3 import get_s3_stream, list_s3_directory
33
40
  from .storage.azure import get_azure_stream, list_azure_directory
34
41
  from .readers import (
35
42
  read_csv_data,
43
+ read_csv_data_streaming,
36
44
  read_json_data,
45
+ read_json_data_streaming,
37
46
  read_parquet_data,
47
+ read_parquet_data_streaming,
38
48
  read_avro_data,
49
+ read_avro_data_streaming,
39
50
  read_orc_data,
51
+ read_orc_data_streaming,
40
52
  read_text_data,
53
+ read_text_data_streaming,
41
54
  HAS_PARQUET,
42
55
  HAS_AVRO,
43
56
  HAS_ORC,
44
57
  )
58
+ from .streaming import (
59
+ StreamingStats,
60
+ format_bytes,
61
+ BytesTrackingStream,
62
+ get_pyarrow_filesystem,
63
+ supports_pyarrow_fs,
64
+ )
45
65
 
46
66
  # Import pyarrow for parquet metadata if available
47
67
  try:
@@ -362,7 +382,7 @@ def read_data(
362
382
  delimiter: Optional[str] = None,
363
383
  offset: int = 0
364
384
  ) -> Tuple[pd.DataFrame, pd.Series]:
365
- """Read data from cloud storage.
385
+ """Read data from cloud storage (legacy interface).
366
386
 
367
387
  Args:
368
388
  service: Cloud service identifier.
@@ -377,31 +397,128 @@ def read_data(
377
397
  Returns:
378
398
  Tuple of (DataFrame, schema).
379
399
  """
380
- # Get appropriate stream based on service
381
- stream = get_stream(service, bucket, object_path)
400
+ df, schema, _ = read_data_streaming(
401
+ service, bucket, object_path, input_format,
402
+ num_rows, columns, delimiter, offset
403
+ )
404
+ return df, schema
405
+
406
+
407
+ def read_data_streaming(
408
+ service: str,
409
+ bucket: str,
410
+ object_path: str,
411
+ input_format: str,
412
+ num_rows: int,
413
+ columns: Optional[str] = None,
414
+ delimiter: Optional[str] = None,
415
+ offset: int = 0
416
+ ) -> Tuple[pd.DataFrame, pd.Series, StreamingStats]:
417
+ """Read data from cloud storage with streaming support.
418
+
419
+ Uses PyArrow native filesystems for columnar formats (Parquet, ORC)
420
+ to enable true column projection with range requests. Uses streaming
421
+ decompression and chunked reading for row-based formats.
382
422
 
383
- # Check for compression and decompress if needed
423
+ Args:
424
+ service: Cloud service identifier.
425
+ bucket: Bucket or container name.
426
+ object_path: Object path.
427
+ input_format: Data format.
428
+ num_rows: Maximum rows to read.
429
+ columns: Columns to select.
430
+ delimiter: CSV delimiter.
431
+ offset: Rows to skip.
432
+
433
+ Returns:
434
+ Tuple of (DataFrame, schema, StreamingStats).
435
+ """
436
+ # Get file size for stats
437
+ try:
438
+ file_size = get_file_size(service, bucket, object_path)
439
+ except Exception:
440
+ file_size = 0
441
+
442
+ # Initialize stats
443
+ stats = StreamingStats(file_size=file_size, format_type=input_format)
444
+
445
+ # Check for compression
384
446
  compression = detect_compression(object_path)
385
- if compression:
386
- click.echo(Fore.BLUE + f"Detected {compression} compression, decompressing..." + Style.RESET_ALL)
387
- stream = decompress_stream(stream, compression)
447
+ stats.compression = compression
388
448
 
389
449
  # Calculate how many rows to read including offset
390
450
  rows_to_read = (offset + num_rows) if num_rows > 0 else 0
391
451
 
392
- # Read based on format
452
+ # For columnar formats without external compression, try native PyArrow filesystem
453
+ if input_format in ('parquet', 'orc') and compression is None and supports_pyarrow_fs():
454
+ try:
455
+ pyarrow_fs, _ = get_pyarrow_filesystem(
456
+ service,
457
+ aws_profile=cloud_config.aws_profile,
458
+ gcp_project=cloud_config.gcp_project,
459
+ gcp_credentials=cloud_config.gcp_credentials,
460
+ azure_account=cloud_config.azure_account
461
+ )
462
+ pyarrow_path = f"{bucket}/{object_path}"
463
+
464
+ if input_format == 'parquet':
465
+ df, schema, stats = read_parquet_data_streaming(
466
+ num_rows=rows_to_read,
467
+ columns=columns,
468
+ stats=stats,
469
+ pyarrow_fs=pyarrow_fs,
470
+ pyarrow_path=pyarrow_path
471
+ )
472
+ else: # orc
473
+ df, schema, stats = read_orc_data_streaming(
474
+ num_rows=rows_to_read,
475
+ columns=columns,
476
+ stats=stats,
477
+ pyarrow_fs=pyarrow_fs,
478
+ pyarrow_path=pyarrow_path
479
+ )
480
+
481
+ # Apply offset
482
+ if offset > 0 and not df.empty:
483
+ if offset >= len(df):
484
+ click.echo(Fore.YELLOW + f"Warning: Offset ({offset}) >= total rows read ({len(df)}). No data to display." + Style.RESET_ALL)
485
+ df = df.iloc[0:0]
486
+ else:
487
+ df = df.iloc[offset:].reset_index(drop=True)
488
+
489
+ return df, schema, stats
490
+
491
+ except Exception as e:
492
+ # Fall back to stream-based approach
493
+ click.echo(Fore.YELLOW + f"Native filesystem unavailable, using stream: {str(e)}" + Style.RESET_ALL)
494
+
495
+ # Get stream for non-native filesystem approach
496
+ stream = get_stream(service, bucket, object_path)
497
+
498
+ # Handle compression with streaming decompression where possible
499
+ if compression:
500
+ if supports_streaming_decompression(compression):
501
+ click.echo(Fore.BLUE + f"Detected {compression} compression, streaming decompression..." + Style.RESET_ALL)
502
+ stream, is_streaming = get_streaming_decompressor(stream, compression)
503
+ stats.is_streaming = is_streaming
504
+ else:
505
+ click.echo(Fore.BLUE + f"Detected {compression} compression, decompressing..." + Style.RESET_ALL)
506
+ stream = decompress_stream(stream, compression)
507
+ stats.is_streaming = False
508
+
509
+ # Read based on format using streaming readers
393
510
  if input_format == 'csv':
394
- df, schema = read_csv_data(stream, rows_to_read, columns, delimiter)
511
+ df, schema, stats = read_csv_data_streaming(stream, rows_to_read, columns, delimiter, stats)
395
512
  elif input_format == 'json':
396
- df, schema = read_json_data(stream, rows_to_read, columns)
513
+ df, schema, stats = read_json_data_streaming(stream, rows_to_read, columns, stats)
397
514
  elif input_format == 'parquet':
398
- df, schema = read_parquet_data(stream, rows_to_read, columns)
515
+ df, schema, stats = read_parquet_data_streaming(stream=stream, num_rows=rows_to_read, columns=columns, stats=stats)
399
516
  elif input_format == 'avro':
400
- df, schema = read_avro_data(stream, rows_to_read, columns)
517
+ df, schema, stats = read_avro_data_streaming(stream, rows_to_read, columns, stats)
401
518
  elif input_format == 'orc':
402
- df, schema = read_orc_data(stream, rows_to_read, columns)
519
+ df, schema, stats = read_orc_data_streaming(stream=stream, num_rows=rows_to_read, columns=columns, stats=stats)
403
520
  elif input_format == 'text':
404
- df, schema = read_text_data(stream, rows_to_read, columns)
521
+ df, schema, stats = read_text_data_streaming(stream, rows_to_read, columns, stats)
405
522
  else:
406
523
  raise ValueError(f"Unsupported format: {input_format}")
407
524
 
@@ -409,11 +526,11 @@ def read_data(
409
526
  if offset > 0 and not df.empty:
410
527
  if offset >= len(df):
411
528
  click.echo(Fore.YELLOW + f"Warning: Offset ({offset}) >= total rows read ({len(df)}). No data to display." + Style.RESET_ALL)
412
- df = df.iloc[0:0] # Empty dataframe with same schema
529
+ df = df.iloc[0:0]
413
530
  else:
414
531
  df = df.iloc[offset:].reset_index(drop=True)
415
532
 
416
- return df, schema
533
+ return df, schema, stats
417
534
 
418
535
 
419
536
  def get_record_count(
@@ -561,7 +678,7 @@ def get_record_count(
561
678
  @click.option('--where', '-w', help='Filter rows (e.g., "status=active", "age>30", "name contains john")')
562
679
  @click.option('--schema', '-s', type=click.Choice(['show', 'dont_show', 'schema_only']), default='show',
563
680
  help='Schema display option (default: show)')
564
- @click.option('--no-count', is_flag=True, help='Disable record count display')
681
+ @click.option('--count', is_flag=True, help='Show total record count (requires scanning entire file)')
565
682
  @click.option('--multi-file-mode', '-m', type=click.Choice(['first', 'auto', 'all']), default='auto',
566
683
  help='How to handle directories with multiple files (default: auto)')
567
684
  @click.option('--max-size-mb', default=25, type=int,
@@ -571,7 +688,7 @@ def get_record_count(
571
688
  @click.option('--project', help='GCP project ID (for GCS access)')
572
689
  @click.option('--credentials', help='Path to GCP service account JSON file')
573
690
  @click.option('--account', help='Azure storage account name')
574
- def main(path, output_format, input_format, columns, num_rows, offset, where, schema, no_count,
691
+ def main(path, output_format, input_format, columns, num_rows, offset, where, schema, count,
575
692
  multi_file_mode, max_size_mb, delimiter, profile, project, credentials, account):
576
693
  """Display data from files in Google Cloud Storage, AWS S3, or Azure Blob Storage.
577
694
 
@@ -668,6 +785,9 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
668
785
  # Check if path is a directory (ends with '/')
669
786
  is_directory = object_path.endswith('/')
670
787
 
788
+ # Initialize streaming stats
789
+ streaming_stats = None
790
+
671
791
  # Handle directory paths based on multi-file-mode
672
792
  if is_directory:
673
793
  click.echo(Fore.BLUE + f"Path is a directory" + Style.RESET_ALL)
@@ -682,8 +802,8 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
682
802
  input_format = detect_format_from_path(object_path)
683
803
  click.echo(Fore.BLUE + f"Inferred input format: {input_format}" + Style.RESET_ALL)
684
804
 
685
- # Read the data from the single file
686
- df, full_schema = read_data(service, bucket, object_path, input_format, num_rows, columns, delimiter, offset)
805
+ # Read the data from the single file with streaming
806
+ df, full_schema, streaming_stats = read_data_streaming(service, bucket, object_path, input_format, num_rows, columns, delimiter, offset)
687
807
  total_record_count = None # Will be computed later if needed
688
808
  else:
689
809
  # Read from multiple files
@@ -703,6 +823,10 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
703
823
  service, bucket, file_list, input_format, num_rows, columns, delimiter, offset
704
824
  )
705
825
 
826
+ # Calculate total size for stats
827
+ total_size = sum(f[1] for f in file_list)
828
+ streaming_stats = StreamingStats(file_size=total_size, bytes_read=total_size, format_type=input_format)
829
+
706
830
  # Update object_path for display/logging purposes
707
831
  object_path = f"{object_path} ({len(file_list)} files)"
708
832
  else:
@@ -712,8 +836,8 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
712
836
  input_format = detect_format_from_path(object_path)
713
837
  click.echo(Fore.BLUE + f"Inferred input format: {input_format}" + Style.RESET_ALL)
714
838
 
715
- # Read the data
716
- df, full_schema = read_data(service, bucket, object_path, input_format, num_rows, columns, delimiter, offset)
839
+ # Read the data with streaming
840
+ df, full_schema, streaming_stats = read_data_streaming(service, bucket, object_path, input_format, num_rows, columns, delimiter, offset)
717
841
  total_record_count = None # Will be computed later if needed
718
842
 
719
843
  # Apply WHERE filter if specified
@@ -732,8 +856,8 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
732
856
 
733
857
  # Exit if only schema was requested
734
858
  if schema == 'schema_only':
735
- # Still show count even with schema_only unless --no-count is specified
736
- if not no_count:
859
+ # Show count only if --count flag is specified
860
+ if count:
737
861
  try:
738
862
  if total_record_count is None:
739
863
  total_record_count = get_record_count(service, bucket, object_path, input_format, delimiter)
@@ -756,8 +880,8 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
756
880
  elif output_format == 'csv':
757
881
  click.echo(df.to_csv(index=False))
758
882
 
759
- # Count records by default unless --no-count is specified
760
- if not no_count:
883
+ # Show record count only if --count flag is specified
884
+ if count:
761
885
  try:
762
886
  if total_record_count is None:
763
887
  total_record_count = get_record_count(service, bucket, object_path, input_format, delimiter)
@@ -765,6 +889,10 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
765
889
  except Exception as e:
766
890
  click.echo(Fore.YELLOW + f"\nCould not count records: {str(e)}" + Style.RESET_ALL)
767
891
 
892
+ # Display streaming stats footer
893
+ if streaming_stats and streaming_stats.file_size > 0:
894
+ click.echo(Fore.BLUE + f"\n{streaming_stats.format_report()}" + Style.RESET_ALL)
895
+
768
896
  except Exception as e:
769
897
  click.echo(Fore.RED + f"Error: {str(e)}" + Style.RESET_ALL, err=True)
770
898
  sys.exit(1)
@@ -1,8 +1,9 @@
1
1
  """Compression detection and decompression utilities."""
2
2
 
3
+ import bz2
3
4
  import gzip
4
5
  import io
5
- from typing import Optional, Union, BinaryIO
6
+ from typing import Optional, Union, BinaryIO, Tuple
6
7
 
7
8
  from .config import COMPRESSION_EXTENSIONS
8
9
 
@@ -86,7 +87,6 @@ def decompress_stream(stream: Union[BinaryIO, bytes], compression: str) -> io.By
86
87
  raise ValueError("python-snappy package is required for .snappy files. Install with: pip install python-snappy")
87
88
  decompressed = snappy.decompress(data)
88
89
  elif compression == 'bz2':
89
- import bz2
90
90
  decompressed = bz2.decompress(data)
91
91
  else:
92
92
  # No compression or unknown - return original as BytesIO
@@ -112,3 +112,82 @@ def strip_compression_extension(path: str) -> str:
112
112
  if path_lower.endswith(ext):
113
113
  return path[:-len(ext)]
114
114
  return path
115
+
116
+
117
+ def supports_streaming_decompression(compression: str) -> bool:
118
+ """Check if a compression format supports streaming decompression.
119
+
120
+ Args:
121
+ compression: Compression type string.
122
+
123
+ Returns:
124
+ True if the format can be decompressed in a streaming fashion.
125
+ """
126
+ # Snappy does not support streaming decompression
127
+ return compression in ('gzip', 'bz2', 'zstd', 'lz4')
128
+
129
+
130
+ def get_streaming_decompressor(
131
+ stream: BinaryIO,
132
+ compression: str
133
+ ) -> Tuple[BinaryIO, bool]:
134
+ """Get a streaming decompressor for the given compression type.
135
+
136
+ This wraps the stream with a decompressor that reads data on-demand,
137
+ rather than decompressing everything upfront. This enables early
138
+ termination for row-limited queries.
139
+
140
+ Args:
141
+ stream: File-like object containing compressed data.
142
+ compression: Compression type ('gzip', 'zstd', 'lz4', 'snappy', 'bz2').
143
+
144
+ Returns:
145
+ Tuple of (decompressed_stream, is_streaming) where is_streaming
146
+ indicates whether true streaming is used (False for snappy which
147
+ requires full decompression).
148
+
149
+ Raises:
150
+ ValueError: If required compression library is not installed.
151
+ """
152
+ if compression == 'gzip':
153
+ # gzip.GzipFile wraps a stream and decompresses on-demand
154
+ return gzip.GzipFile(fileobj=stream, mode='rb'), True
155
+
156
+ elif compression == 'bz2':
157
+ # BZ2File can wrap a stream for streaming decompression
158
+ return bz2.BZ2File(stream, mode='rb'), True
159
+
160
+ elif compression == 'zstd':
161
+ if not HAS_ZSTD:
162
+ raise ValueError(
163
+ "zstandard package is required for .zst files. "
164
+ "Install with: pip install zstandard"
165
+ )
166
+ # ZstdDecompressor.stream_reader() provides streaming decompression
167
+ dctx = zstd.ZstdDecompressor()
168
+ return dctx.stream_reader(stream), True
169
+
170
+ elif compression == 'lz4':
171
+ if not HAS_LZ4:
172
+ raise ValueError(
173
+ "lz4 package is required for .lz4 files. "
174
+ "Install with: pip install lz4"
175
+ )
176
+ # lz4.frame.open() can wrap a stream for streaming decompression
177
+ # We need to use LZ4FrameDecompressor for streaming
178
+ return lz4.open(stream, mode='rb'), True
179
+
180
+ elif compression == 'snappy':
181
+ if not HAS_SNAPPY:
182
+ raise ValueError(
183
+ "python-snappy package is required for .snappy files. "
184
+ "Install with: pip install python-snappy"
185
+ )
186
+ # Snappy does NOT support streaming - must decompress fully
187
+ data = stream.read()
188
+ decompressed = snappy.decompress(data)
189
+ return io.BytesIO(decompressed), False
190
+
191
+ else:
192
+ # Unknown compression - return original stream
193
+ return stream, True
@@ -0,0 +1,26 @@
1
+ """Data format readers."""
2
+
3
+ from .csv import read_csv_data, read_csv_data_streaming
4
+ from .json import read_json_data, read_json_data_streaming
5
+ from .parquet import read_parquet_data, read_parquet_data_streaming, HAS_PARQUET
6
+ from .avro import read_avro_data, read_avro_data_streaming, HAS_AVRO
7
+ from .orc import read_orc_data, read_orc_data_streaming, HAS_ORC
8
+ from .text import read_text_data, read_text_data_streaming
9
+
10
+ __all__ = [
11
+ 'read_csv_data',
12
+ 'read_csv_data_streaming',
13
+ 'read_json_data',
14
+ 'read_json_data_streaming',
15
+ 'read_parquet_data',
16
+ 'read_parquet_data_streaming',
17
+ 'HAS_PARQUET',
18
+ 'read_avro_data',
19
+ 'read_avro_data_streaming',
20
+ 'HAS_AVRO',
21
+ 'read_orc_data',
22
+ 'read_orc_data_streaming',
23
+ 'HAS_ORC',
24
+ 'read_text_data',
25
+ 'read_text_data_streaming',
26
+ ]