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.
Files changed (23) hide show
  1. {sqlutilpy-0.27.0/src/sqlutilpy.egg-info → sqlutilpy-0.29.0}/PKG-INFO +1 -3
  2. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/pyproject.toml +1 -3
  3. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/src/sqlutilpy/sqlutil.py +211 -98
  4. sqlutilpy-0.29.0/src/sqlutilpy/version.py +1 -0
  5. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0/src/sqlutilpy.egg-info}/PKG-INFO +1 -3
  6. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/src/sqlutilpy.egg-info/requires.txt +0 -2
  7. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/tests/test_consistency.py +2 -0
  8. sqlutilpy-0.29.0/tests/test_duckdb.py +175 -0
  9. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/tests/test_pg.py +189 -56
  10. sqlutilpy-0.29.0/tests/test_sqlite.py +73 -0
  11. sqlutilpy-0.27.0/src/sqlutilpy/version.py +0 -1
  12. sqlutilpy-0.27.0/tests/test_duckdb.py +0 -32
  13. sqlutilpy-0.27.0/tests/test_sqlite.py +0 -32
  14. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/LICENSE.txt +0 -0
  15. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/MANIFEST.in +0 -0
  16. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/README.md +0 -0
  17. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/setup.cfg +0 -0
  18. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/src/sqlutilpy/__init__.py +0 -0
  19. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/src/sqlutilpy.egg-info/SOURCES.txt +0 -0
  20. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/src/sqlutilpy.egg-info/dependency_links.txt +0 -0
  21. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/src/sqlutilpy.egg-info/top_level.txt +0 -0
  22. {sqlutilpy-0.27.0 → sqlutilpy-0.29.0}/tests/killer.py +0 -0
  23. {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.27.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
  [![Build Status](https://github.com/segasai/sqlutilpy/workflows/Testing/badge.svg)](https://github.com/segasai/sqlutilpy/actions)
@@ -7,9 +7,7 @@ authors = [
7
7
  ]
8
8
  dependencies = [
9
9
  "numpy",
10
- "psycopg[binary]",
11
- "numpydoc",
12
- "numpy\n"
10
+ "psycopg[binary]"
13
11
  ]
14
12
  requires-python = ">=3.8"
15
13
  readme = "README.md"
@@ -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
- if driver == 'psycopg2':
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
- if driver == 'psycopg2':
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
- cur = conn.cursor(name='sqlutilcursor')
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
- # Find first non-None value to guess type
300
- valid_mask = (col != None)
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 maximum length of the string.
398
- Strings will be truncated to this length
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
- colNames = [_tmp[0] for _tmp in cur.description]
544
- if len(tups) > 0:
545
- res = numpy.rec.array(tups)
546
- res = __fix_numpy_record_array(res, intNullVal, strNullVal)
605
+ if cur.description is None:
606
+ # no result set (e.g. create/insert statement)
607
+ colNames = []
608
+ res = []
547
609
  else:
548
- res = [numpy.array([]) for _ in cur.description]
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
- tups = cur.fetchall()
551
- colNames = [_tmp[0] for _tmp in cur.description]
552
- if len(tups) > 0:
553
- res = numpy.rec.array(tups)
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
- res = [numpy.array([]) for _ in cur.description]
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.27.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
  [![Build Status](https://github.com/segasai/sqlutilpy/workflows/Testing/badge.svg)](https://github.com/segasai/sqlutilpy/actions)
@@ -1,4 +1,2 @@
1
1
  numpy
2
2
  psycopg[binary]
3
- numpydoc
4
- numpy
@@ -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