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.
Files changed (23) hide show
  1. {sqlutilpy-0.27.0/src/sqlutilpy.egg-info → sqlutilpy-0.28.0}/PKG-INFO +1 -3
  2. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/pyproject.toml +1 -3
  3. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/src/sqlutilpy/sqlutil.py +209 -98
  4. sqlutilpy-0.28.0/src/sqlutilpy/version.py +1 -0
  5. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0/src/sqlutilpy.egg-info}/PKG-INFO +1 -3
  6. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/src/sqlutilpy.egg-info/requires.txt +0 -2
  7. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/tests/test_consistency.py +2 -0
  8. sqlutilpy-0.28.0/tests/test_duckdb.py +175 -0
  9. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/tests/test_pg.py +189 -56
  10. sqlutilpy-0.28.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.28.0}/LICENSE.txt +0 -0
  15. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/MANIFEST.in +0 -0
  16. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/README.md +0 -0
  17. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/setup.cfg +0 -0
  18. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/src/sqlutilpy/__init__.py +0 -0
  19. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/src/sqlutilpy.egg-info/SOURCES.txt +0 -0
  20. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/src/sqlutilpy.egg-info/dependency_links.txt +0 -0
  21. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/src/sqlutilpy.egg-info/top_level.txt +0 -0
  22. {sqlutilpy-0.27.0 → sqlutilpy-0.28.0}/tests/killer.py +0 -0
  23. {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.27.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
  [![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,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
- # 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
+ 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 maximum length of the string.
398
- Strings will be truncated to this length
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
- 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)
603
+ if cur.description is None:
604
+ # no result set (e.g. create/insert statement)
605
+ colNames = []
606
+ res = []
547
607
  else:
548
- res = [numpy.array([]) for _ in cur.description]
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
- 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)
615
+ if cur.description is None:
616
+ # no result set
617
+ colNames = []
618
+ res = []
555
619
  else:
556
- res = [numpy.array([]) for _ in cur.description]
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.27.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
  [![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