cloudcat 0.2.8__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 (54) hide show
  1. {cloudcat-0.2.8 → cloudcat-0.3.1}/PKG-INFO +38 -15
  2. {cloudcat-0.2.8 → cloudcat-0.3.1}/README.md +37 -14
  3. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/__init__.py +1 -1
  4. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/cli.py +219 -34
  5. cloudcat-0.3.1/cloudcat/compression.py +214 -0
  6. cloudcat-0.3.1/cloudcat/readers/__init__.py +26 -0
  7. cloudcat-0.3.1/cloudcat/readers/avro.py +144 -0
  8. cloudcat-0.3.1/cloudcat/readers/csv.py +150 -0
  9. cloudcat-0.3.1/cloudcat/readers/json.py +254 -0
  10. cloudcat-0.3.1/cloudcat/readers/orc.py +188 -0
  11. cloudcat-0.3.1/cloudcat/readers/parquet.py +282 -0
  12. cloudcat-0.3.1/cloudcat/readers/text.py +123 -0
  13. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/storage/__init__.py +2 -1
  14. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/storage/azure.py +16 -0
  15. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/storage/base.py +45 -2
  16. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/storage/gcs.py +17 -0
  17. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/storage/s3.py +15 -0
  18. cloudcat-0.3.1/cloudcat/streaming/__init__.py +13 -0
  19. cloudcat-0.3.1/cloudcat/streaming/filesystems.py +186 -0
  20. cloudcat-0.3.1/cloudcat/streaming/stats.py +96 -0
  21. cloudcat-0.3.1/cloudcat/streaming/tracking.py +231 -0
  22. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat.egg-info/PKG-INFO +38 -15
  23. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat.egg-info/SOURCES.txt +4 -0
  24. {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_cli.py +73 -59
  25. {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_compression.py +24 -1
  26. {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_path_parsing.py +21 -1
  27. cloudcat-0.2.8/cloudcat/compression.py +0 -114
  28. cloudcat-0.2.8/cloudcat/readers/__init__.py +0 -20
  29. cloudcat-0.2.8/cloudcat/readers/avro.py +0 -77
  30. cloudcat-0.2.8/cloudcat/readers/csv.py +0 -53
  31. cloudcat-0.2.8/cloudcat/readers/json.py +0 -102
  32. cloudcat-0.2.8/cloudcat/readers/orc.py +0 -86
  33. cloudcat-0.2.8/cloudcat/readers/parquet.py +0 -115
  34. cloudcat-0.2.8/cloudcat/readers/text.py +0 -57
  35. {cloudcat-0.2.8 → cloudcat-0.3.1}/LICENSE +0 -0
  36. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/config.py +0 -0
  37. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/filtering.py +0 -0
  38. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/formatters.py +0 -0
  39. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat.egg-info/dependency_links.txt +0 -0
  40. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat.egg-info/entry_points.txt +0 -0
  41. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat.egg-info/requires.txt +0 -0
  42. {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat.egg-info/top_level.txt +0 -0
  43. {cloudcat-0.2.8 → cloudcat-0.3.1}/pyproject.toml +0 -0
  44. {cloudcat-0.2.8 → cloudcat-0.3.1}/setup.cfg +0 -0
  45. {cloudcat-0.2.8 → cloudcat-0.3.1}/setup.py +0 -0
  46. {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/__init__.py +0 -0
  47. {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/conftest.py +0 -0
  48. {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_config.py +0 -0
  49. {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_data_reading.py +0 -0
  50. {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_file_filtering.py +0 -0
  51. {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_filtering.py +0 -0
  52. {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_format_detection.py +0 -0
  53. {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_integration.py +0 -0
  54. {cloudcat-0.2.8 → cloudcat-0.3.1}/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.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
@@ -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.1"
@@ -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.
422
+
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)
382
444
 
383
- # Check for compression and decompress if needed
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(
@@ -421,7 +538,8 @@ def get_record_count(
421
538
  bucket: str,
422
539
  object_path: str,
423
540
  input_format: str,
424
- delimiter: Optional[str] = None
541
+ delimiter: Optional[str] = None,
542
+ quiet: bool = False
425
543
  ):
426
544
  """Get record count from a file.
427
545
 
@@ -431,6 +549,7 @@ def get_record_count(
431
549
  object_path: Object path.
432
550
  input_format: Data format.
433
551
  delimiter: CSV delimiter.
552
+ quiet: If True, suppress progress messages.
434
553
 
435
554
  Returns:
436
555
  Record count (int) or "Unknown" on failure.
@@ -465,7 +584,8 @@ def get_record_count(
465
584
  pass # Ignore cleanup errors
466
585
  else:
467
586
  # For CSV and JSON, we need to count the rows
468
- 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)
469
589
 
470
590
  stream = get_stream(service, bucket, object_path)
471
591
  if compression:
@@ -548,6 +668,42 @@ def get_record_count(
548
668
  return "Unknown"
549
669
 
550
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
+
551
707
  @click.command()
552
708
  @click.version_option(version=__version__, prog_name='cloudcat')
553
709
  @click.option('--path', '-p', required=True, help='Path to the file or directory (gcs://, s3://, or az://)')
@@ -561,7 +717,7 @@ def get_record_count(
561
717
  @click.option('--where', '-w', help='Filter rows (e.g., "status=active", "age>30", "name contains john")')
562
718
  @click.option('--schema', '-s', type=click.Choice(['show', 'dont_show', 'schema_only']), default='show',
563
719
  help='Schema display option (default: show)')
564
- @click.option('--no-count', is_flag=True, help='Disable record count display')
720
+ @click.option('--count', is_flag=True, help='Show total record count (requires scanning entire file)')
565
721
  @click.option('--multi-file-mode', '-m', type=click.Choice(['first', 'auto', 'all']), default='auto',
566
722
  help='How to handle directories with multiple files (default: auto)')
567
723
  @click.option('--max-size-mb', default=25, type=int,
@@ -571,7 +727,7 @@ def get_record_count(
571
727
  @click.option('--project', help='GCP project ID (for GCS access)')
572
728
  @click.option('--credentials', help='Path to GCP service account JSON file')
573
729
  @click.option('--account', help='Azure storage account name')
574
- def main(path, output_format, input_format, columns, num_rows, offset, where, schema, no_count,
730
+ def main(path, output_format, input_format, columns, num_rows, offset, where, schema, count,
575
731
  multi_file_mode, max_size_mb, delimiter, profile, project, credentials, account):
576
732
  """Display data from files in Google Cloud Storage, AWS S3, or Azure Blob Storage.
577
733
 
@@ -668,6 +824,10 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
668
824
  # Check if path is a directory (ends with '/')
669
825
  is_directory = object_path.endswith('/')
670
826
 
827
+ # Initialize streaming stats
828
+ streaming_stats = None
829
+ multi_file_list = None # For directory reads with --count
830
+
671
831
  # Handle directory paths based on multi-file-mode
672
832
  if is_directory:
673
833
  click.echo(Fore.BLUE + f"Path is a directory" + Style.RESET_ALL)
@@ -682,8 +842,8 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
682
842
  input_format = detect_format_from_path(object_path)
683
843
  click.echo(Fore.BLUE + f"Inferred input format: {input_format}" + Style.RESET_ALL)
684
844
 
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)
845
+ # Read the data from the single file with streaming
846
+ df, full_schema, streaming_stats = read_data_streaming(service, bucket, object_path, input_format, num_rows, columns, delimiter, offset)
687
847
  total_record_count = None # Will be computed later if needed
688
848
  else:
689
849
  # Read from multiple files
@@ -699,10 +859,19 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
699
859
  file_list = get_files_for_multiread(service, bucket, object_path, input_format, max_size_mb)
700
860
 
701
861
  # Read data from multiple files
702
- df, full_schema, total_record_count = read_data_from_multiple_files(
862
+ df, full_schema, rows_in_files = read_data_from_multiple_files(
703
863
  service, bucket, file_list, input_format, num_rows, columns, delimiter, offset
704
864
  )
705
865
 
866
+ # Calculate total size for stats
867
+ total_size = sum(f[1] for f in file_list)
868
+ streaming_stats = StreamingStats(file_size=total_size, bytes_read=total_size, format_type=input_format)
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
+
706
875
  # Update object_path for display/logging purposes
707
876
  object_path = f"{object_path} ({len(file_list)} files)"
708
877
  else:
@@ -712,8 +881,8 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
712
881
  input_format = detect_format_from_path(object_path)
713
882
  click.echo(Fore.BLUE + f"Inferred input format: {input_format}" + Style.RESET_ALL)
714
883
 
715
- # Read the data
716
- df, full_schema = read_data(service, bucket, object_path, input_format, num_rows, columns, delimiter, offset)
884
+ # Read the data with streaming
885
+ df, full_schema, streaming_stats = read_data_streaming(service, bucket, object_path, input_format, num_rows, columns, delimiter, offset)
717
886
  total_record_count = None # Will be computed later if needed
718
887
 
719
888
  # Apply WHERE filter if specified
@@ -732,12 +901,17 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
732
901
 
733
902
  # Exit if only schema was requested
734
903
  if schema == 'schema_only':
735
- # Still show count even with schema_only unless --no-count is specified
736
- if not no_count:
904
+ # Show count only if --count flag is specified
905
+ if count:
737
906
  try:
738
907
  if total_record_count is None:
739
- total_record_count = get_record_count(service, bucket, object_path, input_format, delimiter)
740
- 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)
741
915
  except Exception as e:
742
916
  click.echo(Fore.YELLOW + f"Could not count records: {str(e)}" + Style.RESET_ALL)
743
917
  return
@@ -756,15 +930,26 @@ def main(path, output_format, input_format, columns, num_rows, offset, where, sc
756
930
  elif output_format == 'csv':
757
931
  click.echo(df.to_csv(index=False))
758
932
 
759
- # Count records by default unless --no-count is specified
760
- if not no_count:
933
+ # Show record count only if --count flag is specified
934
+ if count:
761
935
  try:
762
936
  if total_record_count is None:
763
- total_record_count = get_record_count(service, bucket, object_path, input_format, delimiter)
764
- 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)
765
946
  except Exception as e:
766
947
  click.echo(Fore.YELLOW + f"\nCould not count records: {str(e)}" + Style.RESET_ALL)
767
948
 
949
+ # Display streaming stats footer
950
+ if streaming_stats and streaming_stats.file_size > 0:
951
+ click.echo(Fore.BLUE + f"\n{streaming_stats.format_report()}" + Style.RESET_ALL)
952
+
768
953
  except Exception as e:
769
954
  click.echo(Fore.RED + f"Error: {str(e)}" + Style.RESET_ALL, err=True)
770
955
  sys.exit(1)