pgtern 0.3.0__tar.gz → 0.4.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 (63) hide show
  1. pgtern-0.4.1/CHANGELOG.md +61 -0
  2. {pgtern-0.3.0 → pgtern-0.4.1}/CONTRIBUTING.md +1 -1
  3. pgtern-0.4.1/PKG-INFO +309 -0
  4. pgtern-0.4.1/README.md +277 -0
  5. {pgtern-0.3.0 → pgtern-0.4.1}/SECURITY.md +2 -2
  6. {pgtern-0.3.0 → pgtern-0.4.1}/pyproject.toml +9 -3
  7. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/__init__.py +3 -2
  8. pgtern-0.4.1/src/pgtern/_auth.py +185 -0
  9. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/_binary.py +14 -5
  10. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/_conn.py +227 -67
  11. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/_copy.py +11 -7
  12. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/_cursor.py +5 -3
  13. pgtern-0.4.1/src/pgtern/_dsn.py +298 -0
  14. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/_protocol.py +44 -3
  15. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/_query.py +6 -4
  16. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/_scram.py +19 -6
  17. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/_types.py +83 -28
  18. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/aio.py +474 -125
  19. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/hooks.py +31 -1
  20. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/pipeline.py +108 -41
  21. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/pool.py +39 -0
  22. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/sql.py +1 -1
  23. pgtern-0.4.1/src/pgtern.egg-info/PKG-INFO +309 -0
  24. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern.egg-info/SOURCES.txt +7 -0
  25. pgtern-0.4.1/src/pgtern.egg-info/requires.txt +7 -0
  26. {pgtern-0.3.0 → pgtern-0.4.1}/tests/test_binary.py +2 -2
  27. {pgtern-0.3.0 → pgtern-0.4.1}/tests/test_features.py +2 -1
  28. {pgtern-0.3.0 → pgtern-0.4.1}/tests/test_integration.py +18 -8
  29. {pgtern-0.3.0 → pgtern-0.4.1}/tests/test_journey.py +4 -1
  30. pgtern-0.4.1/tests/test_low_items.py +219 -0
  31. pgtern-0.4.1/tests/test_medium.py +420 -0
  32. {pgtern-0.3.0 → pgtern-0.4.1}/tests/test_pipeline.py +22 -18
  33. pgtern-0.4.1/tests/test_pool_gaps.py +94 -0
  34. pgtern-0.4.1/tests/test_regressions.py +477 -0
  35. pgtern-0.4.1/tests/test_scram_plus.py +108 -0
  36. pgtern-0.4.1/tests/test_session_state.py +163 -0
  37. {pgtern-0.3.0 → pgtern-0.4.1}/tests/test_unit.py +9 -7
  38. pgtern-0.3.0/CHANGELOG.md +0 -7
  39. pgtern-0.3.0/PKG-INFO +0 -265
  40. pgtern-0.3.0/README.md +0 -240
  41. pgtern-0.3.0/src/pgtern/_dsn.py +0 -182
  42. pgtern-0.3.0/src/pgtern.egg-info/PKG-INFO +0 -265
  43. pgtern-0.3.0/src/pgtern.egg-info/requires.txt +0 -4
  44. {pgtern-0.3.0 → pgtern-0.4.1}/LICENSE +0 -0
  45. {pgtern-0.3.0 → pgtern-0.4.1}/MANIFEST.in +0 -0
  46. {pgtern-0.3.0 → pgtern-0.4.1}/scripts/start-test-postgres.sh +0 -0
  47. {pgtern-0.3.0 → pgtern-0.4.1}/setup.cfg +0 -0
  48. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/_composite.py +0 -0
  49. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/adapters.py +0 -0
  50. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/errors.py +0 -0
  51. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/py.typed +0 -0
  52. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/rows.py +0 -0
  53. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/types/__init__.py +0 -0
  54. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern/types/json.py +0 -0
  55. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern.egg-info/dependency_links.txt +0 -0
  56. {pgtern-0.3.0 → pgtern-0.4.1}/src/pgtern.egg-info/top_level.txt +0 -0
  57. {pgtern-0.3.0 → pgtern-0.4.1}/tests/conftest.py +0 -0
  58. {pgtern-0.3.0 → pgtern-0.4.1}/tests/test_adapters.py +0 -0
  59. {pgtern-0.3.0 → pgtern-0.4.1}/tests/test_async.py +0 -0
  60. {pgtern-0.3.0 → pgtern-0.4.1}/tests/test_executemany.py +0 -0
  61. {pgtern-0.3.0 → pgtern-0.4.1}/tests/test_executemany_async.py +0 -0
  62. {pgtern-0.3.0 → pgtern-0.4.1}/tests/test_restart.py +0 -0
  63. {pgtern-0.3.0 → pgtern-0.4.1}/tests/test_transaction_pooler.py +0 -0
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ All notable changes to pgtern are documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.4.1] - 2026-11-06
8
+
9
+ ### Fixed
10
+
11
+ - The notifications example in the README connects with `autocommit=True`, so `LISTEN` takes effect.
12
+ - The package index now shows project links (homepage, source, issues and changelog).
13
+
14
+ ### Documentation
15
+
16
+ - The README follows the usual project layout, with absolute links and a status note.
17
+ - The security support table lists 0.4.x as supported.
18
+ - The contributing guide has the real clone URL.
19
+
20
+ ## [0.4.0] - 2026-11-06
21
+
22
+ ### Changed (behaviour)
23
+
24
+ These changes can affect existing code. Check them when upgrading.
25
+
26
+ - `timestamptz` values are returned in UTC in text mode as well as binary mode, so both formats give the same value and `tzinfo`.
27
+ - `connect_timeout` defaults to 30 seconds and applies to each host attempt. `connect_timeout=0` means no limit.
28
+ - `prepare=True` raises `NotSupportedError`. `prepare=False` is accepted.
29
+ - Bare `%b` and `%t` placeholders are refused with `ProgrammingError`. Only `%s` is accepted.
30
+ - A statement with more placeholders than parameters raises `ProgrammingError` instead of `IndexError`.
31
+ - Non-ASCII SQL or parameters under a non-UTF-8 `client_encoding` raise `NotSupportedError` before anything is sent.
32
+ - Using a connection from inside a COPY block or from inside a notice or notify handler raises `ProgrammingError` at once, instead of breaking the protocol or deadlocking.
33
+ - A query hook that fails on entry or exit is logged, not raised. A notice or notify handler that raises is logged too.
34
+
35
+ ### Added
36
+
37
+ - SCRAM-SHA-256-PLUS: when the server offers it over TLS, the login is bound to that connection (`tls-server-end-point`, RFC 5929).
38
+ - `max_result_bytes` connection attribute, a cap on the bytes one statement may return. It is `None` (no cap) by default. A result over the cap raises `DataError` and leaves the connection usable.
39
+ - The async pool resets itself after `fork()`, so a forked child never uses the parent's connections.
40
+
41
+ ### Fixed
42
+
43
+ - `24:00:00` is refused with `DataError` in binary mode. It used to be read as midnight.
44
+ - A `float4` parameter outside its range is sent as text, so the server reports the error instead of the driver raising it.
45
+ - Python `bool` values are no longer captured by a dumper registered for `int`.
46
+ - Session settings are followed after `SET client_encoding` and `SET DateStyle`, both when reading rows and when copying data.
47
+ - A failed connection open no longer leaves sessions behind.
48
+ - Array element delimiters come from the type, so arrays of `box` split correctly on `;`.
49
+ - The SCRAM client rejects a server nonce that does not extend its own, and rejects malformed base64.
50
+ - Bytes literals are correct when `standard_conforming_strings` is off.
51
+ - The type-loader cache is cleared after DDL, so a changed type is read again.
52
+ - Connect timeouts are enforced for the whole startup, so a silent server cannot hang a connection.
53
+
54
+ ### Infrastructure
55
+
56
+ - Continuous integration: lint and type check, a test matrix over Python 3.11 to 3.13 and PostgreSQL 14 to 18, a TLS, auth-roles, pooler and restart job, an installed-wheel test in a clean environment, and a build job that checks metadata and runtime dependencies.
57
+ - Development status classifier set to Beta.
58
+
59
+ ## [0.3.0]
60
+
61
+ Earlier release. Notes for this version were not kept in this file.
@@ -7,7 +7,7 @@ these in mind when you propose a change.
7
7
  ## Getting started
