sqlutilpy 0.27.0__tar.gz → 0.28.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.
- {sqlutilpy-0.27.0/src/sqlutilpy.egg-info → sqlutilpy-0.28.0}/PKG-INFO +1 -3
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/pyproject.toml +1 -3
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/src/sqlutilpy/sqlutil.py +209 -98
- sqlutilpy-0.28.0/src/sqlutilpy/version.py +1 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0/src/sqlutilpy.egg-info}/PKG-INFO +1 -3
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/src/sqlutilpy.egg-info/requires.txt +0 -2
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/tests/test_consistency.py +2 -0
- sqlutilpy-0.28.0/tests/test_duckdb.py +175 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/tests/test_pg.py +189 -56
- sqlutilpy-0.28.0/tests/test_sqlite.py +73 -0
- sqlutilpy-0.27.0/src/sqlutilpy/version.py +0 -1
- sqlutilpy-0.27.0/tests/test_duckdb.py +0 -32
- sqlutilpy-0.27.0/tests/test_sqlite.py +0 -32
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/LICENSE.txt +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/MANIFEST.in +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/README.md +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/setup.cfg +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/src/sqlutilpy/__init__.py +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/src/sqlutilpy.egg-info/SOURCES.txt +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/src/sqlutilpy.egg-info/dependency_links.txt +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/src/sqlutilpy.egg-info/top_level.txt +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/tests/killer.py +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/tests/test_fix_numpy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlutilpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.0
|
|
4
4
|
Summary: Database query code returning numpy arrays
|
|
5
5
|
Author-email: "Sergey E. Koposov" <skoposov@ed.ac.uk>
|
|
6
6
|
License: BSD
|
|
@@ -14,8 +14,6 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
License-File: LICENSE.txt
|
|
15
15
|
Requires-Dist: numpy
|
|
16
16
|
Requires-Dist: psycopg[binary]
|
|
17
|
-
Requires-Dist: numpydoc
|
|
18
|
-
Requires-Dist: numpy
|
|
19
17
|
Dynamic: license-file
|
|
20
18
|
|
|
21
19
|
[](https://github.com/segasai/sqlutilpy/actions)
|
|
@@ -5,6 +5,7 @@ import numpy as np
|
|
|
5
5
|
import psycopg
|
|
6
6
|
import collections
|
|
7
7
|
import warnings
|
|
8
|
+
import uuid
|
|
8
9
|
import concurrent.futures
|
|
9
10
|
|
|
10
11
|
try:
|
|
@@ -30,6 +31,26 @@ class SqlUtilException(Exception):
|
|
|
30
31
|
pass
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
SUPPORTED_DRIVERS = ('psycopg', 'sqlite3', 'duckdb')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def __check_driver(driver):
|
|
38
|
+
"""
|
|
39
|
+
Validate the driver name and return its canonical form.
|
|
40
|
+
The obsolete 'psycopg2' is accepted with a warning and mapped to
|
|
41
|
+
'psycopg'; any other unknown driver raises SqlUtilException
|
|
42
|
+
"""
|
|
43
|
+
if driver == 'psycopg2':
|
|
44
|
+
warnings.warn(
|
|
45
|
+
'psycopg2 driver is not supported anymore using psycopg instead')
|
|
46
|
+
driver = 'psycopg'
|
|
47
|
+
if driver not in SUPPORTED_DRIVERS:
|
|
48
|
+
raise SqlUtilException(
|
|
49
|
+
'Unknown driver %r; supported drivers are %s' %
|
|
50
|
+
(driver, ', '.join(SUPPORTED_DRIVERS)))
|
|
51
|
+
return driver
|
|
52
|
+
|
|
53
|
+
|
|
33
54
|
def getConnection(db=None,
|
|
34
55
|
driver=None,
|
|
35
56
|
user=None,
|
|
@@ -66,10 +87,7 @@ def getConnection(db=None,
|
|
|
66
87
|
Database Connection
|
|
67
88
|
|
|
68
89
|
"""
|
|
69
|
-
|
|
70
|
-
warnings.warn(
|
|
71
|
-
'psycopg2 driver is not supported anymore using psycopg instead')
|
|
72
|
-
driver = 'psycopg'
|
|
90
|
+
driver = __check_driver(driver)
|
|
73
91
|
if driver == 'psycopg':
|
|
74
92
|
conn_dict = dict()
|
|
75
93
|
if db is not None:
|
|
@@ -91,8 +109,6 @@ def getConnection(db=None,
|
|
|
91
109
|
elif driver == 'duckdb':
|
|
92
110
|
import duckdb
|
|
93
111
|
conn = duckdb.connect(db)
|
|
94
|
-
else:
|
|
95
|
-
raise Exception("Unknown driver")
|
|
96
112
|
return conn
|
|
97
113
|
|
|
98
114
|
|
|
@@ -100,10 +116,7 @@ def getCursor(conn, driver=None, preamb=None, named=True):
|
|
|
100
116
|
"""
|
|
101
117
|
Retrieve the database cursor
|
|
102
118
|
"""
|
|
103
|
-
|
|
104
|
-
warnings.warn(
|
|
105
|
-
'psycopg2 driver is not supported anymore using psycopg instead')
|
|
106
|
-
driver = 'psycopg'
|
|
119
|
+
driver = __check_driver(driver)
|
|
107
120
|
if driver == 'psycopg':
|
|
108
121
|
cur = conn.cursor(scrollable=False)
|
|
109
122
|
if preamb is not None:
|
|
@@ -114,7 +127,9 @@ def getCursor(conn, driver=None, preamb=None, named=True):
|
|
|
114
127
|
# different plan
|
|
115
128
|
if not named:
|
|
116
129
|
return cur
|
|
117
|
-
|
|
130
|
+
# the unique cursor name allows several batched queries
|
|
131
|
+
# to run over the same connection
|
|
132
|
+
cur = conn.cursor(name='sqlutilcursor_' + uuid.uuid4().hex)
|
|
118
133
|
cur.arraysize = config.arraysize
|
|
119
134
|
elif driver == 'sqlite3':
|
|
120
135
|
cur = conn.cursor()
|
|
@@ -124,8 +139,6 @@ def getCursor(conn, driver=None, preamb=None, named=True):
|
|
|
124
139
|
cur = conn.cursor()
|
|
125
140
|
if preamb is not None:
|
|
126
141
|
cur.execute(preamb)
|
|
127
|
-
else:
|
|
128
|
-
raise SqlUtilException('unrecognized driver')
|
|
129
142
|
return cur
|
|
130
143
|
|
|
131
144
|
|
|
@@ -187,6 +200,7 @@ def __fromrecords(recList, dtype=None, intNullVal=None, strNullVal='None'):
|
|
|
187
200
|
arr[k] = tuple(recList[k])
|
|
188
201
|
|
|
189
202
|
# Apply conversions
|
|
203
|
+
keep_object = []
|
|
190
204
|
for name, val in converters.items():
|
|
191
205
|
col = arr[name]
|
|
192
206
|
# Check for None efficiently
|
|
@@ -194,6 +208,16 @@ def __fromrecords(recList, dtype=None, intNullVal=None, strNullVal='None'):
|
|
|
194
208
|
if np.any(mask):
|
|
195
209
|
if val is not None:
|
|
196
210
|
col[mask] = val
|
|
211
|
+
else:
|
|
212
|
+
# no null replacement value was given, so we keep
|
|
213
|
+
# the column as an object column with Nones in it
|
|
214
|
+
keep_object.append(name)
|
|
215
|
+
|
|
216
|
+
if keep_object:
|
|
217
|
+
descr = np.dtype((np.record, [
|
|
218
|
+
(_n, object if _n in keep_object else dtype.fields[_n][0])
|
|
219
|
+
for _n in names
|
|
220
|
+
]))
|
|
197
221
|
|
|
198
222
|
# Cast to final dtype
|
|
199
223
|
retval = arr.astype(descr)
|
|
@@ -208,7 +232,6 @@ def __getDType(row, typeCodes, strLength):
|
|
|
208
232
|
20: 'i8',
|
|
209
233
|
21: 'i2',
|
|
210
234
|
23: 'i4',
|
|
211
|
-
1007: 'i4',
|
|
212
235
|
700: 'f4',
|
|
213
236
|
701: 'f8',
|
|
214
237
|
1000: bool,
|
|
@@ -281,6 +304,70 @@ def __get_representative_row(batch, type_codes):
|
|
|
281
304
|
return first_row
|
|
282
305
|
|
|
283
306
|
|
|
307
|
+
def __fix_object_column(col, intNullVal, strNullVal):
|
|
308
|
+
"""
|
|
309
|
+
Convert an object column into a typed numpy array if possible,
|
|
310
|
+
replacing the Nones in integer/string columns by intNullVal/strNullVal
|
|
311
|
+
and by NaNs in float columns.
|
|
312
|
+
The columns that cannot be converted are returned unchanged.
|
|
313
|
+
"""
|
|
314
|
+
# Find first non-None value to guess type
|
|
315
|
+
# (we avoid col != None here as that fails for columns
|
|
316
|
+
# containing arrays)
|
|
317
|
+
valid_mask = np.fromiter((_tmp is not None for _tmp in col),
|
|
318
|
+
dtype=bool,
|
|
319
|
+
count=len(col))
|
|
320
|
+
if not np.any(valid_mask):
|
|
321
|
+
# All None.
|
|
322
|
+
# We can't determine type. Return as is (Object filled with
|
|
323
|
+
# None)
|
|
324
|
+
return col
|
|
325
|
+
|
|
326
|
+
# Take a sample (first valid)
|
|
327
|
+
first_val = col[valid_mask][0]
|
|
328
|
+
|
|
329
|
+
# INTEGER
|
|
330
|
+
if isinstance(
|
|
331
|
+
first_val,
|
|
332
|
+
(int, np.integer)) and not isinstance(first_val, (bool, np.bool_)):
|
|
333
|
+
# Verify all valid are integers
|
|
334
|
+
valid_vals = col[valid_mask]
|
|
335
|
+
is_valid = all(
|
|
336
|
+
isinstance(x, (int, np.integer))
|
|
337
|
+
and not isinstance(x, (bool, np.bool_)) for x in valid_vals)
|
|
338
|
+
if is_valid:
|
|
339
|
+
# Convert
|
|
340
|
+
temp_col = np.copy(col)
|
|
341
|
+
temp_col[~valid_mask] = intNullVal
|
|
342
|
+
try:
|
|
343
|
+
return temp_col.astype(int)
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
|
|
347
|
+
# FLOAT
|
|
348
|
+
if isinstance(first_val, (float, np.floating)):
|
|
349
|
+
if all(
|
|
350
|
+
isinstance(x, (float, np.floating, int, np.integer))
|
|
351
|
+
for x in col[valid_mask]):
|
|
352
|
+
temp_col = np.copy(col)
|
|
353
|
+
temp_col[~valid_mask] = np.nan
|
|
354
|
+
try:
|
|
355
|
+
return temp_col.astype(float)
|
|
356
|
+
except Exception:
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
# STRING
|
|
360
|
+
if isinstance(first_val, str):
|
|
361
|
+
if all(isinstance(x, str) for x in col[valid_mask]):
|
|
362
|
+
# Replicate PG behavior: None -> strNullVal
|
|
363
|
+
temp_col = np.copy(col)
|
|
364
|
+
temp_col[~valid_mask] = strNullVal
|
|
365
|
+
# Let numpy infer string length
|
|
366
|
+
return np.array(list(temp_col))
|
|
367
|
+
|
|
368
|
+
return col
|
|
369
|
+
|
|
370
|
+
|
|
284
371
|
def __fix_numpy_record_array(res, intNullVal, strNullVal):
|
|
285
372
|
if res.size == 0:
|
|
286
373
|
return res
|
|
@@ -290,76 +377,51 @@ def __fix_numpy_record_array(res, intNullVal, strNullVal):
|
|
|
290
377
|
|
|
291
378
|
for name in names:
|
|
292
379
|
col = res[name]
|
|
293
|
-
dtype = col.dtype
|
|
294
380
|
|
|
295
|
-
if dtype.kind == 'O':
|
|
296
|
-
# Check for None (vectorized)
|
|
381
|
+
if col.dtype.kind == 'O':
|
|
297
382
|
# We treat everything as candidates if it's Object type
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if not np.any(valid_mask):
|
|
302
|
-
# All None.
|
|
303
|
-
# We can't determine type. Return as is (Object filled with
|
|
304
|
-
# None)
|
|
305
|
-
new_arrays.append(col)
|
|
306
|
-
continue
|
|
307
|
-
|
|
308
|
-
# Take a sample (first valid)
|
|
309
|
-
first_val = col[valid_mask][0]
|
|
310
|
-
|
|
311
|
-
# INTEGER
|
|
312
|
-
if isinstance(
|
|
313
|
-
first_val,
|
|
314
|
-
(int, np.integer)) and not isinstance(first_val,
|
|
315
|
-
(bool, np.bool_)):
|
|
316
|
-
# Verify all valid are integers
|
|
317
|
-
valid_vals = col[valid_mask]
|
|
318
|
-
is_valid = all(
|
|
319
|
-
isinstance(x, (int, np.integer))
|
|
320
|
-
and not isinstance(x, (bool, np.bool_))
|
|
321
|
-
for x in valid_vals)
|
|
322
|
-
if is_valid:
|
|
323
|
-
# Convert
|
|
324
|
-
temp_col = np.copy(col)
|
|
325
|
-
temp_col[~valid_mask] = intNullVal
|
|
326
|
-
try:
|
|
327
|
-
new_col = temp_col.astype(int)
|
|
328
|
-
new_arrays.append(new_col)
|
|
329
|
-
continue
|
|
330
|
-
except Exception:
|
|
331
|
-
pass
|
|
332
|
-
|
|
333
|
-
# FLOAT
|
|
334
|
-
if isinstance(first_val, (float, np.floating)):
|
|
335
|
-
if all(
|
|
336
|
-
isinstance(x, (float, np.floating, int, np.integer))
|
|
337
|
-
for x in col[valid_mask]):
|
|
338
|
-
temp_col = np.copy(col)
|
|
339
|
-
temp_col[~valid_mask] = np.nan
|
|
340
|
-
try:
|
|
341
|
-
new_col = temp_col.astype(float)
|
|
342
|
-
new_arrays.append(new_col)
|
|
343
|
-
continue
|
|
344
|
-
except Exception:
|
|
345
|
-
pass
|
|
346
|
-
|
|
347
|
-
# STRING
|
|
348
|
-
if isinstance(first_val, str):
|
|
349
|
-
if all(isinstance(x, str) for x in col[valid_mask]):
|
|
350
|
-
# Replicate PG behavior: None -> strNullVal
|
|
351
|
-
temp_col = np.copy(col)
|
|
352
|
-
temp_col[~valid_mask] = strNullVal
|
|
353
|
-
# Let numpy infer string length
|
|
354
|
-
new_col = np.array(list(temp_col))
|
|
355
|
-
new_arrays.append(new_col)
|
|
356
|
-
continue
|
|
357
|
-
|
|
358
|
-
new_arrays.append(col)
|
|
383
|
+
new_arrays.append(__fix_object_column(col, intNullVal, strNullVal))
|
|
384
|
+
else:
|
|
385
|
+
new_arrays.append(col)
|
|
359
386
|
|
|
360
387
|
return numpy.rec.fromarrays(new_arrays, names=names)
|
|
361
388
|
|
|
362
389
|
|
|
390
|
+
def __process_duckdb_column(col, intNullVal, strNullVal):
|
|
391
|
+
"""
|
|
392
|
+
Convert a single column returned by duckdb's fetchnumpy() into
|
|
393
|
+
the sqlutilpy convention, i.e. nulls in integer/string columns
|
|
394
|
+
replaced by intNullVal/strNullVal, by NaNs in float columns and
|
|
395
|
+
by NaTs in datetime columns; strings converted to fixed-width arrays
|
|
396
|
+
"""
|
|
397
|
+
if isinstance(col, np.ma.MaskedArray):
|
|
398
|
+
kind = col.dtype.kind
|
|
399
|
+
if kind in ('i', 'u') and intNullVal is not None:
|
|
400
|
+
try:
|
|
401
|
+
col = col.filled(intNullVal)
|
|
402
|
+
except (TypeError, OverflowError):
|
|
403
|
+
# intNullVal does not fit into the column type (e.g. a
|
|
404
|
+
# negative value for an unsigned or a tinyint column),
|
|
405
|
+
# so the column is widened to int64 first
|
|
406
|
+
col = col.astype(np.int64).filled(intNullVal)
|
|
407
|
+
elif kind == 'f':
|
|
408
|
+
col = col.filled(np.nan)
|
|
409
|
+
elif kind == 'M':
|
|
410
|
+
col = col.filled(np.array('NaT', dtype=col.dtype))
|
|
411
|
+
else:
|
|
412
|
+
# object columns (i.e. strings), booleans and integers
|
|
413
|
+
# without a null replacement value become object columns
|
|
414
|
+
# with Nones
|
|
415
|
+
# (note that col.filled(None) cannot be used, as None
|
|
416
|
+
# there means the default fill value)
|
|
417
|
+
new_col = col.data.astype(object)
|
|
418
|
+
new_col[np.ma.getmaskarray(col)] = None
|
|
419
|
+
col = new_col
|
|
420
|
+
if col.dtype.kind == 'O':
|
|
421
|
+
col = __fix_object_column(col, intNullVal, strNullVal)
|
|
422
|
+
return col
|
|
423
|
+
|
|
424
|
+
|
|
363
425
|
def get(query,
|
|
364
426
|
params=None,
|
|
365
427
|
db="wsdb",
|
|
@@ -394,8 +456,9 @@ def get(query,
|
|
|
394
456
|
Flag whether to retrieve the results as a dictionary with column
|
|
395
457
|
names as keys
|
|
396
458
|
strLength : integer
|
|
397
|
-
The
|
|
398
|
-
|
|
459
|
+
The minimum length allocated for string columns. If longer
|
|
460
|
+
strings are encountered, the allocated length is extended
|
|
461
|
+
to accommodate them
|
|
399
462
|
intNullVal : integer, optional
|
|
400
463
|
All the integer columns with nulls will have null replaced by
|
|
401
464
|
this value
|
|
@@ -443,6 +506,7 @@ def get(query,
|
|
|
443
506
|
>>> a, b = sqlutilpy.get('select ra,dec from rc3 where name=?', "NGC 3166")
|
|
444
507
|
'''
|
|
445
508
|
|
|
509
|
+
driver = __check_driver(driver)
|
|
446
510
|
connSupplied = (conn is not None)
|
|
447
511
|
if not connSupplied:
|
|
448
512
|
conn = getConnection(db=db,
|
|
@@ -453,10 +517,6 @@ def get(query,
|
|
|
453
517
|
port=port,
|
|
454
518
|
timeout=timeout)
|
|
455
519
|
try:
|
|
456
|
-
if driver == 'psycopg2':
|
|
457
|
-
warnings.warn('psycopg2 driver is not supported anymore. '
|
|
458
|
-
'We using psycopg instead')
|
|
459
|
-
driver = 'psycopg'
|
|
460
520
|
if batched:
|
|
461
521
|
# This is related to postgresql server cursors vs
|
|
462
522
|
# application cursors
|
|
@@ -540,20 +600,35 @@ def get(query,
|
|
|
540
600
|
|
|
541
601
|
elif driver == 'sqlite3':
|
|
542
602
|
tups = cur.fetchall()
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
res =
|
|
603
|
+
if cur.description is None:
|
|
604
|
+
# no result set (e.g. create/insert statement)
|
|
605
|
+
colNames = []
|
|
606
|
+
res = []
|
|
547
607
|
else:
|
|
548
|
-
|
|
608
|
+
colNames = [_tmp[0] for _tmp in cur.description]
|
|
609
|
+
if len(tups) > 0:
|
|
610
|
+
res = numpy.rec.array(tups)
|
|
611
|
+
res = __fix_numpy_record_array(res, intNullVal, strNullVal)
|
|
612
|
+
else:
|
|
613
|
+
res = [numpy.array([]) for _ in cur.description]
|
|
549
614
|
elif driver == 'duckdb':
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
res =
|
|
554
|
-
res = __fix_numpy_record_array(res, intNullVal, strNullVal)
|
|
615
|
+
if cur.description is None:
|
|
616
|
+
# no result set
|
|
617
|
+
colNames = []
|
|
618
|
+
res = []
|
|
555
619
|
else:
|
|
556
|
-
|
|
620
|
+
# fetchnumpy() is much faster than fetchall() followed
|
|
621
|
+
# by a conversion to numpy.
|
|
622
|
+
# It returns a dict of numpy (possibly masked) arrays;
|
|
623
|
+
# in the case of repeated column names the dict keys are
|
|
624
|
+
# deduplicated, so we take the column names from the
|
|
625
|
+
# description instead
|
|
626
|
+
col_dict = cur.fetchnumpy()
|
|
627
|
+
colNames = [_tmp[0] for _tmp in cur.description]
|
|
628
|
+
res = [
|
|
629
|
+
__process_duckdb_column(_tmp, intNullVal, strNullVal)
|
|
630
|
+
for _tmp in col_dict.values()
|
|
631
|
+
]
|
|
557
632
|
|
|
558
633
|
if isinstance(res, numpy.ndarray) and res.dtype.names:
|
|
559
634
|
res = [res[tmp] for tmp in res.dtype.names]
|
|
@@ -595,6 +670,7 @@ def execute(query,
|
|
|
595
670
|
user=None,
|
|
596
671
|
password=None,
|
|
597
672
|
host=None,
|
|
673
|
+
port=None,
|
|
598
674
|
conn=None,
|
|
599
675
|
preamb=None,
|
|
600
676
|
timeout=None,
|
|
@@ -630,6 +706,7 @@ def execute(query,
|
|
|
630
706
|
>>> sqlutil.execute('create table mytab (a int)', db='mydb')
|
|
631
707
|
|
|
632
708
|
"""
|
|
709
|
+
driver = __check_driver(driver)
|
|
633
710
|
connSupplied = (conn is not None)
|
|
634
711
|
if not connSupplied:
|
|
635
712
|
conn = getConnection(db=db,
|
|
@@ -637,6 +714,7 @@ def execute(query,
|
|
|
637
714
|
user=user,
|
|
638
715
|
password=password,
|
|
639
716
|
host=host,
|
|
717
|
+
port=port,
|
|
640
718
|
timeout=timeout)
|
|
641
719
|
try:
|
|
642
720
|
cur = getCursor(conn, driver=driver, preamb=preamb, named=False)
|
|
@@ -679,6 +757,23 @@ def __create_schema(tableName, arrays, names, temp=False):
|
|
|
679
757
|
return outp + '(' + ','.join(outp1) + ')'
|
|
680
758
|
|
|
681
759
|
|
|
760
|
+
def __escape_strings_for_copy(arr, delimiter):
|
|
761
|
+
"""
|
|
762
|
+
Escape the characters that are special in the text format of
|
|
763
|
+
PostgreSQL's COPY FROM (backslash, newline, carriage return, tab
|
|
764
|
+
and the column delimiter) in a numpy unicode string array
|
|
765
|
+
"""
|
|
766
|
+
specials = ['\\', '\n', '\r', '\t']
|
|
767
|
+
# the backslash must be escaped first
|
|
768
|
+
if delimiter not in specials:
|
|
769
|
+
specials.append(delimiter)
|
|
770
|
+
replacements = {'\n': '\\n', '\r': '\\r', '\t': '\\t'}
|
|
771
|
+
for ch in specials:
|
|
772
|
+
rep = replacements.get(ch, '\\' + ch)
|
|
773
|
+
arr = np.char.replace(arr, ch, rep)
|
|
774
|
+
return arr
|
|
775
|
+
|
|
776
|
+
|
|
682
777
|
def __print_arrays(arrays, f, delimiter=' '):
|
|
683
778
|
"""
|
|
684
779
|
print the input arrays into the open file separated by a delimiter
|
|
@@ -703,6 +798,16 @@ def __print_arrays(arrays, f, delimiter=' '):
|
|
|
703
798
|
raise RuntimeError(
|
|
704
799
|
'Unsupported column type %s. Please report' %
|
|
705
800
|
(x.dtype.type))
|
|
801
|
+
# bytes arrays are decoded (as utf-8) into unicode arrays, since
|
|
802
|
+
# otherwise they would be written as the b'...' python literals
|
|
803
|
+
arrays = [
|
|
804
|
+
np.char.decode(x, 'utf-8') if x.dtype.kind == 'S' else x
|
|
805
|
+
for x in arrays
|
|
806
|
+
]
|
|
807
|
+
arrays = [
|
|
808
|
+
__escape_strings_for_copy(x, delimiter) if x.dtype.kind == 'U' else x
|
|
809
|
+
for x in arrays
|
|
810
|
+
]
|
|
706
811
|
recarr = np.rec.fromarrays(arrays)
|
|
707
812
|
if not array_mode:
|
|
708
813
|
np.savetxt(f, recarr, fmt=fmts, delimiter=delimiter)
|
|
@@ -748,6 +853,7 @@ def upload(tableName,
|
|
|
748
853
|
user=None,
|
|
749
854
|
password=None,
|
|
750
855
|
host=None,
|
|
856
|
+
port=None,
|
|
751
857
|
conn=None,
|
|
752
858
|
preamb=None,
|
|
753
859
|
timeout=None,
|
|
@@ -775,6 +881,8 @@ def upload(tableName,
|
|
|
775
881
|
user: string,
|
|
776
882
|
password: string
|
|
777
883
|
host: string
|
|
884
|
+
port: integer
|
|
885
|
+
Port of the database
|
|
778
886
|
conn: object
|
|
779
887
|
SQL connection
|
|
780
888
|
preamb: string
|
|
@@ -790,7 +898,7 @@ def upload(tableName,
|
|
|
790
898
|
if True the table will be created before uploading (default)
|
|
791
899
|
delimiter: string
|
|
792
900
|
the string used for delimiting the input data when ingesting into
|
|
793
|
-
the db (default is
|
|
901
|
+
the db (default is ``|``)
|
|
794
902
|
|
|
795
903
|
Examples
|
|
796
904
|
--------
|
|
@@ -801,6 +909,7 @@ def upload(tableName,
|
|
|
801
909
|
>>> T = astropy.Table({'x':[1, 2, 3], 'y':['a', 'b', 'c'])
|
|
802
910
|
>>> sqlutilpy.upload('mytable', T)
|
|
803
911
|
"""
|
|
912
|
+
driver = __check_driver(driver)
|
|
804
913
|
connSupplied = (conn is not None)
|
|
805
914
|
if not connSupplied:
|
|
806
915
|
conn = getConnection(db=db,
|
|
@@ -808,6 +917,7 @@ def upload(tableName,
|
|
|
808
917
|
user=user,
|
|
809
918
|
password=password,
|
|
810
919
|
host=host,
|
|
920
|
+
port=port,
|
|
811
921
|
timeout=timeout)
|
|
812
922
|
if names is None:
|
|
813
923
|
for i in range(1):
|
|
@@ -872,11 +982,11 @@ table/pandas/dictionary or provide a separate list of arrays and their names')
|
|
|
872
982
|
|
|
873
983
|
finally:
|
|
874
984
|
pass
|
|
985
|
+
if analyze:
|
|
986
|
+
cur.execute('analyze %s' % tableName)
|
|
875
987
|
except BaseException:
|
|
876
988
|
failure_cleanup(conn, connSupplied)
|
|
877
989
|
raise
|
|
878
|
-
if analyze:
|
|
879
|
-
cur.execute('analyze %s' % tableName)
|
|
880
990
|
cur.close()
|
|
881
991
|
if not noCommit:
|
|
882
992
|
conn.commit()
|
|
@@ -926,6 +1036,7 @@ def local_join(query,
|
|
|
926
1036
|
... 'mytable', (x, y), ('x', 'y'))
|
|
927
1037
|
"""
|
|
928
1038
|
|
|
1039
|
+
driver = __check_driver(driver)
|
|
929
1040
|
connSupplied = (conn is not None)
|
|
930
1041
|
if not connSupplied:
|
|
931
1042
|
conn = getConnection(db=db,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.28.0'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlutilpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.0
|
|
4
4
|
Summary: Database query code returning numpy arrays
|
|
5
5
|
Author-email: "Sergey E. Koposov" <skoposov@ed.ac.uk>
|
|
6
6
|
License: BSD
|
|
@@ -14,8 +14,6 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
License-File: LICENSE.txt
|
|
15
15
|
Requires-Dist: numpy
|
|
16
16
|
Requires-Dist: psycopg[binary]
|
|
17
|
-
Requires-Dist: numpydoc
|
|
18
|
-
Requires-Dist: numpy
|
|
19
17
|
Dynamic: license-file
|
|
20
18
|
|
|
21
19
|
[](https://github.com/segasai/sqlutilpy/actions)
|
|
@@ -8,6 +8,7 @@ PG_DB = os.environ.get('SQLUTIL_TEST_PG_DB')
|
|
|
8
8
|
PG_HOST = os.environ.get('SQLUTIL_TEST_PG_HOST')
|
|
9
9
|
PG_USER = os.environ.get('SQLUTIL_TEST_PG_USER')
|
|
10
10
|
PG_PASS = os.environ.get('SQLUTIL_TEST_PG_PASS', "")
|
|
11
|
+
PG_PORT = int(os.environ.get('SQLUTIL_TEST_PG_PORT') or 5432)
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@pytest.fixture(scope="module")
|
|
@@ -18,6 +19,7 @@ def pg_conn():
|
|
|
18
19
|
host=PG_HOST,
|
|
19
20
|
user=PG_USER,
|
|
20
21
|
db=PG_DB,
|
|
22
|
+
port=PG_PORT,
|
|
21
23
|
password=PG_PASS,
|
|
22
24
|
driver='psycopg')
|
|
23
25
|
yield conn
|