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.
- {cloudcat-0.2.8 → cloudcat-0.3.1}/PKG-INFO +38 -15
- {cloudcat-0.2.8 → cloudcat-0.3.1}/README.md +37 -14
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/__init__.py +1 -1
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/cli.py +219 -34
- cloudcat-0.3.1/cloudcat/compression.py +214 -0
- cloudcat-0.3.1/cloudcat/readers/__init__.py +26 -0
- cloudcat-0.3.1/cloudcat/readers/avro.py +144 -0
- cloudcat-0.3.1/cloudcat/readers/csv.py +150 -0
- cloudcat-0.3.1/cloudcat/readers/json.py +254 -0
- cloudcat-0.3.1/cloudcat/readers/orc.py +188 -0
- cloudcat-0.3.1/cloudcat/readers/parquet.py +282 -0
- cloudcat-0.3.1/cloudcat/readers/text.py +123 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/storage/__init__.py +2 -1
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/storage/azure.py +16 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/storage/base.py +45 -2
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/storage/gcs.py +17 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/storage/s3.py +15 -0
- cloudcat-0.3.1/cloudcat/streaming/__init__.py +13 -0
- cloudcat-0.3.1/cloudcat/streaming/filesystems.py +186 -0
- cloudcat-0.3.1/cloudcat/streaming/stats.py +96 -0
- cloudcat-0.3.1/cloudcat/streaming/tracking.py +231 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat.egg-info/PKG-INFO +38 -15
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat.egg-info/SOURCES.txt +4 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_cli.py +73 -59
- {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_compression.py +24 -1
- {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_path_parsing.py +21 -1
- cloudcat-0.2.8/cloudcat/compression.py +0 -114
- cloudcat-0.2.8/cloudcat/readers/__init__.py +0 -20
- cloudcat-0.2.8/cloudcat/readers/avro.py +0 -77
- cloudcat-0.2.8/cloudcat/readers/csv.py +0 -53
- cloudcat-0.2.8/cloudcat/readers/json.py +0 -102
- cloudcat-0.2.8/cloudcat/readers/orc.py +0 -86
- cloudcat-0.2.8/cloudcat/readers/parquet.py +0 -115
- cloudcat-0.2.8/cloudcat/readers/text.py +0 -57
- {cloudcat-0.2.8 → cloudcat-0.3.1}/LICENSE +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/config.py +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/filtering.py +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat/formatters.py +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat.egg-info/dependency_links.txt +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat.egg-info/entry_points.txt +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat.egg-info/requires.txt +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/cloudcat.egg-info/top_level.txt +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/pyproject.toml +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/setup.cfg +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/setup.py +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/__init__.py +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/conftest.py +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_config.py +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_data_reading.py +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_file_filtering.py +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_filtering.py +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_format_detection.py +0 -0
- {cloudcat-0.2.8 → cloudcat-0.3.1}/tests/test_integration.py +0 -0
- {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.
|
|
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://
|
|
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.
|
|
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
|
|
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(
|
|
@@ -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
|
-
|
|
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('--
|
|
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,
|
|
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 =
|
|
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,
|
|
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 =
|
|
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
|
-
#
|
|
736
|
-
if
|
|
904
|
+
# Show count only if --count flag is specified
|
|
905
|
+
if count:
|
|
737
906
|
try:
|
|
738
907
|
if total_record_count is None:
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
#
|
|
760
|
-
if
|
|
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
|
-
|
|
764
|
-
|
|
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)
|