cloudcat 0.1.3__tar.gz → 0.1.4__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.1.3 → cloudcat-0.1.4}/PKG-INFO +1 -1
- {cloudcat-0.1.3 → cloudcat-0.1.4}/README.md +33 -8
- {cloudcat-0.1.3 → cloudcat-0.1.4}/cloudcat/__init__.py +1 -1
- {cloudcat-0.1.3 → cloudcat-0.1.4}/cloudcat/cli.py +49 -19
- {cloudcat-0.1.3 → cloudcat-0.1.4}/cloudcat.egg-info/PKG-INFO +1 -1
- {cloudcat-0.1.3 → cloudcat-0.1.4}/setup.py +1 -1
- {cloudcat-0.1.3 → cloudcat-0.1.4}/LICENSE +0 -0
- {cloudcat-0.1.3 → cloudcat-0.1.4}/cloudcat.egg-info/SOURCES.txt +0 -0
- {cloudcat-0.1.3 → cloudcat-0.1.4}/cloudcat.egg-info/dependency_links.txt +0 -0
- {cloudcat-0.1.3 → cloudcat-0.1.4}/cloudcat.egg-info/entry_points.txt +0 -0
- {cloudcat-0.1.3 → cloudcat-0.1.4}/cloudcat.egg-info/requires.txt +0 -0
- {cloudcat-0.1.3 → cloudcat-0.1.4}/cloudcat.egg-info/top_level.txt +0 -0
- {cloudcat-0.1.3 → cloudcat-0.1.4}/pyproject.toml +0 -0
- {cloudcat-0.1.3 → cloudcat-0.1.4}/setup.cfg +0 -0
|
@@ -6,6 +6,7 @@ A command-line utility to read and display data from cloud storage (Google Cloud
|
|
|
6
6
|
|
|
7
7
|
- **Cloud Storage Support**: Read files from GCS (`gcs://`) and S3 (`s3://`)
|
|
8
8
|
- **Multiple File Formats**: Support for CSV, JSON, and Parquet
|
|
9
|
+
- **CSV Delimiter Support**: Handle tab-delimited and other custom-delimited files
|
|
9
10
|
- **Intelligent Directory Handling**: Automatically find and read data from directories
|
|
10
11
|
- **Multi-File Reading**: Combine data from multiple small files (up to configurable size)
|
|
11
12
|
- **Streaming**: Optimize to avoid downloading entire files when possible
|
|
@@ -36,6 +37,9 @@ pip install cloudcat[all]
|
|
|
36
37
|
# Basic file reading
|
|
37
38
|
cloudcat --path gcs://bucket/file.csv
|
|
38
39
|
|
|
40
|
+
# Reading tab-delimited files
|
|
41
|
+
cloudcat --path s3://bucket/data.csv --delimiter "\t"
|
|
42
|
+
|
|
39
43
|
# Reading from a directory (automatically finds data files)
|
|
40
44
|
cloudcat --path s3://bucket/spark-output/
|
|
41
45
|
|
|
@@ -56,6 +60,7 @@ cloudcat --path gcs://bucket/logs/ --multi-file-mode all
|
|
|
56
60
|
| `--no-count` | | Disable record count display (counts shown by default) |
|
|
57
61
|
| `--multi-file-mode` | `-m` | How to handle directories: `auto` (default), `first`, `all` |
|
|
58
62
|
| `--max-size-mb` | | Maximum size in MB to read for multi-file mode (default: 25) |
|
|
63
|
+
| `--delimiter` | `-d` | Delimiter to use for CSV files (use `\t` for tab) |
|
|
59
64
|
|
|
60
65
|
## Directory Handling
|
|
61
66
|
|
|
@@ -71,6 +76,21 @@ When reading directories, CloudCat automatically:
|
|
|
71
76
|
- Prioritizes files matching the specified format
|
|
72
77
|
- Provides a summary of selected files
|
|
73
78
|
|
|
79
|
+
## CSV Delimiter Support
|
|
80
|
+
|
|
81
|
+
CloudCat can handle various CSV delimiter types:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Tab-delimited files
|
|
85
|
+
cloudcat --path gcs://bucket/data.tsv --delimiter "\t"
|
|
86
|
+
|
|
87
|
+
# Pipe-delimited files
|
|
88
|
+
cloudcat --path s3://data/extract.csv --delimiter "|"
|
|
89
|
+
|
|
90
|
+
# Semicolon-delimited files (common in Europe)
|
|
91
|
+
cloudcat --path gcs://reports/data.csv --delimiter ";"
|
|
92
|
+
```
|
|
93
|
+
|
|
74
94
|
## Output Formats
|
|
75
95
|
|
|
76
96
|
- **table**: Formatted table with colored headers (default)
|
|
@@ -89,6 +109,16 @@ Make sure you have the appropriate credentials configured before using the tool.
|
|
|
89
109
|
|
|
90
110
|
## Example Use Cases
|
|
91
111
|
|
|
112
|
+
### Reading Tab-Delimited Files
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Read a tab-delimited file and show as table
|
|
116
|
+
cloudcat --path gcs://analytics/export.tsv --delimiter "\t"
|
|
117
|
+
|
|
118
|
+
# Convert tab-delimited to JSON
|
|
119
|
+
cloudcat --path s3://exports/data.tsv --delimiter "\t" --output-format jsonp
|
|
120
|
+
```
|
|
121
|
+
|
|
92
122
|
### Exploring a Spark Output Directory
|
|
93
123
|
|
|
94
124
|
```bash
|
|
@@ -114,6 +144,9 @@ cloudcat --path gcs://analytics/events.json --columns user_id,event_type,timesta
|
|
|
114
144
|
```bash
|
|
115
145
|
# Export data as CSV
|
|
116
146
|
cloudcat --path s3://exports/data.parquet --num-rows 1000 --output-format csv > exported_data.csv
|
|
147
|
+
|
|
148
|
+
# Convert tab-delimited to comma-delimited
|
|
149
|
+
cloudcat --path gcs://exports/data.tsv --delimiter "\t" --output-format csv > converted.csv
|
|
117
150
|
```
|
|
118
151
|
|
|
119
152
|
### Handling Small Files
|
|
@@ -123,14 +156,6 @@ cloudcat --path s3://exports/data.parquet --num-rows 1000 --output-format csv >
|
|
|
123
156
|
cloudcat --path gcs://logs/daily/ --multi-file-mode all --max-size-mb 100
|
|
124
157
|
```
|
|
125
158
|
|
|
126
|
-
## Example Outputs
|
|
127
|
-
|
|
128
|
-
### Table Format (default)
|
|
129
|
-

|
|
130
|
-
|
|
131
|
-
### Pretty JSON (jsonp)
|
|
132
|
-

|
|
133
|
-
|
|
134
159
|
## License
|
|
135
160
|
|
|
136
161
|
MIT
|
|
@@ -275,13 +275,16 @@ def get_s3_stream(bucket_name, object_name):
|
|
|
275
275
|
return response['Body']
|
|
276
276
|
|
|
277
277
|
|
|
278
|
-
def read_csv_data(stream, num_rows, columns=None):
|
|
278
|
+
def read_csv_data(stream, num_rows, columns=None, delimiter=None):
|
|
279
279
|
"""Read CSV data from a stream."""
|
|
280
280
|
# First read the data without column filtering to get full schema
|
|
281
|
-
if num_rows > 0
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
281
|
+
pd_args = {'nrows': num_rows} if num_rows > 0 else {}
|
|
282
|
+
|
|
283
|
+
# Add delimiter if specified
|
|
284
|
+
if delimiter:
|
|
285
|
+
pd_args['delimiter'] = delimiter
|
|
286
|
+
|
|
287
|
+
full_df = pd.read_csv(stream, **pd_args)
|
|
285
288
|
|
|
286
289
|
# Store the full schema for later use
|
|
287
290
|
full_schema = full_df.dtypes
|
|
@@ -384,8 +387,19 @@ def read_parquet_data(stream, num_rows, columns=None):
|
|
|
384
387
|
df = table.to_pandas()
|
|
385
388
|
|
|
386
389
|
# Get the full schema as a pandas Series for consistency with other formats
|
|
387
|
-
# First, read a small
|
|
388
|
-
|
|
390
|
+
# First, read a small sample to get pandas dtypes - use read_row_group to limit rows
|
|
391
|
+
# FIXED: PyArrow doesn't have nrows parameter for read_table
|
|
392
|
+
if parquet_file.num_row_groups > 0:
|
|
393
|
+
# Read just first row group
|
|
394
|
+
sample_table = parquet_file.read_row_group(0)
|
|
395
|
+
if sample_table.num_rows > 1:
|
|
396
|
+
# Slice to just the first row if needed
|
|
397
|
+
sample_table = sample_table.slice(0, 1)
|
|
398
|
+
full_df = sample_table.to_pandas()
|
|
399
|
+
else:
|
|
400
|
+
# If no row groups, create empty DataFrame with correct schema
|
|
401
|
+
full_df = df.iloc[0:0] if not df.empty else pd.DataFrame()
|
|
402
|
+
|
|
389
403
|
full_schema = full_df.dtypes
|
|
390
404
|
|
|
391
405
|
return df, full_schema
|
|
@@ -400,7 +414,7 @@ def read_parquet_data(stream, num_rows, columns=None):
|
|
|
400
414
|
pass
|
|
401
415
|
|
|
402
416
|
|
|
403
|
-
def read_data_from_multiple_files(service, bucket, file_list, input_format, num_rows, columns=None):
|
|
417
|
+
def read_data_from_multiple_files(service, bucket, file_list, input_format, num_rows, columns=None, delimiter=None):
|
|
404
418
|
"""Read data from multiple files and concatenate the results."""
|
|
405
419
|
dfs = []
|
|
406
420
|
schemas = []
|
|
@@ -424,7 +438,7 @@ def read_data_from_multiple_files(service, bucket, file_list, input_format, num_
|
|
|
424
438
|
|
|
425
439
|
# Read the file
|
|
426
440
|
if input_format == 'csv':
|
|
427
|
-
df, schema = read_csv_data(stream, remaining_rows if remaining_rows > 0 else 0, columns)
|
|
441
|
+
df, schema = read_csv_data(stream, remaining_rows if remaining_rows > 0 else 0, columns, delimiter)
|
|
428
442
|
elif input_format == 'json':
|
|
429
443
|
df, schema = read_json_data(stream, remaining_rows if remaining_rows > 0 else 0, columns)
|
|
430
444
|
elif input_format == 'parquet':
|
|
@@ -477,7 +491,7 @@ def read_data_from_multiple_files(service, bucket, file_list, input_format, num_
|
|
|
477
491
|
return result_df, full_schema, total_rows
|
|
478
492
|
|
|
479
493
|
|
|
480
|
-
def read_data(service, bucket, object_path, input_format, num_rows, columns=None):
|
|
494
|
+
def read_data(service, bucket, object_path, input_format, num_rows, columns=None, delimiter=None):
|
|
481
495
|
"""Read data from cloud storage."""
|
|
482
496
|
# Get appropriate stream based on service
|
|
483
497
|
if service == 'gcs':
|
|
@@ -489,7 +503,7 @@ def read_data(service, bucket, object_path, input_format, num_rows, columns=None
|
|
|
489
503
|
|
|
490
504
|
# Read based on format
|
|
491
505
|
if input_format == 'csv':
|
|
492
|
-
return read_csv_data(stream, num_rows, columns)
|
|
506
|
+
return read_csv_data(stream, num_rows, columns, delimiter)
|
|
493
507
|
elif input_format == 'json':
|
|
494
508
|
return read_json_data(stream, num_rows, columns)
|
|
495
509
|
elif input_format == 'parquet':
|
|
@@ -498,7 +512,7 @@ def read_data(service, bucket, object_path, input_format, num_rows, columns=None
|
|
|
498
512
|
raise ValueError(f"Unsupported format: {input_format}")
|
|
499
513
|
|
|
500
514
|
|
|
501
|
-
def get_record_count(service, bucket, object_path, input_format):
|
|
515
|
+
def get_record_count(service, bucket, object_path, input_format, delimiter=None):
|
|
502
516
|
"""Get record count from a file."""
|
|
503
517
|
if input_format == 'parquet' and HAS_PARQUET:
|
|
504
518
|
# For Parquet, we can get count from metadata
|
|
@@ -538,7 +552,13 @@ def get_record_count(service, bucket, object_path, input_format):
|
|
|
538
552
|
|
|
539
553
|
if input_format == 'csv':
|
|
540
554
|
chunk_count = 0
|
|
541
|
-
|
|
555
|
+
|
|
556
|
+
# Add delimiter if specified
|
|
557
|
+
read_args = {'chunksize': 10000}
|
|
558
|
+
if delimiter:
|
|
559
|
+
read_args['delimiter'] = delimiter
|
|
560
|
+
|
|
561
|
+
for chunk in pd.read_csv(stream, **read_args):
|
|
542
562
|
chunk_count += len(chunk)
|
|
543
563
|
return chunk_count
|
|
544
564
|
elif input_format == 'json':
|
|
@@ -621,7 +641,9 @@ def format_table_with_colored_header(df):
|
|
|
621
641
|
help='How to handle directories with multiple files (default: auto)')
|
|
622
642
|
@click.option('--max-size-mb', default=25, type=int,
|
|
623
643
|
help='Maximum size in MB to read when reading multiple files (default: 25)')
|
|
624
|
-
|
|
644
|
+
@click.option('--delimiter', '-d', help='Delimiter to use for CSV files (use "\\t" for tab)')
|
|
645
|
+
def main(path, output_format, input_format, columns, num_rows, schema, no_count,
|
|
646
|
+
multi_file_mode, max_size_mb, delimiter):
|
|
625
647
|
"""Display data from files in Google Cloud Storage or AWS S3.
|
|
626
648
|
|
|
627
649
|
Example usage:
|
|
@@ -649,8 +671,16 @@ def main(path, output_format, input_format, columns, num_rows, schema, no_count,
|
|
|
649
671
|
\b
|
|
650
672
|
# Read from multiple files in a directory (up to 25MB)
|
|
651
673
|
cloudcat --path s3://my-bucket/daily-data/ --multi-file-mode all --max-size-mb 25
|
|
674
|
+
|
|
675
|
+
\b
|
|
676
|
+
# Read a tab-delimited file
|
|
677
|
+
cloudcat --path gcs://my-bucket/data.csv --delimiter "\\t"
|
|
652
678
|
"""
|
|
653
679
|
try:
|
|
680
|
+
# Handle special characters in delimiter
|
|
681
|
+
if delimiter == "\\t":
|
|
682
|
+
delimiter = "\t"
|
|
683
|
+
|
|
654
684
|
# Parse the path
|
|
655
685
|
service, bucket, object_path = parse_cloud_path(path)
|
|
656
686
|
|
|
@@ -672,7 +702,7 @@ def main(path, output_format, input_format, columns, num_rows, schema, no_count,
|
|
|
672
702
|
click.echo(Fore.BLUE + f"Inferred input format: {input_format}" + Style.RESET_ALL)
|
|
673
703
|
|
|
674
704
|
# Read the data from the single file
|
|
675
|
-
df, full_schema = read_data(service, bucket, object_path, input_format, num_rows, columns)
|
|
705
|
+
df, full_schema = read_data(service, bucket, object_path, input_format, num_rows, columns, delimiter)
|
|
676
706
|
total_record_count = None # Will be computed later if needed
|
|
677
707
|
else:
|
|
678
708
|
# Read from multiple files
|
|
@@ -689,7 +719,7 @@ def main(path, output_format, input_format, columns, num_rows, schema, no_count,
|
|
|
689
719
|
|
|
690
720
|
# Read data from multiple files
|
|
691
721
|
df, full_schema, total_record_count = read_data_from_multiple_files(
|
|
692
|
-
service, bucket, file_list, input_format, num_rows, columns
|
|
722
|
+
service, bucket, file_list, input_format, num_rows, columns, delimiter
|
|
693
723
|
)
|
|
694
724
|
|
|
695
725
|
# Update object_path for display/logging purposes
|
|
@@ -702,7 +732,7 @@ def main(path, output_format, input_format, columns, num_rows, schema, no_count,
|
|
|
702
732
|
click.echo(Fore.BLUE + f"Inferred input format: {input_format}" + Style.RESET_ALL)
|
|
703
733
|
|
|
704
734
|
# Read the data
|
|
705
|
-
df, full_schema = read_data(service, bucket, object_path, input_format, num_rows, columns)
|
|
735
|
+
df, full_schema = read_data(service, bucket, object_path, input_format, num_rows, columns, delimiter)
|
|
706
736
|
total_record_count = None # Will be computed later if needed
|
|
707
737
|
|
|
708
738
|
# Display schema if requested
|
|
@@ -718,7 +748,7 @@ def main(path, output_format, input_format, columns, num_rows, schema, no_count,
|
|
|
718
748
|
if not no_count:
|
|
719
749
|
try:
|
|
720
750
|
if total_record_count is None:
|
|
721
|
-
total_record_count = get_record_count(service, bucket, object_path, input_format)
|
|
751
|
+
total_record_count = get_record_count(service, bucket, object_path, input_format, delimiter)
|
|
722
752
|
click.echo(Fore.CYAN + f"Total records: {total_record_count}" + Style.RESET_ALL)
|
|
723
753
|
except Exception as e:
|
|
724
754
|
click.echo(Fore.YELLOW + f"Could not count records: {str(e)}" + Style.RESET_ALL)
|
|
@@ -743,7 +773,7 @@ def main(path, output_format, input_format, columns, num_rows, schema, no_count,
|
|
|
743
773
|
if not no_count:
|
|
744
774
|
try:
|
|
745
775
|
if total_record_count is None:
|
|
746
|
-
total_record_count = get_record_count(service, bucket, object_path, input_format)
|
|
776
|
+
total_record_count = get_record_count(service, bucket, object_path, input_format, delimiter)
|
|
747
777
|
click.echo(Fore.CYAN + f"\nTotal records: {total_record_count}" + Style.RESET_ALL)
|
|
748
778
|
except Exception as e:
|
|
749
779
|
click.echo(Fore.YELLOW + f"\nCould not count records: {str(e)}" + Style.RESET_ALL)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|