cloudcat 0.1.1__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+
2
+ #!/usr/bin/env python
3
+ """cloudcat - A CLI utility to read and display files from cloud storage."""
4
+
5
+ __version__ = "0.1.1"
cloudcat/cli.py ADDED
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env python
2
+ import click
3
+ import pandas as pd
4
+ import sys
5
+ import io
6
+ from tabulate import tabulate
7
+ from colorama import init, Fore, Style
8
+ import json
9
+ from urllib.parse import urlparse
10
+ import tempfile
11
+
12
+ # Initialize colorama
13
+ init()
14
+
15
+ # For GCS
16
+ try:
17
+ from google.cloud import storage as gcs
18
+ HAS_GCS = True
19
+ except ImportError:
20
+ HAS_GCS = False
21
+
22
+ # For S3
23
+ try:
24
+ import boto3
25
+ import botocore
26
+ HAS_S3 = True
27
+ except ImportError:
28
+ HAS_S3 = False
29
+
30
+ # For Parquet
31
+ try:
32
+ import pyarrow.parquet as pq
33
+ import pyarrow as pa
34
+ HAS_PARQUET = True
35
+ except ImportError:
36
+ HAS_PARQUET = False
37
+
38
+
39
+ def parse_cloud_path(path):
40
+ """Parse a cloud storage path into service, bucket, and object components."""
41
+ parsed = urlparse(path)
42
+
43
+ if parsed.scheme == 'gs' or parsed.scheme == 'gcs':
44
+ service = 'gcs'
45
+ bucket = parsed.netloc
46
+ object_path = parsed.path.lstrip('/')
47
+ elif parsed.scheme == 's3':
48
+ service = 's3'
49
+ bucket = parsed.netloc
50
+ object_path = parsed.path.lstrip('/')
51
+ else:
52
+ raise ValueError(f"Unsupported scheme: {parsed.scheme}. Use gcs:// or s3://")
53
+
54
+ return service, bucket, object_path
55
+
56
+
57
+ def detect_format_from_path(path):
58
+ """Detect file format from file extension."""
59
+ if path.lower().endswith('.json'):
60
+ return 'json'
61
+ elif path.lower().endswith('.csv'):
62
+ return 'csv'
63
+ elif path.lower().endswith('.parquet'):
64
+ return 'parquet'
65
+ else:
66
+ raise ValueError(f"Could not infer format from path: {path}. Please specify --input-format.")
67
+
68
+
69
+ def get_gcs_stream(bucket_name, object_name):
70
+ """Get a file stream from GCS with minimal downloading."""
71
+ if not HAS_GCS:
72
+ sys.stderr.write(Fore.RED + "Error: google-cloud-storage package is required for GCS access.\n" +
73
+ "Install it with: pip install google-cloud-storage\n" + Style.RESET_ALL)
74
+ sys.exit(1)
75
+
76
+ client = gcs.Client()
77
+ bucket = client.bucket(bucket_name)
78
+ blob = bucket.blob(object_name)
79
+
80
+ # Create a streaming buffer
81
+ buffer = io.BytesIO()
82
+ blob.download_to_file(buffer)
83
+ buffer.seek(0)
84
+
85
+ return buffer
86
+
87
+
88
+ def get_s3_stream(bucket_name, object_name):
89
+ """Get a file stream from S3."""
90
+ if not HAS_S3:
91
+ sys.stderr.write(Fore.RED + "Error: boto3 package is required for S3 access.\n" +
92
+ "Install it with: pip install boto3\n" + Style.RESET_ALL)
93
+ sys.exit(1)
94
+
95
+ s3 = boto3.client('s3')
96
+ response = s3.get_object(Bucket=bucket_name, Key=object_name)
97
+ return response['Body']
98
+
99
+
100
+ def read_csv_data(stream, num_rows, columns=None):
101
+ """Read CSV data from a stream."""
102
+ if num_rows > 0:
103
+ df = pd.read_csv(stream, nrows=num_rows)
104
+ else:
105
+ df = pd.read_csv(stream)
106
+
107
+ if columns:
108
+ cols = [c.strip() for c in columns.split(',')]
109
+ valid_cols = [c for c in cols if c in df.columns]
110
+ if len(valid_cols) != len(cols):
111
+ missing = set(cols) - set(valid_cols)
112
+ click.echo(Fore.YELLOW + f"Warning: Columns not found: {', '.join(missing)}" + Style.RESET_ALL)
113
+ df = df[valid_cols]
114
+
115
+ return df
116
+
117
+
118
+ def read_json_data(stream, num_rows, columns=None):
119
+ """Read JSON data from a stream."""
120
+ if num_rows > 0:
121
+ df = pd.read_json(stream, lines=True, nrows=num_rows)
122
+ else:
123
+ df = pd.read_json(stream, lines=True)
124
+
125
+ if columns:
126
+ cols = [c.strip() for c in columns.split(',')]
127
+ valid_cols = [c for c in cols if c in df.columns]
128
+ if len(valid_cols) != len(cols):
129
+ missing = set(cols) - set(valid_cols)
130
+ click.echo(Fore.YELLOW + f"Warning: Columns not found: {', '.join(missing)}" + Style.RESET_ALL)
131
+ df = df[valid_cols]
132
+
133
+ return df
134
+
135
+
136
+ def read_parquet_data(stream, num_rows, columns=None):
137
+ """Read Parquet data from a stream."""
138
+ if not HAS_PARQUET:
139
+ sys.stderr.write(Fore.RED + "Error: pyarrow package is required for Parquet support.\n" +
140
+ "Install it with: pip install pyarrow\n" + Style.RESET_ALL)
141
+ sys.exit(1)
142
+
143
+ # For Parquet, we need a temporary file to properly read the metadata
144
+ with tempfile.NamedTemporaryFile(delete=False) as temp_file:
145
+ temp_path = temp_file.name
146
+
147
+ try:
148
+ # If stream is a file-like object, copy to temp file
149
+ if hasattr(stream, 'read'):
150
+ with open(temp_path, 'wb') as f:
151
+ f.write(stream.read())
152
+ else:
153
+ # Assume it's already a path
154
+ temp_path = stream
155
+
156
+ parquet_file = pq.ParquetFile(temp_path)
157
+
158
+ # Select columns if specified
159
+ col_names = columns.split(',') if columns else None
160
+
161
+ # Read the data efficiently
162
+ if num_rows > 0:
163
+ tables = []
164
+ rows_read = 0
165
+
166
+ for i in range(parquet_file.num_row_groups):
167
+ if rows_read >= num_rows:
168
+ break
169
+
170
+ table = parquet_file.read_row_group(i, columns=col_names)
171
+
172
+ # Limit rows if needed for the last batch
173
+ if rows_read + table.num_rows > num_rows:
174
+ table = table.slice(0, num_rows - rows_read)
175
+
176
+ tables.append(table)
177
+ rows_read += min(table.num_rows, num_rows - rows_read)
178
+
179
+ if tables:
180
+ result_table = pa.concat_tables(tables)
181
+ return result_table.to_pandas()
182
+ else:
183
+ return pd.DataFrame()
184
+ else:
185
+ # Read all data (potentially with column filtering)
186
+ table = parquet_file.read(columns=col_names)
187
+ return table.to_pandas()
188
+
189
+ finally:
190
+ # Clean up the temporary file
191
+ import os
192
+ try:
193
+ if hasattr(stream, 'read'): # Only delete if we created temp file
194
+ os.unlink(temp_path)
195
+ except:
196
+ pass
197
+
198
+
199
+ def read_data(service, bucket, object_path, input_format, num_rows, columns=None):
200
+ """Read data from cloud storage."""
201
+ # Get appropriate stream based on service
202
+ if service == 'gcs':
203
+ stream = get_gcs_stream(bucket, object_path)
204
+ elif service == 's3':
205
+ stream = get_s3_stream(bucket, object_path)
206
+ else:
207
+ raise ValueError(f"Unsupported service: {service}")
208
+
209
+ # Read based on format
210
+ if input_format == 'csv':
211
+ return read_csv_data(stream, num_rows, columns)
212
+ elif input_format == 'json':
213
+ return read_json_data(stream, num_rows, columns)
214
+ elif input_format == 'parquet':
215
+ return read_parquet_data(stream, num_rows, columns)
216
+ else:
217
+ raise ValueError(f"Unsupported format: {input_format}")
218
+
219
+
220
+ def get_record_count(service, bucket, object_path, input_format):
221
+ """Get record count from a file."""
222
+ if input_format == 'parquet' and HAS_PARQUET:
223
+ # For Parquet, we can get count from metadata
224
+ if service == 'gcs':
225
+ stream = get_gcs_stream(bucket, object_path)
226
+ elif service == 's3':
227
+ stream = get_s3_stream(bucket, object_path)
228
+
229
+ with tempfile.NamedTemporaryFile(delete=False) as temp_file:
230
+ temp_path = temp_file.name
231
+
232
+ try:
233
+ if hasattr(stream, 'read'):
234
+ with open(temp_path, 'wb') as f:
235
+ f.write(stream.read())
236
+ else:
237
+ temp_path = stream
238
+
239
+ parquet_file = pq.ParquetFile(temp_path)
240
+ return parquet_file.metadata.num_rows
241
+ finally:
242
+ import os
243
+ try:
244
+ if hasattr(stream, 'read'):
245
+ os.unlink(temp_path)
246
+ except:
247
+ pass
248
+ else:
249
+ # For CSV and JSON, we need to count the rows
250
+ click.echo(Fore.YELLOW + "Counting records (this might take a while for large files)..." + Style.RESET_ALL)
251
+
252
+ # Use pandas to count rows in chunks
253
+ if service == 'gcs':
254
+ stream = get_gcs_stream(bucket, object_path)
255
+ elif service == 's3':
256
+ stream = get_s3_stream(bucket, object_path)
257
+
258
+ if input_format == 'csv':
259
+ chunk_count = 0
260
+ for chunk in pd.read_csv(stream, chunksize=10000):
261
+ chunk_count += len(chunk)
262
+ return chunk_count
263
+ elif input_format == 'json':
264
+ chunk_count = 0
265
+ for chunk in pd.read_json(stream, lines=True, chunksize=10000):
266
+ chunk_count += len(chunk)
267
+ return chunk_count
268
+
269
+ return "Unknown"
270
+
271
+
272
+ @click.command()
273
+ @click.option('--path', required=True, help='Path to the file (gcs://... or s3://...)')
274
+ @click.option('--output-format', type=click.Choice(['json', 'csv', 'table']), default='table',
275
+ help='Output format (default: table)')
276
+ @click.option('--input-format', type=click.Choice(['json', 'csv', 'parquet']),
277
+ help='Input format (default: inferred from path)')
278
+ @click.option('--columns', help='Comma-separated list of columns to display (default: all)')
279
+ @click.option('--num-rows', '-n', default=10, type=int, help='Number of rows to display (default: 10)')
280
+ @click.option('--schema', type=click.Choice(['show', 'dont_show', 'schema_only']), default='show',
281
+ help='Schema display option (default: show)')
282
+ @click.option('--count', is_flag=True, help='Show record count at the end')
283
+ def main(path, output_format, input_format, columns, num_rows, schema, count):
284
+ """Display data from files in Google Cloud Storage or AWS S3.
285
+
286
+ Example usage:
287
+
288
+ \b
289
+ # Read from GCS
290
+ cloudcat --path gcs://my-bucket/data.csv --output-format table
291
+
292
+ \b
293
+ # Read from S3 with column selection
294
+ cloudcat --path s3://my-bucket/data.parquet --columns id,name,value
295
+
296
+ \b
297
+ # Show record count and limit to 20 rows
298
+ cloudcat --path gcs://bucket/events.json --num-rows 20 --count
299
+ """
300
+ try:
301
+ # Parse the path
302
+ service, bucket, object_path = parse_cloud_path(path)
303
+
304
+ # Determine input format if not specified
305
+ if not input_format:
306
+ input_format = detect_format_from_path(object_path)
307
+ click.echo(Fore.BLUE + f"Inferred input format: {input_format}" + Style.RESET_ALL)
308
+
309
+ # Read the data
310
+ df = read_data(service, bucket, object_path, input_format, num_rows, columns)
311
+
312
+ # Display schema if requested
313
+ if schema in ['show', 'schema_only']:
314
+ click.echo(Fore.GREEN + "Schema:" + Style.RESET_ALL)
315
+ for col, dtype in df.dtypes.items():
316
+ click.echo(f" {col}: {dtype}")
317
+ click.echo("")
318
+
319
+ # Exit if only schema was requested
320
+ if schema == 'schema_only':
321
+ return
322
+
323
+ # Display the data
324
+ if output_format == 'table':
325
+ click.echo(tabulate(df, headers='keys', tablefmt='psql', showindex=False))
326
+ elif output_format == 'json':
327
+ click.echo(df.to_json(orient='records', lines=True))
328
+ elif output_format == 'csv':
329
+ click.echo(df.to_csv(index=False))
330
+
331
+ # Count records if requested
332
+ if count:
333
+ try:
334
+ total_count = get_record_count(service, bucket, object_path, input_format)
335
+ click.echo(Fore.CYAN + f"\nTotal records: {total_count}" + Style.RESET_ALL)
336
+ except Exception as e:
337
+ click.echo(Fore.YELLOW + f"\nCould not count records: {str(e)}" + Style.RESET_ALL)
338
+
339
+ except Exception as e:
340
+ click.echo(Fore.RED + f"Error: {str(e)}" + Style.RESET_ALL, err=True)
341
+ sys.exit(1)
342
+
343
+
344
+ if __name__ == '__main__':
345
+ main()
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: cloudcat
3
+ Version: 0.1.1
4
+ Summary: A CLI utility to read and display files from cloud storage
5
+ Home-page: https://github.com/yourusername/cloudcat
6
+ Author: Your Name
7
+ Author-email: your.email@example.com
8
+ Keywords: cloud,gcs,s3,cli,storage,data
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Utilities
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.7
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Requires-Python: >=3.7
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: click>=8.0.0
21
+ Requires-Dist: pandas>=1.3.0
22
+ Requires-Dist: tabulate>=0.8.9
23
+ Requires-Dist: colorama>=0.4.4
24
+ Provides-Extra: gcs
25
+ Requires-Dist: google-cloud-storage>=2.0.0; extra == "gcs"
26
+ Provides-Extra: s3
27
+ Requires-Dist: boto3>=1.18.0; extra == "s3"
28
+ Provides-Extra: parquet
29
+ Requires-Dist: pyarrow>=5.0.0; extra == "parquet"
30
+ Provides-Extra: all
31
+ Requires-Dist: google-cloud-storage>=2.0.0; extra == "all"
32
+ Requires-Dist: boto3>=1.18.0; extra == "all"
33
+ Requires-Dist: pyarrow>=5.0.0; extra == "all"
34
+ Dynamic: author
35
+ Dynamic: author-email
36
+ Dynamic: classifier
37
+ Dynamic: description
38
+ Dynamic: description-content-type
39
+ Dynamic: home-page
40
+ Dynamic: keywords
41
+ Dynamic: license-file
42
+ Dynamic: provides-extra
43
+ Dynamic: requires-dist
44
+ Dynamic: requires-python
45
+ Dynamic: summary
46
+
47
+
48
+ # CloudCat
49
+
50
+ A command-line utility to read and display files from Google Cloud Storage and AWS S3 buckets.
51
+
52
+ ## Features
53
+
54
+ - Read files from GCS (gcs://) or S3 (s3://)
55
+ - Stream data (avoid downloading entire files when possible)
56
+ - Support for CSV, JSON, and Parquet formats
57
+ - Column selection and row limiting
58
+ - Schema display
59
+ - Record counting
60
+
@@ -0,0 +1,8 @@
1
+ cloudcat/__init__.py,sha256=ZntHrMzdkU3hB1jG5FaBVk25PFOl-JZaGTX2sYhm62w,122
2
+ cloudcat/cli.py,sha256=nKfjOHEVx3Qszj9cXouMsZhlmgAHY6rdmNvM5-zlOyQ,11793
3
+ cloudcat-0.1.1.dist-info/licenses/LICENSE,sha256=_yLa5HTj2C4OoQQR4xIdE6JQd9tfm2Uk-1Ir0dM5iQM,1074
4
+ cloudcat-0.1.1.dist-info/METADATA,sha256=I1KjSACgc2j-slzTc1M29xv-rLejMkjs-nhAhGiYogA,1880
5
+ cloudcat-0.1.1.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
6
+ cloudcat-0.1.1.dist-info/entry_points.txt,sha256=SREU3NzaQasfEMWKR70wteKtLNhSmOGdNB3LCL8b4QI,47
7
+ cloudcat-0.1.1.dist-info/top_level.txt,sha256=ivkPFw9-wEfCEGSzaIpoM89ohFkPslHlce8RN04Cfck,9
8
+ cloudcat-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.4.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cloudcat = cloudcat.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jonathan Sudhakar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ cloudcat