sql2csv 1.4__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.
sql2csv/__init__.py ADDED
File without changes
sql2csv/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from . import sql2csv
2
+
3
+ if __name__ == '__main__':
4
+ sql2csv.main()
sql2csv/sql2csv.py ADDED
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import sys
4
+ import csv
5
+ import tempfile
6
+ import os
7
+ from os.path import expanduser
8
+ import json
9
+
10
+ import argparse
11
+ import pymysql.cursors
12
+ import pymysql.constants.CLIENT
13
+ import psycopg2.extras
14
+ import psycopg2
15
+
16
+ file_ = None
17
+
18
+
19
+ def get_mysql_connection(host, user, port, password, database):
20
+ """ MySQL connection """
21
+
22
+ return pymysql.connect(host=host,
23
+ user=user,
24
+ port=port,
25
+ password=password,
26
+ database=database,
27
+ charset='utf8mb4',
28
+ client_flag=pymysql.constants.CLIENT.MULTI_STATEMENTS
29
+ )
30
+
31
+
32
+ def get_pg_connection(host, user, port, password, database):
33
+ """ PostgreSQL connection """
34
+
35
+ return psycopg2.connect(host=host,
36
+ user=user,
37
+ port=port,
38
+ password=password,
39
+ dbname=database
40
+ )
41
+
42
+
43
+ def get_connection(engine, host, user, port, password, database):
44
+ """ Get SQL connection """
45
+
46
+ if engine == 'mysql':
47
+ return get_mysql_connection(host, user, port, password, database)
48
+ elif engine == 'postgresql':
49
+ return get_pg_connection(host, user, port, password, database)
50
+ else:
51
+ raise RuntimeError(
52
+ '"%s" engine is not supported.' % (engine))
53
+
54
+
55
+ def get_cursor(connection):
56
+ """ Return connection cursor """
57
+
58
+ return connection.cursor()
59
+
60
+
61
+ def execute_query(cursor, query):
62
+ """ Run a query and yield each row """
63
+
64
+ cursor.execute(query)
65
+
66
+
67
+ def fetch_rows(cursor):
68
+ """ Fetch and yield rows """
69
+
70
+ for row in cursor.fetchall():
71
+ yield row
72
+
73
+
74
+ def fetch_headers(cursor):
75
+ """ Return list of headers """
76
+
77
+ return [i[0] for i in cursor.description]
78
+
79
+
80
+ def discard_line(line):
81
+ """ Decide whether we should keep or discard a line """
82
+
83
+ if line[:1] in ['', '+', '(', '-']:
84
+ return True
85
+
86
+ return False
87
+
88
+
89
+ def remove_leading_trailing_pipe(line):
90
+ """ Remove optional leading and trailing pipe """
91
+
92
+ return line.strip('|')
93
+
94
+
95
+ def get_column_separator(input_):
96
+ """ Return the column separator """
97
+ """ This logic needs to be improved """
98
+
99
+ if input_.count('|') > input_.count('\t'):
100
+ return '|'
101
+
102
+ return '\t'
103
+
104
+
105
+ def split_columns(line, separator='\t'):
106
+ """ Split a line with a "separator" """
107
+
108
+ return line.split(separator)
109
+
110
+
111
+ def strip_whitespaces(tpl):
112
+ """ Strip white spaces before and after each item """
113
+
114
+ return [item.strip() for item in tpl]
115
+
116
+
117
+ def stringify_items(row):
118
+ """ Loop thru each item and Json-encode dicts """
119
+
120
+ row = list(row)
121
+ for k, item in enumerate(row):
122
+ if (isinstance(item, dict)):
123
+ row[k] = json.dumps(item)
124
+
125
+ return tuple(row)
126
+
127
+
128
+ def has_stdin_input():
129
+ """ Return `True` if there is an stdin input """
130
+
131
+ if not sys.stdin.isatty():
132
+ return True
133
+
134
+ return False
135
+
136
+
137
+ def resolve_home_dir(destination):
138
+ """ Resolve `~` to a full path """
139
+
140
+ if destination[:1] == '~':
141
+ return expanduser("~") + destination[1:]
142
+
143
+ return destination
144
+
145
+
146
+ def open_file(destination):
147
+ """ Open file """
148
+
149
+ global file_
150
+
151
+ file_ = open(destination, 'w+', newline='')
152
+
153
+ return file_
154
+
155
+
156
+ def open_tempfile():
157
+ """ Open a temporary file """
158
+
159
+ global file_
160
+
161
+ file_ = tempfile.NamedTemporaryFile('w+', newline='', delete=False)
162
+
163
+ return file_
164
+
165
+
166
+ def get_writer(file_, delimiter=',', quotechar='"'):
167
+ """ Return a writer object """
168
+
169
+ return csv.writer(
170
+ file_,
171
+ delimiter=delimiter,
172
+ quotechar=quotechar,
173
+ quoting=csv.QUOTE_MINIMAL
174
+ )
175
+
176
+
177
+ def file_to_stdout(remove=False):
178
+ """ Print file content to stdout """
179
+
180
+ with open(file_.name) as f:
181
+ print(f.read())
182
+
183
+ # Remove the temporary file once it has been printed
184
+ if remove:
185
+ os.remove(file_.name)
186
+
187
+
188
+ def stdin_to_csv(delimiter=',', quotechar='"'):
189
+ """ Parse stdin and return output in a CSV format """
190
+
191
+ # Open CSV
192
+ with open_tempfile() as file_:
193
+ writer = get_writer(file_, delimiter=delimiter, quotechar=quotechar)
194
+
195
+ # Parse lines and add to file
196
+ separator = None
197
+ for line in sys.stdin:
198
+
199
+ if not discard_line(line):
200
+ # Get column separator
201
+ separator = get_column_separator(
202
+ line) if not separator else separator
203
+
204
+ # Remove leading and trailing |
205
+ line = remove_leading_trailing_pipe(line)
206
+
207
+ if line.strip():
208
+ # Split columns with separator
209
+ row = split_columns(line, separator)
210
+
211
+ # Add to CSV
212
+ row = strip_whitespaces(row)
213
+
214
+ # Write row
215
+ writer.writerow(row)
216
+
217
+ file_to_stdout(remove=True)
218
+
219
+
220
+ def query_to_csv(engine, host, user, port, password, database, query, headers=False, out_type='stdout', destination_file=None, delimiter=',', quotechar='"', print_info=1000):
221
+ """ Run a query and store the result to a CSV file """
222
+
223
+ # Get SQL connection
224
+ connection = get_connection(
225
+ engine=engine,
226
+ host=host,
227
+ user=user,
228
+ port=port,
229
+ password=password,
230
+ database=database
231
+ )
232
+ cursor = get_cursor(connection)
233
+
234
+ if out_type == 'file':
235
+ print('\n* Exporting rows...')
236
+
237
+ with open_tempfile() if out_type == 'stdout' else open_file(resolve_home_dir(destination_file)) as file_:
238
+ writer = get_writer(file_, delimiter=delimiter, quotechar=quotechar)
239
+
240
+ # Execute query
241
+ execute_query(cursor=cursor, query=query)
242
+
243
+ # Write headers if requested
244
+ if headers:
245
+ writer.writerow(fetch_headers(cursor=cursor))
246
+
247
+ # Write rows to CSV
248
+ i = 0
249
+ for row in fetch_rows(cursor=cursor):
250
+ # Increment row counter
251
+ i += 1
252
+
253
+ if out_type == 'file' and i % print_info == 0:
254
+ print(' ...%s rows written' % "{:,}".format(i))
255
+
256
+ row = stringify_items(row)
257
+
258
+ writer.writerow(row)
259
+
260
+ if out_type == 'file':
261
+ print(' ...done')
262
+ print('* The result has been exported to %s.\n' %
263
+ (destination_file))
264
+
265
+ # Print stdout
266
+ if out_type == 'stdout':
267
+ file_to_stdout(remove=True)
268
+
269
+
270
+ def main():
271
+ """ Parses arguments and run module """
272
+
273
+ # Intercept and parse stdin input
274
+ if has_stdin_input():
275
+ # Parse arguments
276
+ parser = argparse.ArgumentParser()
277
+ parser.add_argument("-D", "--delimiter",
278
+ help="CSV delimiter", default=',')
279
+ parser.add_argument("-Q", "--quotechar",
280
+ help="CSV quote character", default='"')
281
+ args = parser.parse_args()
282
+
283
+ return stdin_to_csv(delimiter=args.delimiter, quotechar=args.quotechar)
284
+
285
+ # Parse arguments
286
+ parser = argparse.ArgumentParser()
287
+ parser.add_argument("-e", "--engine", type=str, help="Database engine",
288
+ choices=['mysql', 'postgresql'], default='mysql')
289
+ parser.add_argument("-H", "--host", default="127.0.0.1",
290
+ help="Database host")
291
+ parser.add_argument("-P", "--port", type=int,
292
+ help="Database port")
293
+ parser.add_argument("-u", "--user", required=True, help="Database user")
294
+ parser.add_argument("-p", "--password", default='',
295
+ help="Database password")
296
+ parser.add_argument("-d", "--database", required=True,
297
+ help="Database name")
298
+ parser.add_argument("-q", "--query", required=True,
299
+ help="SQL query")
300
+ parser.add_argument("-o", "--out",
301
+ help="CSV destination", choices=['stdout', 'file'],
302
+ default='stdout')
303
+ parser.add_argument("-f", "--destination_file",
304
+ help="CSV destination file")
305
+ parser.add_argument("-D", "--delimiter", help="CSV delimiter", default=',')
306
+ parser.add_argument("-Q", "--quotechar",
307
+ help="CSV quote character", default='"')
308
+ parser.add_argument("-t", "--headers", action='store_true',
309
+ help="Include headers")
310
+ args = parser.parse_args()
311
+
312
+ # Set default port
313
+ if not args.port:
314
+ if args.engine == 'postgresql':
315
+ args.port = 5432 # PG default
316
+ else:
317
+ args.port = 3306 # MySQL default
318
+
319
+ # Force output to file if there is a file
320
+ if args.destination_file:
321
+ args.out = 'file'
322
+
323
+ query_to_csv(
324
+ engine=args.engine,
325
+ host=args.host,
326
+ user=args.user,
327
+ port=args.port,
328
+ password=args.password,
329
+ database=args.database,
330
+ query=args.query,
331
+ headers=args.headers,
332
+ out_type=args.out,
333
+ destination_file=args.destination_file,
334
+ delimiter=args.delimiter,
335
+ quotechar=args.quotechar
336
+ )
337
+
338
+
339
+ if __name__ == '__main__':
340
+ main()
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: sql2csv
3
+ Version: 1.4
4
+ Summary: Run MySQL and PostgreSQL queries and store result in CSV
5
+ Author-email: Gabriel Bordeaux <pypi@gab.lc>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/gabfl/sql2csv
8
+ Classifier: Topic :: Database
9
+ Classifier: Topic :: Database :: Database Engines/Servers
10
+ Classifier: Operating System :: MacOS
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Natural Language :: English
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Development Status :: 4 - Beta
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: PyMySQL>=1.1
26
+ Requires-Dist: psycopg2-binary>=2.9.10
27
+ Dynamic: license-file
28
+
29
+ # sql2csv
30
+
31
+ [![Pypi](https://img.shields.io/pypi/v/sql2csv.svg)](https://pypi.org/project/sql2csv)
32
+ [![Build Status](https://github.com/gabfl/sql2csv/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/gabfl/sql2csv/actions)
33
+ [![codecov](https://codecov.io/gh/gabfl/sql2csv/branch/main/graph/badge.svg)](https://codecov.io/gh/gabfl/sql2csv)
34
+ [![MIT licensed](https://img.shields.io/badge/license-MIT-green.svg)](https://raw.githubusercontent.com/gabfl/sql2csv/main/LICENSE)
35
+
36
+ Run MySQL and PostgreSQL queries and store the results in CSV.
37
+
38
+ ## Why sql2csv
39
+
40
+ `sql2csv` is a small utility to run MySQL and PostgreSQL queries and store the output in a CSV file.
41
+
42
+ In some environments like when using MySQL or Aurora in AWS RDS, exporting queries' results to CSV is not available with native tools. `sql2csv` is a simple module that offers this feature.
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip3 install sql2csv
48
+
49
+ # Basic usage
50
+ mysql [...] -e "SELECT * FROM table" | sql2csv
51
+ # or
52
+ psql [...] -c "SELECT * FROM table" | sql2csv
53
+ ```
54
+
55
+ ## Example
56
+
57
+ ### From stdin
58
+
59
+ For simple queries you can pipe a result directly from `mysql` or `psql` to `sql2csv`.
60
+
61
+ For more complex queries, it is recommended to use the CLI (see below) to ensure a properly formatted CSV.
62
+
63
+ ```bash
64
+ mysql -U root -p"secret" my_db -e "SELECT * FROM some_mysql_table;" | sql2csv
65
+
66
+ id,some_int,some_str,some_date
67
+ 1,12,hello world,2018-12-01 12:23:12
68
+ 2,15,hello,2018-12-05 12:18:12
69
+ 3,18,world,2018-12-08 12:17:12
70
+ ```
71
+
72
+ ```bash
73
+ psql -U postgres my_db -c "SELECT * FROM some_pg_table" | sql2csv
74
+
75
+ id,some_int,some_str,some_date
76
+ 1,12,hello world,2018-12-01 12:23:12
77
+ 2,15,hello,2018-12-05 12:18:12
78
+ 3,18,world,2018-12-08 12:17:12
79
+ ```
80
+
81
+ ### Using `sql2csv` CLI
82
+
83
+ #### Output to stdout
84
+
85
+ ```bash
86
+ $ sql2csv --engine mysql \
87
+ --database my_db --user root --password "secret" \
88
+ --query "SELECT * FROM some_mysql_table"
89
+
90
+ 1,12,hello world,2018-12-01 12:23:12
91
+ 2,15,hello,2018-12-05 12:18:12
92
+ 3,18,world,2018-12-08 12:17:12
93
+ ```
94
+
95
+ #### Output saved in a file
96
+
97
+ ```bash
98
+ $ sql2csv --engine mysql \
99
+ --database my_db --user root --password "secret" \
100
+ --query "SELECT * FROM some_mysql_table" \
101
+ --headers \
102
+ --out file --destination_file export.csv
103
+
104
+ # * Exporting rows...
105
+ # ...done
106
+ # * The result has been exported to export.csv.
107
+
108
+ $ cat export.csv
109
+ id,some_int,some_str,some_date
110
+ 1,12,hello world,2018-12-01 12:23:12
111
+ 2,15,hello,2018-12-05 12:18:12
112
+ 3,18,world,2018-12-08 12:17:12
113
+ ```
114
+
115
+ ## Usage
116
+
117
+ ```bash
118
+ usage: sql2csv [-h] [-e {mysql,postgresql}] [-H HOST] [-P PORT] -u USER
119
+ [-p PASSWORD] -d DATABASE -q QUERY [-o {stdout,file}]
120
+ [-f DESTINATION_FILE] [-D DELIMITER] [-Q QUOTECHAR] [-t]
121
+
122
+ optional arguments:
123
+ -h, --help show this help message and exit
124
+ -e {mysql,postgresql}, --engine {mysql,postgresql}
125
+ Database engine
126
+ -H HOST, --host HOST Database host
127
+ -P PORT, --port PORT Database port
128
+ -u USER, --user USER Database user
129
+ -p PASSWORD, --password PASSWORD
130
+ Database password
131
+ -d DATABASE, --database DATABASE
132
+ Database name
133
+ -q QUERY, --query QUERY
134
+ SQL query
135
+ -o {stdout,file}, --out {stdout,file}
136
+ CSV destination
137
+ -f DESTINATION_FILE, --destination_file DESTINATION_FILE
138
+ CSV destination file
139
+ -D DELIMITER, --delimiter DELIMITER
140
+ CSV delimiter
141
+ -Q QUOTECHAR, --quotechar QUOTECHAR
142
+ CSV quote character
143
+ -t, --headers Include headers
144
+ ```
@@ -0,0 +1,9 @@
1
+ sql2csv/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sql2csv/__main__.py,sha256=elJffGxPxqOYcg0q8wZAUcysRB31g3mMOxuEOJYOyWw,69
3
+ sql2csv/sql2csv.py,sha256=CDZpJJsIsih91juzf5OMQSitIl7jrmJdTNRZkqwvfSw,9054
4
+ sql2csv-1.4.dist-info/licenses/LICENSE,sha256=WqS7GgVJk_T1GouVCLKKsR1q83EOah9Hpb72mq0pQ64,1073
5
+ sql2csv-1.4.dist-info/METADATA,sha256=n2caXnT9zGhqP2wCabzqKEkE2cgfPccsvqar2uvfmik,4605
6
+ sql2csv-1.4.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
7
+ sql2csv-1.4.dist-info/entry_points.txt,sha256=r6P7X-kTfb-_i1IIPX2R6e8v9ypJeVQSHO9nMXT5teU,49
8
+ sql2csv-1.4.dist-info/top_level.txt,sha256=cNCzLiAnuxrr1OQUoDDLb_6mkjGLOwEcqi0ErtFrfl0,8
9
+ sql2csv-1.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sql2csv = sql2csv.sql2csv:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Gabriel Bordeaux
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
+ sql2csv