pglib 5.7.1__tar.gz → 5.8.1__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 (52) hide show
  1. {pglib-5.7.1/pglib.egg-info → pglib-5.8.1}/PKG-INFO +1 -1
  2. {pglib-5.7.1 → pglib-5.8.1/pglib.egg-info}/PKG-INFO +1 -1
  3. {pglib-5.7.1 → pglib-5.8.1}/setup.py +1 -1
  4. {pglib-5.7.1 → pglib-5.8.1}/src/connection.cpp +144 -6
  5. {pglib-5.7.1 → pglib-5.8.1}/src/pglib.h +1 -1
  6. {pglib-5.7.1 → pglib-5.8.1}/test/test_sync.py +57 -11
  7. {pglib-5.7.1 → pglib-5.8.1}/LICENSE +0 -0
  8. {pglib-5.7.1 → pglib-5.8.1}/MANIFEST.in +0 -0
  9. {pglib-5.7.1 → pglib-5.8.1}/README.rst +0 -0
  10. {pglib-5.7.1 → pglib-5.8.1}/pglib/__init__.py +0 -0
  11. {pglib-5.7.1 → pglib-5.8.1}/pglib/_version.py +0 -0
  12. {pglib-5.7.1 → pglib-5.8.1}/pglib/asyncpglib.py +0 -0
  13. {pglib-5.7.1 → pglib-5.8.1}/pglib.egg-info/SOURCES.txt +0 -0
  14. {pglib-5.7.1 → pglib-5.8.1}/pglib.egg-info/dependency_links.txt +0 -0
  15. {pglib-5.7.1 → pglib-5.8.1}/pglib.egg-info/top_level.txt +0 -0
  16. {pglib-5.7.1 → pglib-5.8.1}/setup.cfg +0 -0
  17. {pglib-5.7.1 → pglib-5.8.1}/src/byteswap.h +0 -0
  18. {pglib-5.7.1 → pglib-5.8.1}/src/connection.h +0 -0
  19. {pglib-5.7.1 → pglib-5.8.1}/src/conninfoopt.cpp +0 -0
  20. {pglib-5.7.1 → pglib-5.8.1}/src/conninfoopt.h +0 -0
  21. {pglib-5.7.1 → pglib-5.8.1}/src/datatypes.cpp +0 -0
  22. {pglib-5.7.1 → pglib-5.8.1}/src/datatypes.h +0 -0
  23. {pglib-5.7.1 → pglib-5.8.1}/src/debug.cpp +0 -0
  24. {pglib-5.7.1 → pglib-5.8.1}/src/debug.h +0 -0
  25. {pglib-5.7.1 → pglib-5.8.1}/src/enums.cpp +0 -0
  26. {pglib-5.7.1 → pglib-5.8.1}/src/enums.h +0 -0
  27. {pglib-5.7.1 → pglib-5.8.1}/src/errors.cpp +0 -0
  28. {pglib-5.7.1 → pglib-5.8.1}/src/errors.h +0 -0
  29. {pglib-5.7.1 → pglib-5.8.1}/src/getdata.cpp +0 -0
  30. {pglib-5.7.1 → pglib-5.8.1}/src/getdata.h +0 -0
  31. {pglib-5.7.1 → pglib-5.8.1}/src/juliandate.cpp +0 -0
  32. {pglib-5.7.1 → pglib-5.8.1}/src/juliandate.h +0 -0
  33. {pglib-5.7.1 → pglib-5.8.1}/src/params.cpp +0 -0
  34. {pglib-5.7.1 → pglib-5.8.1}/src/params.h +0 -0
  35. {pglib-5.7.1 → pglib-5.8.1}/src/pgarrays.cpp +0 -0
  36. {pglib-5.7.1 → pglib-5.8.1}/src/pgarrays.h +0 -0
  37. {pglib-5.7.1 → pglib-5.8.1}/src/pglib.cpp +0 -0
  38. {pglib-5.7.1 → pglib-5.8.1}/src/pgtypes.h +0 -0
  39. {pglib-5.7.1 → pglib-5.8.1}/src/resultset.cpp +0 -0
  40. {pglib-5.7.1 → pglib-5.8.1}/src/resultset.h +0 -0
  41. {pglib-5.7.1 → pglib-5.8.1}/src/row.cpp +0 -0
  42. {pglib-5.7.1 → pglib-5.8.1}/src/row.h +0 -0
  43. {pglib-5.7.1 → pglib-5.8.1}/src/runtime.cpp +0 -0
  44. {pglib-5.7.1 → pglib-5.8.1}/src/runtime.h +0 -0
  45. {pglib-5.7.1 → pglib-5.8.1}/src/type_hstore.cpp +0 -0
  46. {pglib-5.7.1 → pglib-5.8.1}/src/type_hstore.h +0 -0
  47. {pglib-5.7.1 → pglib-5.8.1}/src/type_json.cpp +0 -0
  48. {pglib-5.7.1 → pglib-5.8.1}/src/type_json.h +0 -0
  49. {pglib-5.7.1 → pglib-5.8.1}/src/type_ltree.cpp +0 -0
  50. {pglib-5.7.1 → pglib-5.8.1}/src/type_ltree.h +0 -0
  51. {pglib-5.7.1 → pglib-5.8.1}/test/test_async.py +0 -0
  52. {pglib-5.7.1 → pglib-5.8.1}/test/testutils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pglib
