sqlutilpy 0.27.0__tar.gz → 0.29.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.29.0}/PKG-INFO +1 -3
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/pyproject.toml +1 -3
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/src/sqlutilpy/sqlutil.py +211 -98
- sqlutilpy-0.29.0/src/sqlutilpy/version.py +1 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0/src/sqlutilpy.egg-info}/PKG-INFO +1 -3
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/src/sqlutilpy.egg-info/requires.txt +0 -2
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/tests/test_consistency.py +2 -0
- sqlutilpy-0.29.0/tests/test_duckdb.py +175 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/tests/test_pg.py +189 -56
- sqlutilpy-0.29.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.29.0}/LICENSE.txt +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/MANIFEST.in +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/README.md +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/setup.cfg +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/src/sqlutilpy/__init__.py +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/src/sqlutilpy.egg-info/SOURCES.txt +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/src/sqlutilpy.egg-info/dependency_links.txt +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/src/sqlutilpy.egg-info/top_level.txt +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/tests/killer.py +0 -0
- {sqlutilpy-0.27.0 → sqlutilpy-0.29.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.29.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,53 @@ 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
|
+
info = np.iinfo(col.dtype)
|
|
401
|
+
if not (info.min <= intNullVal <= info.max):
|
|
402
|
+
# intNullVal does not fit into the column type (e.g. a
|
|
403
|
+
# negative value for an unsigned or a tinyint column),
|
|
404
|
+
# so the column is widened to int64 first.
|
|
405
|
+
# (we check explicitly rather than catch an exception,
|
|
406
|
+
# because numpy<2 silently wraps the value instead)
|
|
407
|
+
col = col.astype(np.int64)
|
|
408
|
+
col = col.filled(intNullVal)
|
|
409
|
+
elif kind == 'f':
|
|
410
|
+
col = col.filled(np.nan)
|
|
411
|
+
elif kind == 'M':
|
|
412
|
+
col = col.filled(np.array('NaT', dtype=col.dtype))
|
|
413
|
+
else:
|
|
414
|
+
# object columns (i.e. strings), booleans and integers
|
|
415
|
+
# without a null replacement value become object columns
|
|
416
|
+
# with Nones
|
|
417
|
+
# (note that col.filled(None) cannot be used, as None
|
|
418
|
+
# there means the default fill value)
|
|
419
|
+
new_col = col.data.astype(object)
|
|
420
|
+
new_col[np.ma.getmaskarray(col)] = None
|
|
421
|
+
col = new_col
|
|
422
|
+
if col.dtype.kind == 'O':
|
|
423
|
+
col = __fix_object_column(col, intNullVal, strNullVal)
|
|
424
|
+
return col
|
|
425
|
+
|
|
426
|
+
|
|
363
427
|
def get(query,
|
|
364
428
|
params=None,
|
|
365
429
|
db="wsdb",
|
|
@@ -394,8 +458,9 @@ def get(query,
|
|
|
394
458
|
Flag whether to retrieve the results as a dictionary with column
|
|
395
459
|
names as keys
|
|
396
460
|
strLength : integer
|
|
397
|
-
The
|
|
398
|
-
|
|
461
|
+
The minimum length allocated for string columns. If longer
|
|
462
|
+
strings are encountered, the allocated length is extended
|
|
463
|
+
to accommodate them
|
|
399
464
|
intNullVal : integer, optional
|
|
400
465
|
All the integer columns with nulls will have null replaced by
|
|
401
466
|
this value
|
|
@@ -443,6 +508,7 @@ def get(query,
|
|
|
443
508
|
>>> a, b = sqlutilpy.get('select ra,dec from rc3 where name=?', "NGC 3166")
|
|
444
509
|
'''
|
|
445
510
|
|
|
511
|
+
driver = __check_driver(driver)
|
|
446
512
|
connSupplied = (conn is not None)
|
|
447
513
|
if not connSupplied:
|
|
448
514
|
conn = getConnection(db=db,
|
|
@@ -453,10 +519,6 @@ def get(query,
|
|
|
453
519
|
port=port,
|
|
454
520
|
timeout=timeout)
|
|
455
521
|
try:
|
|
456
|
-
if driver == 'psycopg2':
|
|
457
|
-
warnings.warn('psycopg2 driver is not supported anymore. '
|
|
458
|
-
'We using psycopg instead')
|
|
459
|
-
driver = 'psycopg'
|
|
460
522
|
if batched:
|
|
461
523
|
# This is related to postgresql server cursors vs
|
|
462
524
|
# application cursors
|
|
@@ -540,20 +602,35 @@ def get(query,
|
|
|
540
602
|
|
|
541
603
|
elif driver == 'sqlite3':
|
|
542
604
|
tups = cur.fetchall()
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
res =
|
|
605
|
+
if cur.description is None:
|
|
606
|
+
# no result set (e.g. create/insert statement)
|
|
607
|
+
colNames = []
|
|
608
|
+
res = []
|
|
547
609
|
else:
|
|
548
|
-
|
|
610
|
+
colNames = [_tmp[0] for _tmp in cur.description]
|
|
611
|
+
if len(tups) > 0:
|
|
612
|
+
res = numpy.rec.array(tups)
|
|
613
|
+
res = __fix_numpy_record_array(res, intNullVal, strNullVal)
|
|
614
|
+
else:
|
|
615
|
+
res = [numpy.array([]) for _ in cur.description]
|
|
549
616
|
elif driver == 'duckdb':
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
res =
|
|
554
|
-
res = __fix_numpy_record_array(res, intNullVal, strNullVal)
|
|
617
|
+
if cur.description is None:
|
|
618
|
+
# no result set
|
|
619
|
+
colNames = []
|
|
620
|
+
res = []
|
|
555
621
|
else:
|
|
556
|
-
|
|
622
|
+
# fetchnumpy() is much faster than fetchall() followed
|
|
623
|
+
# by a conversion to numpy.
|
|
624
|
+
# It returns a dict of numpy (possibly masked) arrays;
|
|
625
|
+
# in the case of repeated column names the dict keys are
|
|
626
|
+
# deduplicated, so we take the column names from the
|
|
627
|
+
# description instead
|
|
628
|
+
col_dict = cur.fetchnumpy()
|
|
629
|
+
colNames = [_tmp[0] for _tmp in cur.description]
|
|
630
|
+
res = [
|
|
631
|
+
__process_duckdb_column(_tmp, intNullVal, strNullVal)
|
|
632
|
+
for _tmp in col_dict.values()
|
|
633
|
+
]
|
|
557
634
|
|
|
558
635
|
if isinstance(res, numpy.ndarray) and res.dtype.names:
|
|
559
636
|
res = [res[tmp] for tmp in res.dtype.names]
|
|
@@ -595,6 +672,7 @@ def execute(query,
|
|
|
595
672
|
user=None,
|
|
596
673
|
password=None,
|
|
597
674
|
host=None,
|
|
675
|
+
port=None,
|
|
598
676
|
conn=None,
|
|
599
677
|
preamb=None,
|
|
600
678
|
timeout=None,
|
|
@@ -630,6 +708,7 @@ def execute(query,
|
|
|
630
708
|
>>> sqlutil.execute('create table mytab (a int)', db='mydb')
|
|
631
709
|
|
|
632
710
|
"""
|
|
711
|
+
driver = __check_driver(driver)
|
|
633
712
|
connSupplied = (conn is not None)
|
|
634
713
|
if not connSupplied:
|
|
635
714
|
conn = getConnection(db=db,
|
|
@@ -637,6 +716,7 @@ def execute(query,
|
|
|
637
716
|
user=user,
|
|
638
717
|
password=password,
|
|
639
718
|
host=host,
|
|
719
|
+
port=port,
|
|
640
720
|
timeout=timeout)
|
|
641
721
|
try:
|
|
642
722
|
cur = getCursor(conn, driver=driver, preamb=preamb, named=False)
|
|
@@ -679,6 +759,23 @@ def __create_schema(tableName, arrays, names, temp=False):
|
|
|
679
759
|
return outp + '(' + ','.join(outp1) + ')'
|
|
680
760
|
|
|
681
761
|
|
|
762
|
+
def __escape_strings_for_copy(arr, delimiter):
|
|
763
|
+
"""
|
|
764
|
+
Escape the characters that are special in the text format of
|
|
765
|
+
PostgreSQL's COPY FROM (backslash, newline, carriage return, tab
|
|
766
|
+
and the column delimiter) in a numpy unicode string array
|
|
767
|
+
"""
|
|
768
|
+
specials = ['\\', '\n', '\r', '\t']
|
|
769
|
+
# the backslash must be escaped first
|
|
770
|
+
if delimiter not in specials:
|
|
771
|
+
specials.append(delimiter)
|
|
772
|
+
replacements = {'\n': '\\n', '\r': '\\r', '\t': '\\t'}
|
|
773
|
+
for ch in specials:
|
|
774
|
+
rep = replacements.get(ch, '\\' + ch)
|
|
775
|
+
arr = np.char.replace(arr, ch, rep)
|
|
776
|
+
return arr
|
|
777
|
+
|
|
778
|
+
|
|
682
779
|
def __print_arrays(arrays, f, delimiter=' '):
|
|
683
780
|
"""
|
|
684
781
|
print the input arrays into the open file separated by a delimiter
|
|
@@ -703,6 +800,16 @@ def __print_arrays(arrays, f, delimiter=' '):
|
|
|
703
800
|
raise RuntimeError(
|
|
704
801
|
'Unsupported column type %s. Please report' %
|
|
705
802
|
(x.dtype.type))
|
|
803
|
+
# bytes arrays are decoded (as utf-8) into unicode arrays, since
|
|
804
|
+
# otherwise they would be written as the b'...' python literals
|
|
805
|
+
arrays = [
|
|
806
|
+
np.char.decode(x, 'utf-8') if x.dtype.kind == 'S' else x
|
|
807
|
+
for x in arrays
|
|
808
|
+
]
|
|
809
|
+
arrays = [
|
|
810
|
+
__escape_strings_for_copy(x, delimiter) if x.dtype.kind == 'U' else x
|
|
811
|
+
for x in arrays
|
|
812
|
+
]
|
|
706
813
|
recarr = np.rec.fromarrays(arrays)
|
|
707
814
|
if not array_mode:
|
|
708
815
|
np.savetxt(f, recarr, fmt=fmts, delimiter=delimiter)
|
|
@@ -748,6 +855,7 @@ def upload(tableName,
|
|
|
748
855
|
user=None,
|
|
749
856
|
password=None,
|
|
750
857
|
host=None,
|
|
858
|
+
port=None,
|
|
751
859
|
conn=None,
|
|
752
860
|
preamb=None,
|
|
753
861
|
timeout=None,
|
|
@@ -775,6 +883,8 @@ def upload(tableName,
|
|
|
775
883
|
user: string,
|
|
776
884
|
password: string
|
|
777
885
|
host: string
|
|
886
|
+
port: integer
|
|
887
|
+
Port of the database
|
|
778
888
|
conn: object
|
|
779
889
|
SQL connection
|
|
780
890
|
preamb: string
|
|
@@ -790,7 +900,7 @@ def upload(tableName,
|
|
|
790
900
|
if True the table will be created before uploading (default)
|
|
791
901
|
delimiter: string
|
|
792
902
|
the string used for delimiting the input data when ingesting into
|
|
793
|
-
the db (default is
|
|
903
|
+
the db (default is ``|``)
|
|
794
904
|
|
|
795
905
|
Examples
|
|
796
906
|
--------
|
|
@@ -801,6 +911,7 @@ def upload(tableName,
|
|
|
801
911
|
>>> T = astropy.Table({'x':[1, 2, 3], 'y':['a', 'b', 'c'])
|
|
802
912
|
>>> sqlutilpy.upload('mytable', T)
|
|
803
913
|
"""
|
|
914
|
+
driver = __check_driver(driver)
|
|
804
915
|
connSupplied = (conn is not None)
|
|
805
916
|
if not connSupplied:
|
|
806
917
|
conn = getConnection(db=db,
|
|
@@ -808,6 +919,7 @@ def upload(tableName,
|
|
|
808
919
|
user=user,
|
|
809
920
|
password=password,
|
|
810
921
|
host=host,
|
|
922
|
+
port=port,
|
|
811
923
|
timeout=timeout)
|
|
812
924
|
if names is None:
|
|
813
925
|
for i in range(1):
|
|
@@ -872,11 +984,11 @@ table/pandas/dictionary or provide a separate list of arrays and their names')
|
|
|
872
984
|
|
|
873
985
|
finally:
|
|
874
986
|
pass
|
|
987
|
+
if analyze:
|
|
988
|
+
cur.execute('analyze %s' % tableName)
|
|
875
989
|
except BaseException:
|
|
876
990
|
failure_cleanup(conn, connSupplied)
|
|
877
991
|
raise
|
|
878
|
-
if analyze:
|
|
879
|
-
cur.execute('analyze %s' % tableName)
|
|
880
992
|
cur.close()
|
|
881
993
|
if not noCommit:
|
|
882
994
|
conn.commit()
|
|
@@ -926,6 +1038,7 @@ def local_join(query,
|
|
|
926
1038
|
... 'mytable', (x, y), ('x', 'y'))
|
|
927
1039
|
"""
|
|
928
1040
|
|
|
1041
|
+
driver = __check_driver(driver)
|
|
929
1042
|
connSupplied = (conn is not None)
|
|
930
1043
|
if not connSupplied:
|
|
931
1044
|
conn = getConnection(db=db,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.29.0'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlutilpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.29.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
|