cloudcat 0.2.7__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.
- {cloudcat-0.2.7 → cloudcat-0.3.0}/PKG-INFO +38 -15
- {cloudcat-0.2.7 → cloudcat-0.3.0}/README.md +37 -14
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat/__init__.py +1 -1
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat/cli.py +155 -27
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat/compression.py +81 -2
- cloudcat-0.3.0/cloudcat/readers/__init__.py +26 -0
- cloudcat-0.3.0/cloudcat/readers/avro.py +144 -0
- cloudcat-0.3.0/cloudcat/readers/csv.py +150 -0
- cloudcat-0.3.0/cloudcat/readers/json.py +254 -0
- cloudcat-0.3.0/cloudcat/readers/orc.py +188 -0
- cloudcat-0.3.0/cloudcat/readers/parquet.py +282 -0
- cloudcat-0.3.0/cloudcat/readers/text.py +123 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat/storage/__init__.py +2 -1
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat/storage/azure.py +16 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat/storage/base.py +45 -2
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat/storage/gcs.py +17 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat/storage/s3.py +15 -0
- cloudcat-0.3.0/cloudcat/streaming/__init__.py +13 -0
- cloudcat-0.3.0/cloudcat/streaming/filesystems.py +186 -0
- cloudcat-0.3.0/cloudcat/streaming/stats.py +96 -0
- cloudcat-0.3.0/cloudcat/streaming/tracking.py +231 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat.egg-info/PKG-INFO +38 -15
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat.egg-info/SOURCES.txt +4 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/tests/test_cli.py +73 -59
- {cloudcat-0.2.7 → cloudcat-0.3.0}/tests/test_path_parsing.py +21 -1
- cloudcat-0.2.7/cloudcat/readers/__init__.py +0 -20
- cloudcat-0.2.7/cloudcat/readers/avro.py +0 -77
- cloudcat-0.2.7/cloudcat/readers/csv.py +0 -53
- cloudcat-0.2.7/cloudcat/readers/json.py +0 -102
- cloudcat-0.2.7/cloudcat/readers/orc.py +0 -86
- cloudcat-0.2.7/cloudcat/readers/parquet.py +0 -115
- cloudcat-0.2.7/cloudcat/readers/text.py +0 -57
- {cloudcat-0.2.7 → cloudcat-0.3.0}/LICENSE +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat/config.py +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat/filtering.py +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat/formatters.py +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat.egg-info/dependency_links.txt +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat.egg-info/entry_points.txt +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat.egg-info/requires.txt +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/cloudcat.egg-info/top_level.txt +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/pyproject.toml +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/setup.cfg +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/setup.py +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/tests/__init__.py +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/tests/conftest.py +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/tests/test_compression.py +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/tests/test_config.py +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/tests/test_data_reading.py +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/tests/test_file_filtering.py +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/tests/test_filtering.py +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/tests/test_format_detection.py +0 -0
- {cloudcat-0.2.7 → cloudcat-0.3.0}/tests/test_integration.py +0 -0
- {cloudcat-0.2.7 → 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.
|
|
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://
|
|
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 |
|
|
227
|
-
|
|
228
|
-
| CSV | ✅ |
|
|
229
|
-
| JSON | ✅ |
|
|
230
|
-
| JSON Lines | ✅ |
|
|
231
|
-
| Parquet | ✅ |
|
|
232
|
-
| Avro | ✅ |
|
|
233
|
-
| ORC | ✅ |
|
|
234
|
-
| Text | ✅ |
|
|
235
|
-
| TSV |
|
|
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 --
|
|
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
|
-
--
|
|
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. **
|
|
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://
|
|
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 |
|
|
149
|
-
|
|
150
|
-
| CSV | ✅ |
|
|
151
|
-
| JSON | ✅ |
|
|
152
|
-
| JSON Lines | ✅ |
|
|
153
|
-
| Parquet | ✅ |
|
|
154
|
-
| Avro | ✅ |
|
|
155
|
-
| ORC | ✅ |
|
|
156
|
-
| Text | ✅ |
|
|
157
|
-
| TSV |
|
|
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 --
|
|
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
|
-
--
|
|
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. **
|
|
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)
|
|
@@ -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
|
|
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
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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 =
|
|
511
|
+
df, schema, stats = read_csv_data_streaming(stream, rows_to_read, columns, delimiter, stats)
|
|
395
512
|
elif input_format == 'json':
|
|
396
|
-
df, schema =
|
|
513
|
+
df, schema, stats = read_json_data_streaming(stream, rows_to_read, columns, stats)
|
|
397
514
|
elif input_format == 'parquet':
|
|
398
|
-
df, schema =
|
|
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 =
|
|
517
|
+
df, schema, stats = read_avro_data_streaming(stream, rows_to_read, columns, stats)
|
|
401
518
|
elif input_format == 'orc':
|
|
402
|
-
df, schema =
|
|
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 =
|
|
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]
|
|
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('--
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
-
#
|
|
736
|
-
if
|
|
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
|
-
#
|
|
760
|
-
if
|
|
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
|
+
]
|