3
- Version: 5.7.1
3
+ Version: 5.8.1
4
4
  Summary: A PostgreSQL interface
5
5
  Home-page: https://gitlab.com/mkleehammer/pglib
6
6
  Maintainer: Michael Kleehammer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pglib
3
- Version: 5.7.1
3
+ Version: 5.8.1
4
4
  Summary: A PostgreSQL interface
5
5
  Home-page: https://gitlab.com/mkleehammer/pglib
6
6
  Maintainer: Michael Kleehammer
@@ -130,7 +130,7 @@ def _get_settings():
130
130
 
131
131
  setup(
132
132
  name='pglib',
133
- version='5.7.1',
133
+ version='5.8.1',
134
134
  description='A PostgreSQL interface',
135
135
  long_description=long_description,
136
136
  maintainer='Michael Kleehammer',
@@ -176,6 +176,7 @@ static PGresult* internal_execute(PyObject* self, PyObject* args)
176
176
  return result;
177
177
  }
178
178
 
179
+
179
180
  static const char doc_script[] = "Connection.script(sql) --> None\n\n"
180
181
  "Executes a script which can contain multiple statements separated by semicolons.";
181
182
 
@@ -204,6 +205,7 @@ static PyObject* Connection_script(PyObject* self, PyObject* args)
204
205
  }
205
206
  }
206
207
 
208
+
207
209
  const char* doc_copy_from =
208
210
  "Connection.copy_from(command, source) --> int\n"
209
211
  "\n"
@@ -234,7 +236,7 @@ static PyObject* Connection_copy_from(PyObject* self, PyObject* args)
234
236
  // an object with a read method (e.g. file).
235
237
  const char* buffer = 0;
236
238
  Py_ssize_t buffer_size = 0;
237
- PyObject* read_method = 0;
239
+ Object read_method;
238
240
 
239
241
  if (PyUnicode_Check(source))
240
242
  {
@@ -246,7 +248,7 @@ static PyObject* Connection_copy_from(PyObject* self, PyObject* args)
246
248
  {
247
249
  if (!PyObject_HasAttrString(source, "read"))
248
250
  return PyErr_Format(Error, "CSV source must be a string or file-like object.");
249
- read_method = PyObject_GetAttrString(source, "read");
251
+ read_method.Attach(PyObject_GetAttrString(source, "read"));
250
252
  }
251
253
 
252
254
  Connection* cnxn = CastConnection(self, REQUIRE_OPEN);
@@ -354,6 +356,140 @@ static PyObject* Connection_copy_from(PyObject* self, PyObject* args)
354
356
  }