8
8
 
9
9
  ```
10
- git clone <repository-url> pgtern
10
+ git clone https://github.com/karawash-projects/pgtern.git pgtern
11
11
  cd pgtern
12
12
  python -m venv .venv && . .venv/bin/activate
13
13
  pip install -e ".[dev]"
pgtern-0.4.1/PKG-INFO ADDED
@@ -0,0 +1,309 @@
1
+ Metadata-Version: 2.4
2
+ Name: pgtern
3
+ Version: 0.4.1
4
+ Summary: A zero-dependency, pure-Python PostgreSQL client with blocking and asyncio APIs.
5
+ Author: Ahmad Karawash
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/karawash-projects/pgtern
8
+ Project-URL: Source, https://github.com/karawash-projects/pgtern
9
+ Project-URL: Issues, https://github.com/karawash-projects/pgtern/issues
10
+ Project-URL: Changelog, https://github.com/karawash-projects/pgtern/blob/main/CHANGELOG.md
11
+ Keywords: postgresql,postgres,database,driver,dbapi,scram,pure-python,zero-dependency
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8; extra == "dev"
27
+ Requires-Dist: pytest-timeout>=2; extra == "dev"
28
+ Requires-Dist: build>=1.2; extra == "dev"
29
+ Requires-Dist: pyflakes>=3; extra == "dev"
30
+ Requires-Dist: mypy>=1.10; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # pgtern
34
+
35
+ [![CI](https://github.com/karawash-projects/pgtern/actions/workflows/ci.yml/badge.svg)](https://github.com/karawash-projects/pgtern/actions/workflows/ci.yml)
36
+ [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/karawash-projects/pgtern/blob/main/LICENSE)
37
+
38
+ pgtern is a PostgreSQL client for Python. It speaks the PostgreSQL wire protocol directly and uses only the Python standard library, so there are no runtime dependencies, no C extensions and no build step. It provides a DB-API style interface in two forms: blocking, and `asyncio`.
39
+
40
+ > **Status: Beta.** pgtern is at version 0.x. The API may change before 1.0; changes are listed in the [CHANGELOG](https://github.com/karawash-projects/pgtern/blob/main/CHANGELOG.md).
41
+
42
+ ```python
43
+ import pgtern
44
+ from pgtern.rows import dict_row
45
+
46
+ with pgtern.connect("postgresql://app:secret@localhost/app", row_factory=dict_row) as conn:
47
+ with conn.transaction():
48
+ conn.execute("INSERT INTO notes (body) VALUES (%s)", ["hello"])
49
+ print(conn.execute("SELECT id, body FROM notes ORDER BY id").fetchall())
50
+ ```
51
+
52
+ ```python
53
+ import asyncio
54
+ from pgtern import aio
55
+
56
+ async def main():
57
+ async with aio.AsyncConnectionPool("postgresql://app:secret@localhost/app", min_size=2, max_size=10) as pool:
58
+ async with pool.connection() as conn:
59
+ cur = await conn.execute("SELECT %s::int", [1])
60
+ print(cur.fetchall())
61
+
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ ## Features
66
+
67
+ - No runtime dependencies. The package is a pure-Python wheel for Python 3.11 and newer.
68
+ - Blocking and `asyncio` APIs with the same names and behaviour, including pools, transactions, `COPY`, notifications and pipelines.
69
+ - SCRAM-SHA-256 authentication, and SCRAM-SHA-256-PLUS channel binding over TLS.
70
+ - TLS modes `disable`, `allow`, `prefer`, `require`, `verify-ca` and `verify-full`.
71
+ - Connection pools with liveness checks, idle and lifetime limits, and a fork-safe reset.
72
+ - Parameters are always sent separately from the SQL text. A helper module composes identifiers and literals safely.
73
+ - Opt-in binary result format for common types, and text format for everything else.
74
+ - Server-side cursors for large results, and batched `executemany`.
75
+ - Query hooks for logging, timing and tracing. They never receive bound parameter values.
76
+ - Exceptions organised by SQLSTATE class.
77
+
78
+ ## Installation
79
+
80
+ ```
81
+ pip install pgtern
82
+ ```
83
+
84
+ pgtern requires Python 3.11 or newer.
85
+
86
+ ## Connecting
87
+
88
+ `pgtern.connect` accepts URL and key=value connection strings, the standard `PG*` environment variables, Unix sockets, and a list of hosts for failover.
89
+
90
+ ```python
91
+ conn = pgtern.connect("postgresql://app@db1.internal,db2.internal/app?sslmode=verify-full&sslrootcert=/etc/ssl/ca.pem")
92
+ ```
93
+
94
+ TLS follows the PostgreSQL conventions. Under `allow` and `prefer`, the client refuses to send MD5 or cleartext passwords over an unencrypted connection. Under `disable` it sends them, because encryption was explicitly turned off. Set `require_auth=scram-sha-256` to refuse any server that does not use SCRAM, including servers that allow trust authentication.
95
+
96
+ Failover moves to the next host after any startup failure, including a timeout during TLS negotiation. Passwords can come from a `.pgpass` file (`PGPASSFILE` or `~/.pgpass`). A password file that other users can read is ignored.
97
+
98
+ Other supported options:
99
+
100
+ - `target_session_attrs`: `read-write`, `read-only`, `primary`, `standby` or `prefer-standby`.
101
+ - `hostaddr`: connect to an address while keeping the host name for certificate checks.
102
+ - `sslpassword` and `sslcrl`.
103
+ - `connect_timeout`: seconds allowed for each host attempt, from opening the socket to the end of startup. The default is 30. `0` means no limit.
104
+
105
+ ## Queries and transactions
106
+
107
+ ```python
108
+ with pgtern.connect(dsn) as conn:
109
+ conn.execute("CREATE TABLE IF NOT EXISTS accounts (id int PRIMARY KEY, balance numeric)")
110
+
111
+ with conn.transaction(): # commits on success, rolls back on error
112
+ conn.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", [10, 1])
113
+ conn.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", [10, 2])
114
+
115
+ cur = conn.execute("SELECT id, balance FROM accounts WHERE balance > %s", [0])
116
+ for row in cur:
117
+ print(row)
118
+ ```
119
+
120
+ Placeholders are `%s`. Values are sent as parameters, never interpolated into the SQL text. To build identifiers and literals safely, use the `sql` module:
121
+
122
+ ```python
123
+ from pgtern import sql
124
+
125
+ with pgtern.connect(dsn) as conn:
126
+ query = sql.SQL("SELECT * FROM {} WHERE id = %s").format(sql.Identifier("accounts"))
127
+ conn.execute(query, [1])
128
+ ```
129
+
130
+ ## Row formats
131
+
132
+ Choose how rows are returned with a row factory: `tuple_row` (the default), `dict_row` or `namedtuple_row`. Pass one to `connect`, or to a single cursor.
133
+
134
+ ## Bulk loading
135
+
136
+ `executemany` sends the whole batch as one pipelined transaction. In autocommit mode, a failing row rolls back the entire batch. Inside a transaction, it aborts that transaction as usual.
137
+
138
+ ```python
139
+ with pgtern.connect(dsn, autocommit=True) as conn:
140
+ conn.cursor().executemany("INSERT INTO events (kind, payload) VALUES (%s, %s)", rows)
141
+ ```
142
+
143
+ For the largest loads, use `COPY`:
144
+
145
+ ```python
146
+ with pgtern.connect(dsn) as conn:
147
+ with conn.cursor().copy("COPY events (kind, payload) FROM STDIN") as copy:
148
+ for kind, payload in rows:
149
+ copy.write_row((kind, payload))
150
+ ```
151
+
152
+ Export uses `COPY ... TO STDOUT`, read with `for chunk in copy` or `copy.rows()`.
153
+
154
+ ## Streaming large results
155
+
156
+ A named cursor runs on the server and fetches rows in batches, so a result never has to fit in memory:
157
+
158
+ ```python
159
+ with pgtern.connect(dsn) as conn:
160
+ with conn.transaction():
161
+ with conn.cursor("big_report") as cur:
162
+ cur.execute("SELECT * FROM events")
163
+ cur.itersize = 1000
164
+ for row in cur:
165
+ process(row)
166
+ ```
167
+
168
+ Server-side cursors support `scrollable`, `withhold` and `scroll()`.
169
+
170
+ ## Notifications
171
+
172
+ ```python
173
+ with pgtern.connect(dsn, autocommit=True) as conn: # LISTEN takes effect at once only in autocommit
174
+ conn.execute("LISTEN orders")
175
+ for note in conn.notifies(timeout=30):
176
+ print(note.channel, note.payload)
177
+ ```
178
+
179
+ The async connection provides the same notifications through `async for`. `LISTEN` needs a session that stays on one server connection, so use a direct connection or session-mode pooling for it.
180
+
181
+ ## Pipelines
182
+
183
+ Pipeline mode sends many statements before reading any reply:
184
+
185
+ ```python
186
+ with pgtern.connect(dsn) as conn:
187
+ with conn.pipeline() as pipe:
188
+ a = pipe.execute("INSERT INTO queue (job) VALUES (%s)", ["a"])
189
+ b = pipe.execute("SELECT count(*) FROM queue")
190
+ print(b.fetchone()) # results are ready once the block ends
191
+ ```
192
+
193
+ Replies are read in order at each sync point: `pipe.sync()`, the end of the block, or every `batch_size` statements. A sync point raises the first failure in its batch, so a failed statement cannot pass silently. Under autocommit, a sync point is also a commit point, so the statements in one batch succeed or fail together. If the block exits with an exception under autocommit, the batch is rolled back. Statements after a failure raise `pgtern.errors.PipelineAborted`. Pipelines support text format only, and `COPY` cannot run inside one.
194
+
195
+ ## Values and types
196
+
197
+ - Python `int` parameters are sent as `int4`, `int8` or `numeric`, depending on size. Use an explicit cast such as `%s::smallint` when a function takes `smallint`.
198
+ - `timestamptz` values are returned in UTC, in both text and binary format, with the same `tzinfo`.
199
+ - Intervals with months or years decode to `pgtern.Interval(months, delta)`, because a month has no fixed length. Other intervals decode to `timedelta`.
200
+ - `time` values of `24:00:00` raise `DataError`, because Python has no such time.
201
+ - Dates and times follow the session's `DateStyle`, and text follows its `client_encoding`. Both are read from the server after every change. Non-ASCII text under a non-UTF-8 encoding raises `NotSupportedError` before anything is sent.
202
+
203
+ Register loaders for PostgreSQL types and dumpers for Python classes:
204
+
205
+ ```python
206
+ from enum import Enum
207
+ from pgtern import adapters
208
+
209
+ class Mood(Enum):
210
+ SAD = "sad"
211
+ HAPPY = "happy"
212
+
213
+ adapters.register_loader("mood", adapters.enum_loader(Mood)) # CREATE TYPE mood AS ENUM (...)
214
+ adapters.register_loader("span", adapters.range_loader(int)) # CREATE TYPE span AS RANGE (subtype = int4)
215
+ adapters.register_dumper(Mood, lambda m: m.value)
216
+ ```
217
+
218
+ Loaders are looked up by type name once per connection and cached. A registered loader applies to connections opened afterwards, and to pooled connections when they are next returned to the pool.
219
+
220
+ ## Binary format
221
+
222
+ Opt in per statement with `binary=True`. It covers integers, floats, booleans, text, `bytea`, `uuid`, dates, times, intervals, `json` and `jsonb`, `numeric` results, and one-dimensional arrays of these. Other types use text.
223
+
224
+ ## Pools
225
+
226
+ ```python
227
+ from pgtern.pool import ConnectionPool
228
+
229
+ pool = ConnectionPool(dsn, min_size=4, max_size=20, timeout=5)
230
+ with pool.connection() as conn:
231
+ conn.execute("SELECT 1")
232
+ ```
233
+
234
+ A pool keeps `min_size` connections open and grows up to `max_size` on demand. The async pool keeps `min_size` open with a background task, and retires connections that have been idle longer than `max_idle` while the pool is above `min_size`. A connection is checked for liveness when it is handed out, rolled back if it was returned in the middle of a transaction, and replaced if the server has closed it. A caller that waits longer than `timeout` seconds receives `pgtern.PoolTimeout`.
235
+
236
+ ## Forking
237
+
238
+ Do not use a connection across `fork()`. A forked child that uses an inherited connection gets `OperationalError`. A pool resets itself in the child: on first use it discards the parent's connections without closing them, which would otherwise end the parent's sessions, and then opens its own.
239
+
240
+ ## Query hooks
241
+
242
+ A hook sees every statement, its row count and the connection target. It never sees bound parameter values:
243
+
244
+ ```python
245
+ from contextlib import contextmanager
246
+ import time
247
+ import pgtern
248
+
249
+ @contextmanager
250
+ def timing(info):
251
+ started = time.perf_counter()
252
+ try:
253
+ yield
254
+ finally:
255
+ print(f"{info.statement!r} took {time.perf_counter() - started:.3f}s ({info.rowcount} rows)")
256
+
257
+ remove = pgtern.add_query_hook(timing)
258
+ ```
259
+
260
+ A hook that fails on entry or exit is logged and ignored, so it never changes the result of a statement.
261
+
262
+ ## Limits and safety
263
+
264
+ - `max_result_bytes` is a connection attribute that caps the bytes one statement may return. It is `None` (no cap) by default. A result over the cap is read and discarded, the connection stays usable, and the statement raises `DataError`. Results are held in memory, so use a server-side cursor for large ones.
265
+ - Using a connection from inside a `COPY` block, or from inside a notice or notify handler, raises `ProgrammingError` at once.
266
+ - A password is not sent in plaintext unless you choose `sslmode=disable`, so a server that declines TLS under `prefer` cannot collect it.
267
+ - Over TLS, the login is bound to that connection with SCRAM-SHA-256-PLUS, so a login relayed through another connection fails.
268
+ - `verify-ca` and `verify-full` require a root certificate (`sslrootcert`). They fail rather than fall back to the system store.
269
+
270
+ ## Errors
271
+
272
+ Every exception derives from `pgtern.Error`. Each common SQLSTATE has its own class, such as `UniqueViolation` or `DeadlockDetected`, and each class also belongs to its broader category, such as `IntegrityError`:
273
+
274
+ ```python
275
+ with pgtern.connect(dsn) as conn:
276
+ try:
277
+ conn.execute("INSERT INTO accounts (id) VALUES (%s)", [1])
278
+ except pgtern.IntegrityError:
279
+ conn.rollback() # a failed transaction must be rolled back before the block ends
280
+ ```
281
+
282
+ ## Compatibility
283
+
284
+ - **PostgreSQL:** 14, 15, 16, 17 and 18.
285
+ - **Python:** 3.11, 3.12 and 3.13 are tested in continuous integration.
286
+ - **Platforms:** tested on Linux. Unix sockets, `fork()` and `.pgpass` permission checks behave differently on Windows, so check those yourself there.
287
+
288
+ ## Development
289
+
290
+ ```
291
+ pip install -e ".[dev]"
292
+ eval "$(scripts/start-test-postgres.sh)" # throwaway server with SCRAM, MD5, cleartext, trust and TLS users
293
+ pytest
294
+ scripts/start-test-postgres.sh stop
295
+ ```
296
+
297
+ The unit tests run without a server. The integration tests need `PGTERN_TEST_DSN`. Optional suites read `PGTERN_TEST_POOLER_DSN`, which points at a transaction-mode connection pooler, and `PGTERN_TEST_RESTART_CMD`, a command that restarts the test server.
298
+
299
+ ## Contributing
300
+
301
+ See [CONTRIBUTING.md](https://github.com/karawash-projects/pgtern/blob/main/CONTRIBUTING.md). Report security issues privately as described in [SECURITY.md](https://github.com/karawash-projects/pgtern/blob/main/SECURITY.md).
302
+
303
+ ## Changelog
304
+
305
+ Release history is in [CHANGELOG.md](https://github.com/karawash-projects/pgtern/blob/main/CHANGELOG.md).
306
+
307
+ ## License
308
+
309
+ Apache License 2.0. See [LICENSE](https://github.com/karawash-projects/pgtern/blob/main/LICENSE).
pgtern-0.4.1/README.md ADDED
@@ -0,0 +1,277 @@
1
+ # pgtern
2
+
3
+ [![CI](https://github.com/karawash-projects/pgtern/actions/workflows/ci.yml/badge.svg)](https://github.com/karawash-projects/pgtern/actions/workflows/ci.yml)
4
+ [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/karawash-projects/pgtern/blob/main/LICENSE)
5
+
6
+ pgtern is a PostgreSQL client for Python. It speaks the PostgreSQL wire protocol directly and uses only the Python standard library, so there are no runtime dependencies, no C extensions and no build step. It provides a DB-API style interface in two forms: blocking, and `asyncio`.
7
+
8
+ > **Status: Beta.** pgtern is at version 0.x. The API may change before 1.0; changes are listed in the [CHANGELOG](https://github.com/karawash-projects/pgtern/blob/main/CHANGELOG.md).
9
+
10
+ ```python
11
+ import pgtern
12
+ from pgtern.rows import dict_row
13
+
14
+ with pgtern.connect("postgresql://app:secret@localhost/app", row_factory=dict_row) as conn:
15
+ with conn.transaction():
16
+ conn.execute("INSERT INTO notes (body) VALUES (%s)", ["hello"])
17
+ print(conn.execute("SELECT id, body FROM notes ORDER BY id").fetchall())
18
+ ```
19
+
20
+ ```python
21
+ import asyncio
22
+ from pgtern import aio
23
+
24
+ async def main():
25
+ async with aio.AsyncConnectionPool("postgresql://app:secret@localhost/app", min_size=2, max_size=10) as pool:
26
+ async with pool.connection() as conn:
27
+ cur = await conn.execute("SELECT %s::int", [1])
28
+ print(cur.fetchall())
29
+
30
+ asyncio.run(main())
31
+ ```
32
+
33
+ ## Features
34
+
35
+ - No runtime dependencies. The package is a pure-Python wheel for Python 3.11 and newer.
36
+ - Blocking and `asyncio` APIs with the same names and behaviour, including pools, transactions, `COPY`, notifications and pipelines.
37
+ - SCRAM-SHA-256 authentication, and SCRAM-SHA-256-PLUS channel binding over TLS.
38
+ - TLS modes `disable`, `allow`, `prefer`, `require`, `verify-ca` and `verify-full`.
39
+ - Connection pools with liveness checks, idle and lifetime limits, and a fork-safe reset.
40
+ - Parameters are always sent separately from the SQL text. A helper module composes identifiers and literals safely.
41
+ - Opt-in binary result format for common types, and text format for everything else.
42
+ - Server-side cursors for large results, and batched `executemany`.
43
+ - Query hooks for logging, timing and tracing. They never receive bound parameter values.
44
+ - Exceptions organised by SQLSTATE class.
45
+
46
+ ## Installation
47
+
48
+ ```
49
+ pip install pgtern
50
+ ```
51
+
52
+ pgtern requires Python 3.11 or newer.
53
+
54
+ ## Connecting
55
+
56
+ `pgtern.connect` accepts URL and key=value connection strings, the standard `PG*` environment variables, Unix sockets, and a list of hosts for failover.
57
+
58
+ ```python
59
+ conn = pgtern.connect("postgresql://app@db1.internal,db2.internal/app?sslmode=verify-full&sslrootcert=/etc/ssl/ca.pem")
60
+ ```
61
+
62
+ TLS follows the PostgreSQL conventions. Under `allow` and `prefer`, the client refuses to send MD5 or cleartext passwords over an unencrypted connection. Under `disable` it sends them, because encryption was explicitly turned off. Set `require_auth=scram-sha-256` to refuse any server that does not use SCRAM, including servers that allow trust authentication.
63
+
64
+ Failover moves to the next host after any startup failure, including a timeout during TLS negotiation. Passwords can come from a `.pgpass` file (`PGPASSFILE` or `~/.pgpass`). A password file that other users can read is ignored.
65
+
66
+ Other supported options:
67
+
68
+ - `target_session_attrs`: `read-write`, `read-only`, `primary`, `standby` or `prefer-standby`.
69
+ - `hostaddr`: connect to an address while keeping the host name for certificate checks.
70
+ - `sslpassword` and `sslcrl`.
71
+ - `connect_timeout`: seconds allowed for each host attempt, from opening the socket to the end of startup. The default is 30. `0` means no limit.
72
+
73
+ ## Queries and transactions
74
+
75
+ ```python
76
+ with pgtern.connect(dsn) as conn:
77
+ conn.execute("CREATE TABLE IF NOT EXISTS accounts (id int PRIMARY KEY, balance numeric)")
78
+
79
+ with conn.transaction(): # commits on success, rolls back on error
80
+ conn.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", [10, 1])
81
+ conn.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", [10, 2])
82
+
83
+ cur = conn.execute("SELECT id, balance FROM accounts WHERE balance > %s", [0])
84
+ for row in cur:
85
+ print(row)
86
+ ```
87
+
88
+ Placeholders are `%s`. Values are sent as parameters, never interpolated into the SQL text. To build identifiers and literals safely, use the `sql` module:
89
+
90
+ ```python
91
+ from pgtern import sql
92
+
93
+ with pgtern.connect(dsn) as conn:
94
+ query = sql.SQL("SELECT * FROM {} WHERE id = %s").format(sql.Identifier("accounts"))
95
+ conn.execute(query, [1])
96
+ ```
97
+
98
+ ## Row formats
99
+
100
+ Choose how rows are returned with a row factory: `tuple_row` (the default), `dict_row` or `namedtuple_row`. Pass one to `connect`, or to a single cursor.
101
+
102
+ ## Bulk loading
103
+
104
+ `executemany` sends the whole batch as one pipelined transaction. In autocommit mode, a failing row rolls back the entire batch. Inside a transaction, it aborts that transaction as usual.
105
+
106
+ ```python
107
+ with pgtern.connect(dsn, autocommit=True) as conn:
108
+ conn.cursor().executemany("INSERT INTO events (kind, payload) VALUES (%s, %s)", rows)
109
+ ```
110
+
111
+ For the largest loads, use `COPY`:
112
+
113
+ ```python
114
+ with pgtern.connect(dsn) as conn:
115
+ with conn.cursor().copy("COPY events (kind, payload) FROM STDIN") as copy:
116
+ for kind, payload in rows:
117
+ copy.write_row((kind, payload))
118
+ ```
119
+
120
+ Export uses `COPY ... TO STDOUT`, read with `for chunk in copy` or `copy.rows()`.
121
+
122
+ ## Streaming large results
123
+
124
+ A named cursor runs on the server and fetches rows in batches, so a result never has to fit in memory:
125
+
126
+ ```python
127
+ with pgtern.connect(dsn) as conn:
128
+ with conn.transaction():
129
+ with conn.cursor("big_report") as cur:
130
+ cur.execute("SELECT * FROM events")
131
+ cur.itersize = 1000
132
+ for row in cur:
133
+ process(row)
134
+ ```
135
+
136
+ Server-side cursors support `scrollable`, `withhold` and `scroll()`.
137
+
138
+ ## Notifications
139
+
140
+ ```python
141
+ with pgtern.connect(dsn, autocommit=True) as conn: # LISTEN takes effect at once only in autocommit
142
+ conn.execute("LISTEN orders")
143
+ for note in conn.notifies(timeout=30):
144
+ print(note.channel, note.payload)
145
+ ```
146
+
147
+ The async connection provides the same notifications through `async for`. `LISTEN` needs a session that stays on one server connection, so use a direct connection or session-mode pooling for it.
148
+
149
+ ## Pipelines
150
+
151
+ Pipeline mode sends many statements before reading any reply:
152
+
153
+ ```python
154
+ with pgtern.connect(dsn) as conn:
155
+ with conn.pipeline() as pipe:
156
+ a = pipe.execute("INSERT INTO queue (job) VALUES (%s)", ["a"])
157
+ b = pipe.execute("SELECT count(*) FROM queue")
158
+ print(b.fetchone()) # results are ready once the block ends
159
+ ```
160
+
161
+ Replies are read in order at each sync point: `pipe.sync()`, the end of the block, or every `batch_size` statements. A sync point raises the first failure in its batch, so a failed statement cannot pass silently. Under autocommit, a sync point is also a commit point, so the statements in one batch succeed or fail together. If the block exits with an exception under autocommit, the batch is rolled back. Statements after a failure raise `pgtern.errors.PipelineAborted`. Pipelines support text format only, and `COPY` cannot run inside one.
162
+
163
+ ## Values and types
164
+
165
+ - Python `int` parameters are sent as `int4`, `int8` or `numeric`, depending on size. Use an explicit cast such as `%s::smallint` when a function takes `smallint`.
166
+ - `timestamptz` values are returned in UTC, in both text and binary format, with the same `tzinfo`.
167
+ - Intervals with months or years decode to `pgtern.Interval(months, delta)`, because a month has no fixed length. Other intervals decode to `timedelta`.
168
+ - `time` values of `24:00:00` raise `DataError`, because Python has no such time.
169
+ - Dates and times follow the session's `DateStyle`, and text follows its `client_encoding`. Both are read from the server after every change. Non-ASCII text under a non-UTF-8 encoding raises `NotSupportedError` before anything is sent.
170
+
171
+ Register loaders for PostgreSQL types and dumpers for Python classes:
172
+
173
+ ```python
174
+ from enum import Enum
175
+ from pgtern import adapters
176
+
177
+ class Mood(Enum):
178
+ SAD = "sad"
179
+ HAPPY = "happy"
180
+
181
+ adapters.register_loader("mood", adapters.enum_loader(Mood)) # CREATE TYPE mood AS ENUM (...)
182
+ adapters.register_loader("span", adapters.range_loader(int)) # CREATE TYPE span AS RANGE (subtype = int4)
183
+ adapters.register_dumper(Mood, lambda m: m.value)
184
+ ```
185
+
186
+ Loaders are looked up by type name once per connection and cached. A registered loader applies to connections opened afterwards, and to pooled connections when they are next returned to the pool.
187
+
188
+ ## Binary format
189
+
190
+ Opt in per statement with `binary=True`. It covers integers, floats, booleans, text, `bytea`, `uuid`, dates, times, intervals, `json` and `jsonb`, `numeric` results, and one-dimensional arrays of these. Other types use text.
191
+
192
+ ## Pools
193
+
194
+ ```python
195
+ from pgtern.pool import ConnectionPool
196
+
197
+ pool = ConnectionPool(dsn, min_size=4, max_size=20, timeout=5)
198
+ with pool.connection() as conn:
199
+ conn.execute("SELECT 1")
200
+ ```
201
+
202
+ A pool keeps `min_size` connections open and grows up to `max_size` on demand. The async pool keeps `min_size` open with a background task, and retires connections that have been idle longer than `max_idle` while the pool is above `min_size`. A connection is checked for liveness when it is handed out, rolled back if it was returned in the middle of a transaction, and replaced if the server has closed it. A caller that waits longer than `timeout` seconds receives `pgtern.PoolTimeout`.
203
+
204
+ ## Forking
205
+
206
+ Do not use a connection across `fork()`. A forked child that uses an inherited connection gets `OperationalError`. A pool resets itself in the child: on first use it discards the parent's connections without closing them, which would otherwise end the parent's sessions, and then opens its own.
207
+
208
+ ## Query hooks
209
+
210
+ A hook sees every statement, its row count and the connection target. It never sees bound parameter values:
211
+
212
+ ```python
213
+ from contextlib import contextmanager
214
+ import time
215
+ import pgtern
216
+
217
+ @contextmanager
218
+ def timing(info):
219
+ started = time.perf_counter()
220
+ try:
221
+ yield
222
+ finally:
223
+ print(f"{info.statement!r} took {time.perf_counter() - started:.3f}s ({info.rowcount} rows)")
224
+
225
+ remove = pgtern.add_query_hook(timing)
226
+ ```
227
+
228
+ A hook that fails on entry or exit is logged and ignored, so it never changes the result of a statement.
229
+
230
+ ## Limits and safety
231
+
232
+ - `max_result_bytes` is a connection attribute that caps the bytes one statement may return. It is `None` (no cap) by default. A result over the cap is read and discarded, the connection stays usable, and the statement raises `DataError`. Results are held in memory, so use a server-side cursor for large ones.
233
+ - Using a connection from inside a `COPY` block, or from inside a notice or notify handler, raises `ProgrammingError` at once.
234
+ - A password is not sent in plaintext unless you choose `sslmode=disable`, so a server that declines TLS under `prefer` cannot collect it.
235
+ - Over TLS, the login is bound to that connection with SCRAM-SHA-256-PLUS, so a login relayed through another connection fails.
236
+ - `verify-ca` and `verify-full` require a root certificate (`sslrootcert`). They fail rather than fall back to the system store.
237
+
238
+ ## Errors
239
+
240
+ Every exception derives from `pgtern.Error`. Each common SQLSTATE has its own class, such as `UniqueViolation` or `DeadlockDetected`, and each class also belongs to its broader category, such as `IntegrityError`:
241
+
242
+ ```python
243
+ with pgtern.connect(dsn) as conn:
244
+ try:
245
+ conn.execute("INSERT INTO accounts (id) VALUES (%s)", [1])
246
+ except pgtern.IntegrityError:
247
+ conn.rollback() # a failed transaction must be rolled back before the block ends
248
+ ```
249
+
250
+ ## Compatibility
251
+
252
+ - **PostgreSQL:** 14, 15, 16, 17 and 18.
253
+ - **Python:** 3.11, 3.12 and 3.13 are tested in continuous integration.
254
+ - **Platforms:** tested on Linux. Unix sockets, `fork()` and `.pgpass` permission checks behave differently on Windows, so check those yourself there.
255
+
256
+ ## Development
257
+
258
+ ```
259
+ pip install -e ".[dev]"
260
+ eval "$(scripts/start-test-postgres.sh)" # throwaway server with SCRAM, MD5, cleartext, trust and TLS users
261
+ pytest
262
+ scripts/start-test-postgres.sh stop
263
+ ```
264
+
265
+ The unit tests run without a server. The integration tests need `PGTERN_TEST_DSN`. Optional suites read `PGTERN_TEST_POOLER_DSN`, which points at a transaction-mode connection pooler, and `PGTERN_TEST_RESTART_CMD`, a command that restarts the test server.
266
+
267
+ ## Contributing
268
+
269
+ See [CONTRIBUTING.md](https://github.com/karawash-projects/pgtern/blob/main/CONTRIBUTING.md). Report security issues privately as described in [SECURITY.md](https://github.com/karawash-projects/pgtern/blob/main/SECURITY.md).
270
+
271
+ ## Changelog
272
+
273
+ Release history is in [CHANGELOG.md](https://github.com/karawash-projects/pgtern/blob/main/CHANGELOG.md).
274
+
275
+ ## License
276
+
277
+ Apache License 2.0. See [LICENSE](https://github.com/karawash-projects/pgtern/blob/main/LICENSE).
@@ -6,8 +6,8 @@ Security fixes are released for the most recent minor version of pgtern.
6
6
 
7
7
  | Version | Supported |
8
8
  |---------|-----------|
9
- | 0.3.x | Yes |
10
- | < 0.3 | No |
9
+ | 0.4.x | Yes |
10
+ | < 0.4 | No |
11
11
 
12
12
  ## Reporting a vulnerability
13
13