355
357
 
356
358
 
359
+ const char* doc_copy_to_csv =
360
+ "Connection.copy_to_csv(table, dest, header=0, delimiter=',', quote='\"')\n"
361
+ "\n"
362
+ "Execute a COPY TO command and return the number of records copied.\n"
363
+ "\n"
364
+ "table\n"
365
+ " The table to copy from.\n"
366
+ "\n"
367
+ "dest\n"
368
+ " The file-like object to write to. Strings will be written, not bytes, so\n"
369
+ " open in text mode.\n"
370
+ "\n"
371
+ "header\n"
372
+ " If non-zero, a CSV header will be written.\n";
373
+
374
+
375
+ static PyObject* Connection_copy_to_csv(PyObject* self, PyObject* args, PyObject* kwargs)
376
+ {
377
+ // This is not nearly as efficient as I'd like since newer Python versions no longer give
378
+ // us access to underlying file objects. We have to write strings through a write method
379
+ // since there are io layers involved.
380
+ //
381
+ // For maximum performance, we should probably offer an option where we open the file given
382
+ // a filename. We can either check the parameter type here or we could make a separate
383
+ // method with "file" in the name like copy_to_file.
384
+
385
+ static const char* kwlist[] = {"table", "dest", "header", "delimiter", "quote", 0};
386
+
387
+ PyObject* table;
388
+ PyObject* dest;
389
+ int header = 0;
390
+ char* szDelimiter = 0;
391
+ char* szQuote = 0;
392
+
393
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "UO|pzz", (char**)kwlist, &table, &dest, &header,
394
+ &szDelimiter, &szQuote)) {
395
+ return 0;
396
+ }
397
+
398
+ Connection* cnxn = CastConnection(self, REQUIRE_OPEN);
399
+ if (!cnxn)
400
+ return 0;
401
+
402
+ if (!PyObject_HasAttrString(dest, "write"))
403
+ return PyErr_Format(Error, "CSV destination must be a file-like object.");
404
+ Object write_method(PyObject_GetAttrString(dest, "write"));
405
+
406
+ char header_token[] = "header";
407
+ if (header == 0) {
408
+ header_token[0] = 0;
409
+ }
410
+
411
+ const char* pszDelimiter = szDelimiter ? szDelimiter : ",";
412
+ const char* pszQuote = szQuote ? szQuote : "\"";
413
+
414
+ Object sql(PyUnicode_FromFormat("copy %U to stdout with csv %s delimiter '%s' quote '%s'",
415
+ table, header_token, pszDelimiter, pszQuote));
416
+
417
+ const char* szSQL = PyUnicode_AsUTF8(sql);
418
+ ResultHolder result;
419
+ Py_BEGIN_ALLOW_THREADS
420
+ result = PQexec(cnxn->pgconn, szSQL);
421
+ Py_END_ALLOW_THREADS
422
+
423
+ if (result == 0)
424
+ return 0;
425
+
426
+ switch (PQresultStatus(result)) {
427
+ case PGRES_COPY_OUT:
428
+ // This is what we are expecting.
429
+ break;
430
+
431
+ case PGRES_BAD_RESPONSE:
432
+ case PGRES_NONFATAL_ERROR:
433
+ case PGRES_FATAL_ERROR:
434
+ return SetResultError(result.Detach());
435
+
436
+ default:
437
+ return PyErr_Format(Error, "Result was not PGRES_COPY_IN: %d", (int)PQresultStatus(result));
438
+ }
439
+
440
+
441
+ for (;;) {
442
+ int cb = 0;
443
+ char* buffer;
444
+ Py_BEGIN_ALLOW_THREADS
445
+ cb = PQgetCopyData(cnxn->pgconn, &buffer, 0);
446
+ Py_END_ALLOW_THREADS
447
+
448
+ if (cb == -2) {
449
+ return SetResultError(result.Detach());
450
+ }
451
+
452
+ if (cb == -1) {
453
+ // The copy is complete.
454
+ break;
455
+ }
456
+
457
+ // We have a buffer of byte data. We have the length, but the libpq docs say that the
458
+ // string is also zero terminated, so we're going to try not calling 'write'.
459
+
460
+ int err = PyFile_WriteString(buffer, dest);
461
+
462
+ // while (cb > 0) {
463
+ // PyObject* res = PyObject_CallObject(write_method)
464
+ // }
465
+
466
+ PQfreemem(buffer);
467
+ if (err) {
468
+ return 0;
469
+ }
470
+ }
471
+
472
+ // After a copy, you have to get another result to know if it was successful.
473
+
474
+ ResultHolder final_result;
475
+ ExecStatusType status = PGRES_COMMAND_OK;
476
+ Py_BEGIN_ALLOW_THREADS
477
+ final_result = PQgetResult(cnxn->pgconn);
478
+ status = PQresultStatus(final_result);
479
+ Py_END_ALLOW_THREADS
480
+
481
+ if (status != PGRES_COMMAND_OK) {
482
+ // SetResultError will take ownership of `result`.
483
+ return SetResultError(final_result.Detach());
484
+ }
485
+
486
+ const char* sz = PQcmdTuples(final_result);
487
+ if (sz == 0 || *sz == 0)
488
+ Py_RETURN_NONE;
489
+ return PyLong_FromLong(atoi(sz));
490
+ }
491
+
492
+
357
493
  const char* doc_copy_from_csv =
358
494
  "Connection.copy_from_csv(table, source, header=0) --> int\n"
359
495
  "\n"
@@ -390,15 +526,16 @@ static PyObject* Connection_copy_from_csv(PyObject* self, PyObject* args, PyObje
390
526
 
391
527
  const char* pszDelimiter = szDelimiter ? szDelimiter : ",";
392
528
  const char* pszQuote = szQuote ? szQuote : "\"";
393
- PyObject* sql = PyUnicode_FromFormat("copy %U from stdin with csv %s delimiter '%s' quote '%s'",
394
- table, header_token, pszDelimiter, pszQuote);
529
+ Object sql(PyUnicode_FromFormat("copy %U from stdin with csv %s delimiter '%s' quote '%s'",
530
+ table, header_token, pszDelimiter, pszQuote));
395
531
 
396
532
  // If source is a string (Unicode), store the UTF-encoded value in buffer. If a byte
397
533
  // object, store directly in buffer. Otherwise, buffer will be zero and `source` must be
398
534
  // an object with a read method (e.g. file).
399
535
  const char* buffer = 0;
400
536
  Py_ssize_t buffer_size = 0;
401
- PyObject* read_method = 0;
537
+ Object read_method;
538
+ // PyObject* read_method = 0;
402
539
 
403
540
  if (PyUnicode_Check(source))
404
541
  {
@@ -410,7 +547,7 @@ static PyObject* Connection_copy_from_csv(PyObject* self, PyObject* args, PyObje
410
547
  {
411
548
  if (!PyObject_HasAttrString(source, "read"))
412
549
  return PyErr_Format(Error, "CSV source must be a string or file-like object.");
413
- read_method = PyObject_GetAttrString(source, "read");
550
+ read_method.Attach(PyObject_GetAttrString(source, "read"));
414
551
  }
415
552
 
416
553
  Connection* cnxn = CastConnection(self, REQUIRE_OPEN);
@@ -1462,6 +1599,7 @@ static struct PyMethodDef Connection_methods[] =
1462
1599
  { "script", Connection_script, METH_VARARGS, doc_script },
1463
1600
  { "copy_from", (PyCFunction) Connection_copy_from, METH_VARARGS | METH_KEYWORDS, doc_copy_from },
1464
1601
  { "copy_from_csv", (PyCFunction) Connection_copy_from_csv, METH_VARARGS | METH_KEYWORDS, doc_copy_from_csv },
1602
+ { "copy_to_csv", (PyCFunction) Connection_copy_to_csv, METH_VARARGS | METH_KEYWORDS, doc_copy_to_csv},
1465
1603
  { "begin", Connection_begin, METH_NOARGS, doc_begin },
1466
1604
  { "commit", Connection_commit, METH_NOARGS, doc_commit },
1467
1605
  { "rollback", Connection_rollback, METH_NOARGS, doc_rollback },
@@ -135,7 +135,7 @@ struct Object
135
135
  {
136
136
  PyObject* p;
137
137
 
138
- // Borrows the reference (takes ownership without adding a new referencing).
138
+ // Borrows the reference (takes ownership without adding a new reference).
139
139
  Object(PyObject* _p = 0) { p = _p; }
140
140
 
141
141
  // If it still has the pointer, it dereferences it.
@@ -3,11 +3,11 @@
3
3
 
4
4
  # pylint: disable=missing-function-docstring,redefined-outer-name,unidiomatic-typecheck
5
5
 
6
- import sys, threading, gzip, uuid, locale
6
+ import sys, threading, gzip, uuid, locale, tempfile, csv
7
7
  from time import sleep
8
8
  from os.path import join, dirname, exists
9
9
  from decimal import Decimal
10
- from datetime import date, time, datetime, timedelta, timezone
10
+ from datetime import date, time, datetime, timedelta
11
11
  from pathlib import Path
12
12
  import pytest
13
13
 
@@ -32,7 +32,7 @@ def cnxn():
32
32
  cnxn = pglib.connect(CONNINFO)
33
33
  for i in range(3):
34
34
  try:
35
- cnxn.execute("drop table t%d" % i)
35
+ cnxn.execute(f"drop table t{i}")
36
36
  except: # noqa
37
37
  pass
38
38
  yield cnxn
@@ -44,9 +44,9 @@ def _test_strtype(cnxn, sqltype, value, resulttype=None, colsize=None):
44
44
  assert colsize is None or (value is None or colsize >= len(value))
45
45
 
46
46
  if colsize:
47
- sql = "create table t1(s %s(%s))" % (sqltype, colsize)
47
+ sql = f"create table t1(s {sqltype}({colsize}))"
48
48
  else:
49
- sql = "create table t1(s %s)" % sqltype
49
+ sql = f"create table t1(s {sqltype})"
50
50
 
51
51
  if resulttype is None:
52
52
  resulttype = type(value)
@@ -148,7 +148,37 @@ def test_script(cnxn):
148
148
 
149
149
 
150
150
  #
151
- # copy
151
+ # copy to
152
+ #
153
+
154
+ def test_copytocsv(cnxn):
155
+ # Write a table to a CSV file.
156
+ #
157
+ # It isn't as efficient as I'd like since we can no longer get ahold of the file
158
+ # descriptor. I think we'll want to make this accept a string filename for a more
159
+ # efficient write. For now, make sure the output file is opened in text mode.
160
+
161
+ cnxn.execute("create table t1(a text, b text)")
162
+ cnxn.execute("insert into t1 values ('1', 'one'), ('2', 'two'), ('3', 'three')")
163
+
164
+ with tempfile.NamedTemporaryFile(mode='w', encoding='utf8') as tf:
165
+ print(tf.name)
166
+ count = cnxn.copy_to_csv('t1', tf, header=1)
167
+ assert count == 3
168
+
169
+ tf.seek(0)
170
+ with open(tf.name, mode='r', encoding='utf8') as fd:
171
+ reader = csv.reader(fd)
172
+ rows = []
173
+ for row in reader:
174
+ print('ROW=', row)
175
+ rows.append(row)
176
+
177
+ assert rows == [['a', 'b'], ['1', 'one'], ['2', 'two'], ['3', 'three']]
178
+
179
+
180
+ #
181
+ # copy from
152
182
  #
153
183
 
154
184
  def _datapath(filename):
@@ -159,7 +189,8 @@ def _datapath(filename):
159
189
 
160
190
  def test_copyfromcsv_csv(cnxn):
161
191
  cnxn.execute("create table t1(a int, b varchar(20))")
162
- count = cnxn.copy_from_csv("t1", open(_datapath('test-noheader.csv')))
192
+ with open(_datapath('test-noheader.csv')) as fd:
193
+ count = cnxn.copy_from_csv("t1", fd)
163
194
  assert count == 2
164
195
  assert cnxn.fetchval("select count(*) from t1") == 2
165
196
  row = cnxn.fetchrow("select a,b from t1 where a=2")
@@ -169,7 +200,8 @@ def test_copyfromcsv_csv(cnxn):
169
200
 
170
201
  def test_copyfromcsv_csv_header(cnxn):
171
202
  cnxn.execute("create table t1(a int, b varchar(20))")
172
- count = cnxn.copy_from_csv("t1", open(_datapath('test-header.csv')), header=True)
203
+ with open(_datapath('test-header.csv')) as fd:
204
+ count = cnxn.copy_from_csv("t1", fd, header=True)
173
205
  assert count == 2
174
206
  assert cnxn.fetchval("select count(*) from t1") == 2
175
207
  row = cnxn.fetchrow("select a,b from t1 where a=2")
@@ -184,13 +216,15 @@ def test_copyfromcsv_csv_error(cnxn):
184
216
  # We'll make the second column too small.
185
217
  cnxn.execute("create table t1(a int, b varchar(1) not null)")
186
218
  with pytest.raises(pglib.Error):
187
- cnxn.copy_from_csv("t1", open(_datapath('test-noheader.csv')))
219
+ with open(_datapath('test-noheader.csv')) as fd:
220
+ cnxn.copy_from_csv("t1", fd)
188
221
 
189
222
 
190
223
  def test_copyfromcsv_csv_gzip(cnxn):
191
224
  # I don't remember why this test is here. We're feeding it unzipped data.
192
225
  cnxn.execute("create table t1(a int, b varchar(20))")
193
- cnxn.copy_from_csv("t1", gzip.open(_datapath('test-header.csv.gz')), header=True)
226
+ with gzip.open(_datapath('test-header.csv.gz')) as fd:
227
+ cnxn.copy_from_csv("t1", fd, header=True)
194
228
  assert cnxn.fetchval("select count(*) from t1") == 2
195
229
  row = cnxn.fetchrow("select a,b from t1 where a=2")
196
230
  assert row.a == 2
@@ -1063,7 +1097,7 @@ def test_closed_error(cnxn):
1063
1097
  cnxn.rollback()
1064
1098
 
1065
1099
 
1066
- def test_count():
1100
+ def test_connection_count():
1067
1101
  before = pglib.connection_count()
1068
1102
  cnxn = pglib.connect(CONNINFO)
1069
1103
  assert pglib.connection_count() == (before + 1)
@@ -1073,6 +1107,18 @@ def test_count():
1073
1107
  assert pglib.connection_count() == before
1074
1108
 
1075
1109
 
1110
+ def test_count(cnxn):
1111
+ "Ensure delete statements return affected row count."
1112
+ cnxn.execute(
1113
+ """
1114
+ select generate_series(1, 3) as id
1115
+ into t1
1116
+ """)
1117
+
1118
+ count = cnxn.execute("delete from t1 where id in (1, 2, 3)")
1119
+ assert count == 3
1120
+
1121
+
1076
1122
  def test_hstore(cnxn):
1077
1123
  cnxn.execute("create extension if not exists hstore")
1078
1124
  row = cnxn.fetchrow("select oid, typname from pg_type where typname='hstore'")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes