pgtern 0.3.0__tar.gz → 0.4.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.
- pgtern-0.4.0/CHANGELOG.md +48 -0
- pgtern-0.4.0/PKG-INFO +303 -0
- pgtern-0.4.0/README.md +275 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/pyproject.toml +3 -3
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/__init__.py +3 -2
- pgtern-0.4.0/src/pgtern/_auth.py +185 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/_binary.py +14 -5
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/_conn.py +227 -67
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/_copy.py +11 -7
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/_cursor.py +5 -3
- pgtern-0.4.0/src/pgtern/_dsn.py +298 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/_protocol.py +44 -3
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/_query.py +6 -4
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/_scram.py +19 -6
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/_types.py +83 -28
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/aio.py +474 -125
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/hooks.py +31 -1
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/pipeline.py +108 -41
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/pool.py +39 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/sql.py +1 -1
- pgtern-0.4.0/src/pgtern.egg-info/PKG-INFO +303 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern.egg-info/SOURCES.txt +7 -0
- pgtern-0.4.0/src/pgtern.egg-info/requires.txt +7 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/test_binary.py +2 -2
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/test_features.py +2 -1
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/test_integration.py +18 -8
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/test_journey.py +4 -1
- pgtern-0.4.0/tests/test_low_items.py +219 -0
- pgtern-0.4.0/tests/test_medium.py +420 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/test_pipeline.py +22 -18
- pgtern-0.4.0/tests/test_pool_gaps.py +94 -0
- pgtern-0.4.0/tests/test_regressions.py +477 -0
- pgtern-0.4.0/tests/test_scram_plus.py +108 -0
- pgtern-0.4.0/tests/test_session_state.py +163 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/test_unit.py +9 -7
- pgtern-0.3.0/CHANGELOG.md +0 -7
- pgtern-0.3.0/PKG-INFO +0 -265
- pgtern-0.3.0/README.md +0 -240
- pgtern-0.3.0/src/pgtern/_dsn.py +0 -182
- pgtern-0.3.0/src/pgtern.egg-info/PKG-INFO +0 -265
- pgtern-0.3.0/src/pgtern.egg-info/requires.txt +0 -4
- {pgtern-0.3.0 → pgtern-0.4.0}/CONTRIBUTING.md +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/LICENSE +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/MANIFEST.in +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/SECURITY.md +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/scripts/start-test-postgres.sh +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/setup.cfg +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/_composite.py +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/adapters.py +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/errors.py +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/py.typed +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/rows.py +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/types/__init__.py +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern/types/json.py +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern.egg-info/dependency_links.txt +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/src/pgtern.egg-info/top_level.txt +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/conftest.py +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/test_adapters.py +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/test_async.py +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/test_executemany.py +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/test_executemany_async.py +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/test_restart.py +0 -0
- {pgtern-0.3.0 → pgtern-0.4.0}/tests/test_transaction_pooler.py +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to pgtern are documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.4.0] - 2026-11-06
|
|
8
|
+
|
|
9
|
+
### Changed (behaviour)
|
|
10
|
+
|
|
11
|
+
These changes can affect existing code. Check them when upgrading.
|
|
12
|
+
|
|
13
|
+
- `timestamptz` values are returned in UTC in text mode as well as binary mode, so both formats give the same value and `tzinfo`.
|
|
14
|
+
- `connect_timeout` defaults to 30 seconds and applies to each host attempt. `connect_timeout=0` means no limit.
|
|
15
|
+
- `prepare=True` raises `NotSupportedError`. `prepare=False` is accepted.
|
|
16
|
+
- Bare `%b` and `%t` placeholders are refused with `ProgrammingError`. Only `%s` is accepted.
|
|
17
|
+
- A statement with more placeholders than parameters raises `ProgrammingError` instead of `IndexError`.
|
|
18
|
+
- Non-ASCII SQL or parameters under a non-UTF-8 `client_encoding` raise `NotSupportedError` before anything is sent.
|
|
19
|
+
- 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.
|
|
20
|
+
- A query hook that fails on entry or exit is logged, not raised. A notice or notify handler that raises is logged too.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- SCRAM-SHA-256-PLUS: when the server offers it over TLS, the login is bound to that connection (`tls-server-end-point`, RFC 5929).
|
|
25
|
+
- `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.
|
|
26
|
+
- The async pool resets itself after `fork()`, so a forked child never uses the parent's connections.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `24:00:00` is refused with `DataError` in binary mode. It used to be read as midnight.
|
|
31
|
+
- A `float4` parameter outside its range is sent as text, so the server reports the error instead of the driver raising it.
|
|
32
|
+
- Python `bool` values are no longer captured by a dumper registered for `int`.
|
|
33
|
+
- Session settings are followed after `SET client_encoding` and `SET DateStyle`, both when reading rows and when copying data.
|
|
34
|
+
- A failed connection open no longer leaves sessions behind.
|
|
35
|
+
- Array element delimiters come from the type, so arrays of `box` split correctly on `;`.
|
|
36
|
+
- The SCRAM client rejects a server nonce that does not extend its own, and rejects malformed base64.
|
|
37
|
+
- Bytes literals are correct when `standard_conforming_strings` is off.
|
|
38
|
+
- The type-loader cache is cleared after DDL, so a changed type is read again.
|
|
39
|
+
- Connect timeouts are enforced for the whole startup, so a silent server cannot hang a connection.
|
|
40
|
+
|
|
41
|
+
### Infrastructure
|
|
42
|
+
|
|
43
|
+
- 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.
|
|
44
|
+
- Development status classifier set to Beta.
|
|
45
|
+
|
|
46
|
+
## [0.3.0]
|
|
47
|
+
|
|
48
|
+
Earlier release. Notes for this version were not kept in this file.
|
pgtern-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pgtern
|
|
3
|
+
Version: 0.4.0
|
|
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
|
+
Keywords: postgresql,postgres,database,driver,dbapi,scram,pure-python,zero-dependency
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Database
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-timeout>=2; extra == "dev"
|
|
24
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
25
|
+
Requires-Dist: pyflakes>=3; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# pgtern
|
|
30
|
+
|
|
31
|
+
[](https://github.com/karawash-projects/pgtern/actions/workflows/ci.yml)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
|
|
34
|
+
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`.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import pgtern
|
|
38
|
+
from pgtern.rows import dict_row
|
|
39
|
+
|
|
40
|
+
with pgtern.connect("postgresql://app:secret@localhost/app", row_factory=dict_row) as conn:
|
|
41
|
+
with conn.transaction():
|
|
42
|
+
conn.execute("INSERT INTO notes (body) VALUES (%s)", ["hello"])
|
|
43
|
+
print(conn.execute("SELECT id, body FROM notes ORDER BY id").fetchall())
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
from pgtern import aio
|
|
49
|
+
|
|
50
|
+
async def main():
|
|
51
|
+
async with aio.AsyncConnectionPool("postgresql://app:secret@localhost/app", min_size=2, max_size=10) as pool:
|
|
52
|
+
async with pool.connection() as conn:
|
|
53
|
+
cur = await conn.execute("SELECT %s::int", [1])
|
|
54
|
+
print(cur.fetchall())
|
|
55
|
+
|
|
56
|
+
asyncio.run(main())
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- No runtime dependencies. The package is a pure-Python wheel for Python 3.11 and newer.
|
|
62
|
+
- Blocking and `asyncio` APIs with the same names and behaviour, including pools, transactions, `COPY`, notifications and pipelines.
|
|
63
|
+
- SCRAM-SHA-256 authentication, and SCRAM-SHA-256-PLUS channel binding over TLS.
|
|
64
|
+
- TLS modes `disable`, `allow`, `prefer`, `require`, `verify-ca` and `verify-full`.
|
|
65
|
+
- Connection pools with liveness checks, idle and lifetime limits, and a fork-safe reset.
|
|
66
|
+
- Parameters are always sent separately from the SQL text. A helper module composes identifiers and literals safely.
|
|
67
|
+
- Opt-in binary result format for common types, and text format for everything else.
|
|
68
|
+
- Server-side cursors for large results, and batched `executemany`.
|
|
69
|
+
- Query hooks for logging, timing and tracing. They never receive bound parameter values.
|
|
70
|
+
- Exceptions organised by SQLSTATE class.
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
pip install pgtern
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
pgtern requires Python 3.11 or newer.
|
|
79
|
+
|
|
80
|
+
## Connecting
|
|
81
|
+
|
|
82
|
+
`pgtern.connect` accepts URL and key=value connection strings, the standard `PG*` environment variables, Unix sockets, and a list of hosts for failover.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
conn = pgtern.connect("postgresql://app@db1.internal,db2.internal/app?sslmode=verify-full&sslrootcert=/etc/ssl/ca.pem")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
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.
|
|
91
|
+
|
|
92
|
+
Other supported options:
|
|
93
|
+
|
|
94
|
+
- `target_session_attrs`: `read-write`, `read-only`, `primary`, `standby` or `prefer-standby`.
|
|
95
|
+
- `hostaddr`: connect to an address while keeping the host name for certificate checks.
|
|
96
|
+
- `sslpassword` and `sslcrl`.
|
|
97
|
+
- `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.
|
|
98
|
+
|
|
99
|
+
## Queries and transactions
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
with pgtern.connect(dsn) as conn:
|
|
103
|
+
conn.execute("CREATE TABLE IF NOT EXISTS accounts (id int PRIMARY KEY, balance numeric)")
|
|
104
|
+
|
|
105
|
+
with conn.transaction(): # commits on success, rolls back on error
|
|
106
|
+
conn.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", [10, 1])
|
|
107
|
+
conn.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", [10, 2])
|
|
108
|
+
|
|
109
|
+
cur = conn.execute("SELECT id, balance FROM accounts WHERE balance > %s", [0])
|
|
110
|
+
for row in cur:
|
|
111
|
+
print(row)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Placeholders are `%s`. Values are sent as parameters, never interpolated into the SQL text. To build identifiers and literals safely, use the `sql` module:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from pgtern import sql
|
|
118
|
+
|
|
119
|
+
with pgtern.connect(dsn) as conn:
|
|
120
|
+
query = sql.SQL("SELECT * FROM {} WHERE id = %s").format(sql.Identifier("accounts"))
|
|
121
|
+
conn.execute(query, [1])
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Row formats
|
|
125
|
+
|
|
126
|
+
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.
|
|
127
|
+
|
|
128
|
+
## Bulk loading
|
|
129
|
+
|
|
130
|
+
`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.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
with pgtern.connect(dsn, autocommit=True) as conn:
|
|
134
|
+
conn.cursor().executemany("INSERT INTO events (kind, payload) VALUES (%s, %s)", rows)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
For the largest loads, use `COPY`:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
with pgtern.connect(dsn) as conn:
|
|
141
|
+
with conn.cursor().copy("COPY events (kind, payload) FROM STDIN") as copy:
|
|
142
|
+
for kind, payload in rows:
|
|
143
|
+
copy.write_row((kind, payload))
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Export uses `COPY ... TO STDOUT`, read with `for chunk in copy` or `copy.rows()`.
|
|
147
|
+
|
|
148
|
+
## Streaming large results
|
|
149
|
+
|
|
150
|
+
A named cursor runs on the server and fetches rows in batches, so a result never has to fit in memory:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
with pgtern.connect(dsn) as conn:
|
|
154
|
+
with conn.transaction():
|
|
155
|
+
with conn.cursor("big_report") as cur:
|
|
156
|
+
cur.execute("SELECT * FROM events")
|
|
157
|
+
cur.itersize = 1000
|
|
158
|
+
for row in cur:
|
|
159
|
+
process(row)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Server-side cursors support `scrollable`, `withhold` and `scroll()`.
|
|
163
|
+
|
|
164
|
+
## Notifications
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
with pgtern.connect(dsn) as conn:
|
|
168
|
+
conn.execute("LISTEN orders")
|
|
169
|
+
for note in conn.notifies(timeout=30):
|
|
170
|
+
print(note.channel, note.payload)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
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.
|
|
174
|
+
|
|
175
|
+
## Pipelines
|
|
176
|
+
|
|
177
|
+
Pipeline mode sends many statements before reading any reply:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
with pgtern.connect(dsn) as conn:
|
|
181
|
+
with conn.pipeline() as pipe:
|
|
182
|
+
a = pipe.execute("INSERT INTO queue (job) VALUES (%s)", ["a"])
|
|
183
|
+
b = pipe.execute("SELECT count(*) FROM queue")
|
|
184
|
+
print(b.fetchone()) # results are ready once the block ends
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
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.
|
|
188
|
+
|
|
189
|
+
## Values and types
|
|
190
|
+
|
|
191
|
+
- 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`.
|
|
192
|
+
- `timestamptz` values are returned in UTC, in both text and binary format, with the same `tzinfo`.
|
|
193
|
+
- Intervals with months or years decode to `pgtern.Interval(months, delta)`, because a month has no fixed length. Other intervals decode to `timedelta`.
|
|
194
|
+
- `time` values of `24:00:00` raise `DataError`, because Python has no such time.
|
|
195
|
+
- 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.
|
|
196
|
+
|
|
197
|
+
Register loaders for PostgreSQL types and dumpers for Python classes:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from enum import Enum
|
|
201
|
+
from pgtern import adapters
|
|
202
|
+
|
|
203
|
+
class Mood(Enum):
|
|
204
|
+
SAD = "sad"
|
|
205
|
+
HAPPY = "happy"
|
|
206
|
+
|
|
207
|
+
adapters.register_loader("mood", adapters.enum_loader(Mood)) # CREATE TYPE mood AS ENUM (...)
|
|
208
|
+
adapters.register_loader("span", adapters.range_loader(int)) # CREATE TYPE span AS RANGE (subtype = int4)
|
|
209
|
+
adapters.register_dumper(Mood, lambda m: m.value)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
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.
|
|
213
|
+
|
|
214
|
+
## Binary format
|
|
215
|
+
|
|
216
|
+
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.
|
|
217
|
+
|
|
218
|
+
## Pools
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from pgtern.pool import ConnectionPool
|
|
222
|
+
|
|
223
|
+
pool = ConnectionPool(dsn, min_size=4, max_size=20, timeout=5)
|
|
224
|
+
with pool.connection() as conn:
|
|
225
|
+
conn.execute("SELECT 1")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
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`.
|
|
229
|
+
|
|
230
|
+
## Forking
|
|
231
|
+
|
|
232
|
+
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.
|
|
233
|
+
|
|
234
|
+
## Query hooks
|
|
235
|
+
|
|
236
|
+
A hook sees every statement, its row count and the connection target. It never sees bound parameter values:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from contextlib import contextmanager
|
|
240
|
+
import time
|
|
241
|
+
import pgtern
|
|
242
|
+
|
|
243
|
+
@contextmanager
|
|
244
|
+
def timing(info):
|
|
245
|
+
started = time.perf_counter()
|
|
246
|
+
try:
|
|
247
|
+
yield
|
|
248
|
+
finally:
|
|
249
|
+
print(f"{info.statement!r} took {time.perf_counter() - started:.3f}s ({info.rowcount} rows)")
|
|
250
|
+
|
|
251
|
+
remove = pgtern.add_query_hook(timing)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
A hook that fails on entry or exit is logged and ignored, so it never changes the result of a statement.
|
|
255
|
+
|
|
256
|
+
## Limits and safety
|
|
257
|
+
|
|
258
|
+
- `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.
|
|
259
|
+
- Using a connection from inside a `COPY` block, or from inside a notice or notify handler, raises `ProgrammingError` at once.
|
|
260
|
+
- A password is not sent in plaintext unless you choose `sslmode=disable`, so a server that declines TLS under `prefer` cannot collect it.
|
|
261
|
+
- Over TLS, the login is bound to that connection with SCRAM-SHA-256-PLUS, so a login relayed through another connection fails.
|
|
262
|
+
- `verify-ca` and `verify-full` require a root certificate (`sslrootcert`). They fail rather than fall back to the system store.
|
|
263
|
+
|
|
264
|
+
## Errors
|
|
265
|
+
|
|
266
|
+
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`:
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
with pgtern.connect(dsn) as conn:
|
|
270
|
+
try:
|
|
271
|
+
conn.execute("INSERT INTO accounts (id) VALUES (%s)", [1])
|
|
272
|
+
except pgtern.IntegrityError:
|
|
273
|
+
conn.rollback() # a failed transaction must be rolled back before the block ends
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Compatibility
|
|
277
|
+
|
|
278
|
+
- **PostgreSQL:** 14, 15, 16, 17 and 18.
|
|
279
|
+
- **Python:** 3.11, 3.12 and 3.13 are tested in continuous integration.
|
|
280
|
+
- **Platforms:** anywhere CPython runs.
|
|
281
|
+
|
|
282
|
+
## Development
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
pip install -e ".[dev]"
|
|
286
|
+
eval "$(scripts/start-test-postgres.sh)" # throwaway server with SCRAM, MD5, cleartext, trust and TLS users
|
|
287
|
+
pytest
|
|
288
|
+
scripts/start-test-postgres.sh stop
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
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.
|
|
292
|
+
|
|
293
|
+
## Contributing
|
|
294
|
+
|
|
295
|
+
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).
|
|
296
|
+
|
|
297
|
+
## Changelog
|
|
298
|
+
|
|
299
|
+
Release history is in [CHANGELOG.md](https://github.com/karawash-projects/pgtern/blob/main/CHANGELOG.md).
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
Apache License 2.0. See [LICENSE](https://github.com/karawash-projects/pgtern/blob/main/LICENSE).
|
pgtern-0.4.0/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# pgtern
|
|
2
|
+
|
|
3
|
+
[](https://github.com/karawash-projects/pgtern/actions/workflows/ci.yml)
|
|
4
|
+
[](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
|
+
```python
|
|
9
|
+
import pgtern
|
|
10
|
+
from pgtern.rows import dict_row
|
|
11
|
+
|
|
12
|
+
with pgtern.connect("postgresql://app:secret@localhost/app", row_factory=dict_row) as conn:
|
|
13
|
+
with conn.transaction():
|
|
14
|
+
conn.execute("INSERT INTO notes (body) VALUES (%s)", ["hello"])
|
|
15
|
+
print(conn.execute("SELECT id, body FROM notes ORDER BY id").fetchall())
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import asyncio
|
|
20
|
+
from pgtern import aio
|
|
21
|
+
|
|
22
|
+
async def main():
|
|
23
|
+
async with aio.AsyncConnectionPool("postgresql://app:secret@localhost/app", min_size=2, max_size=10) as pool:
|
|
24
|
+
async with pool.connection() as conn:
|
|
25
|
+
cur = await conn.execute("SELECT %s::int", [1])
|
|
26
|
+
print(cur.fetchall())
|
|
27
|
+
|
|
28
|
+
asyncio.run(main())
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- No runtime dependencies. The package is a pure-Python wheel for Python 3.11 and newer.
|
|
34
|
+
- Blocking and `asyncio` APIs with the same names and behaviour, including pools, transactions, `COPY`, notifications and pipelines.
|
|
35
|
+
- SCRAM-SHA-256 authentication, and SCRAM-SHA-256-PLUS channel binding over TLS.
|
|
36
|
+
- TLS modes `disable`, `allow`, `prefer`, `require`, `verify-ca` and `verify-full`.
|
|
37
|
+
- Connection pools with liveness checks, idle and lifetime limits, and a fork-safe reset.
|
|
38
|
+
- Parameters are always sent separately from the SQL text. A helper module composes identifiers and literals safely.
|
|
39
|
+
- Opt-in binary result format for common types, and text format for everything else.
|
|
40
|
+
- Server-side cursors for large results, and batched `executemany`.
|
|
41
|
+
- Query hooks for logging, timing and tracing. They never receive bound parameter values.
|
|
42
|
+
- Exceptions organised by SQLSTATE class.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
pip install pgtern
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
pgtern requires Python 3.11 or newer.
|
|
51
|
+
|
|
52
|
+
## Connecting
|
|
53
|
+
|
|
54
|
+
`pgtern.connect` accepts URL and key=value connection strings, the standard `PG*` environment variables, Unix sockets, and a list of hosts for failover.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
conn = pgtern.connect("postgresql://app@db1.internal,db2.internal/app?sslmode=verify-full&sslrootcert=/etc/ssl/ca.pem")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
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.
|
|
61
|
+
|
|
62
|
+
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.
|
|
63
|
+
|
|
64
|
+
Other supported options:
|
|
65
|
+
|
|
66
|
+
- `target_session_attrs`: `read-write`, `read-only`, `primary`, `standby` or `prefer-standby`.
|
|
67
|
+
- `hostaddr`: connect to an address while keeping the host name for certificate checks.
|
|
68
|
+
- `sslpassword` and `sslcrl`.
|
|
69
|
+
- `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.
|
|
70
|
+
|
|
71
|
+
## Queries and transactions
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
with pgtern.connect(dsn) as conn:
|
|
75
|
+
conn.execute("CREATE TABLE IF NOT EXISTS accounts (id int PRIMARY KEY, balance numeric)")
|
|
76
|
+
|
|
77
|
+
with conn.transaction(): # commits on success, rolls back on error
|
|
78
|
+
conn.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", [10, 1])
|
|
79
|
+
conn.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", [10, 2])
|
|
80
|
+
|
|
81
|
+
cur = conn.execute("SELECT id, balance FROM accounts WHERE balance > %s", [0])
|
|
82
|
+
for row in cur:
|
|
83
|
+
print(row)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Placeholders are `%s`. Values are sent as parameters, never interpolated into the SQL text. To build identifiers and literals safely, use the `sql` module:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from pgtern import sql
|
|
90
|
+
|
|
91
|
+
with pgtern.connect(dsn) as conn:
|
|
92
|
+
query = sql.SQL("SELECT * FROM {} WHERE id = %s").format(sql.Identifier("accounts"))
|
|
93
|
+
conn.execute(query, [1])
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Row formats
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
## Bulk loading
|
|
101
|
+
|
|
102
|
+
`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.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
with pgtern.connect(dsn, autocommit=True) as conn:
|
|
106
|
+
conn.cursor().executemany("INSERT INTO events (kind, payload) VALUES (%s, %s)", rows)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
For the largest loads, use `COPY`:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
with pgtern.connect(dsn) as conn:
|
|
113
|
+
with conn.cursor().copy("COPY events (kind, payload) FROM STDIN") as copy:
|
|
114
|
+
for kind, payload in rows:
|
|
115
|
+
copy.write_row((kind, payload))
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Export uses `COPY ... TO STDOUT`, read with `for chunk in copy` or `copy.rows()`.
|
|
119
|
+
|
|
120
|
+
## Streaming large results
|
|
121
|
+
|
|
122
|
+
A named cursor runs on the server and fetches rows in batches, so a result never has to fit in memory:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
with pgtern.connect(dsn) as conn:
|
|
126
|
+
with conn.transaction():
|
|
127
|
+
with conn.cursor("big_report") as cur:
|
|
128
|
+
cur.execute("SELECT * FROM events")
|
|
129
|
+
cur.itersize = 1000
|
|
130
|
+
for row in cur:
|
|
131
|
+
process(row)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Server-side cursors support `scrollable`, `withhold` and `scroll()`.
|
|
135
|
+
|
|
136
|
+
## Notifications
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
with pgtern.connect(dsn) as conn:
|
|
140
|
+
conn.execute("LISTEN orders")
|
|
141
|
+
for note in conn.notifies(timeout=30):
|
|
142
|
+
print(note.channel, note.payload)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
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.
|
|
146
|
+
|
|
147
|
+
## Pipelines
|
|
148
|
+
|
|
149
|
+
Pipeline mode sends many statements before reading any reply:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
with pgtern.connect(dsn) as conn:
|
|
153
|
+
with conn.pipeline() as pipe:
|
|
154
|
+
a = pipe.execute("INSERT INTO queue (job) VALUES (%s)", ["a"])
|
|
155
|
+
b = pipe.execute("SELECT count(*) FROM queue")
|
|
156
|
+
print(b.fetchone()) # results are ready once the block ends
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
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.
|
|
160
|
+
|
|
161
|
+
## Values and types
|
|
162
|
+
|
|
163
|
+
- 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`.
|
|
164
|
+
- `timestamptz` values are returned in UTC, in both text and binary format, with the same `tzinfo`.
|
|
165
|
+
- Intervals with months or years decode to `pgtern.Interval(months, delta)`, because a month has no fixed length. Other intervals decode to `timedelta`.
|
|
166
|
+
- `time` values of `24:00:00` raise `DataError`, because Python has no such time.
|
|
167
|
+
- 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.
|
|
168
|
+
|
|
169
|
+
Register loaders for PostgreSQL types and dumpers for Python classes:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from enum import Enum
|
|
173
|
+
from pgtern import adapters
|
|
174
|
+
|
|
175
|
+
class Mood(Enum):
|
|
176
|
+
SAD = "sad"
|
|
177
|
+
HAPPY = "happy"
|
|
178
|
+
|
|
179
|
+
adapters.register_loader("mood", adapters.enum_loader(Mood)) # CREATE TYPE mood AS ENUM (...)
|
|
180
|
+
adapters.register_loader("span", adapters.range_loader(int)) # CREATE TYPE span AS RANGE (subtype = int4)
|
|
181
|
+
adapters.register_dumper(Mood, lambda m: m.value)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
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.
|
|
185
|
+
|
|
186
|
+
## Binary format
|
|
187
|
+
|
|
188
|
+
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.
|
|
189
|
+
|
|
190
|
+
## Pools
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from pgtern.pool import ConnectionPool
|
|
194
|
+
|
|
195
|
+
pool = ConnectionPool(dsn, min_size=4, max_size=20, timeout=5)
|
|
196
|
+
with pool.connection() as conn:
|
|
197
|
+
conn.execute("SELECT 1")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
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`.
|
|
201
|
+
|
|
202
|
+
## Forking
|
|
203
|
+
|
|
204
|
+
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.
|
|
205
|
+
|
|
206
|
+
## Query hooks
|
|
207
|
+
|
|
208
|
+
A hook sees every statement, its row count and the connection target. It never sees bound parameter values:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from contextlib import contextmanager
|
|
212
|
+
import time
|
|
213
|
+
import pgtern
|
|
214
|
+
|
|
215
|
+
@contextmanager
|
|
216
|
+
def timing(info):
|
|
217
|
+
started = time.perf_counter()
|
|
218
|
+
try:
|
|
219
|
+
yield
|
|
220
|
+
finally:
|
|
221
|
+
print(f"{info.statement!r} took {time.perf_counter() - started:.3f}s ({info.rowcount} rows)")
|
|
222
|
+
|
|
223
|
+
remove = pgtern.add_query_hook(timing)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
A hook that fails on entry or exit is logged and ignored, so it never changes the result of a statement.
|
|
227
|
+
|
|
228
|
+
## Limits and safety
|
|
229
|
+
|
|
230
|
+
- `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.
|
|
231
|
+
- Using a connection from inside a `COPY` block, or from inside a notice or notify handler, raises `ProgrammingError` at once.
|
|
232
|
+
- A password is not sent in plaintext unless you choose `sslmode=disable`, so a server that declines TLS under `prefer` cannot collect it.
|
|
233
|
+
- Over TLS, the login is bound to that connection with SCRAM-SHA-256-PLUS, so a login relayed through another connection fails.
|
|
234
|
+
- `verify-ca` and `verify-full` require a root certificate (`sslrootcert`). They fail rather than fall back to the system store.
|
|
235
|
+
|
|
236
|
+
## Errors
|
|
237
|
+
|
|
238
|
+
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`:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
with pgtern.connect(dsn) as conn:
|
|
242
|
+
try:
|
|
243
|
+
conn.execute("INSERT INTO accounts (id) VALUES (%s)", [1])
|
|
244
|
+
except pgtern.IntegrityError:
|
|
245
|
+
conn.rollback() # a failed transaction must be rolled back before the block ends
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Compatibility
|
|
249
|
+
|
|
250
|
+
- **PostgreSQL:** 14, 15, 16, 17 and 18.
|
|
251
|
+
- **Python:** 3.11, 3.12 and 3.13 are tested in continuous integration.
|
|
252
|
+
- **Platforms:** anywhere CPython runs.
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
pip install -e ".[dev]"
|
|
258
|
+
eval "$(scripts/start-test-postgres.sh)" # throwaway server with SCRAM, MD5, cleartext, trust and TLS users
|
|
259
|
+
pytest
|
|
260
|
+
scripts/start-test-postgres.sh stop
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
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.
|
|
264
|
+
|
|
265
|
+
## Contributing
|
|
266
|
+
|
|
267
|
+
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).
|
|
268
|
+
|
|
269
|
+
## Changelog
|
|
270
|
+
|
|
271
|
+
Release history is in [CHANGELOG.md](https://github.com/karawash-projects/pgtern/blob/main/CHANGELOG.md).
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
Apache License 2.0. See [LICENSE](https://github.com/karawash-projects/pgtern/blob/main/LICENSE).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pgtern"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "A zero-dependency, pure-Python PostgreSQL client with blocking and asyncio APIs."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -13,7 +13,7 @@ license-files = ["LICENSE"]
|
|
|
13
13
|
authors = [{ name = "Ahmad Karawash" }]
|
|
14
14
|
keywords = ["postgresql", "postgres", "database", "driver", "dbapi", "scram", "pure-python", "zero-dependency"]
|
|
15
15
|
classifiers = [
|
|
16
|
-
"Development Status ::
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
17
|
"Intended Audience :: Developers",
|
|
18
18
|
"Operating System :: OS Independent",
|
|
19
19
|
"Programming Language :: Python :: 3",
|
|
@@ -28,7 +28,7 @@ classifiers = [
|
|
|
28
28
|
dependencies = []
|
|
29
29
|
|
|
30
30
|
[project.optional-dependencies]
|
|
31
|
-
dev = ["pytest>=8", "build>=1.2"]
|
|
31
|
+
dev = ["pytest>=8", "pytest-timeout>=2", "build>=1.2", "pyflakes>=3", "mypy>=1.10"]
|
|
32
32
|
|
|
33
33
|
[tool.setuptools.packages.find]
|
|
34
34
|
where = ["src"]
|
|
@@ -16,6 +16,7 @@ from . import adapters, aio, errors, hooks, rows, sql, types
|
|
|
16
16
|
from ._conn import Connection, ConnectionInfo, Transaction, TransactionStatus
|
|
17
17
|
from ._cursor import Cursor, ServerCursor
|
|
18
18
|
from ._protocol import Notify
|
|
19
|
+
from ._types import Interval
|
|
19
20
|
from .errors import (
|
|
20
21
|
DatabaseError, DataError, Error, IntegrityError, InterfaceError, InternalError,
|
|
21
22
|
NotSupportedError, OperationalError, ProgrammingError, Warning,
|
|
@@ -23,7 +24,7 @@ from .errors import (
|
|
|
23
24
|
from .hooks import QueryInfo, add_query_hook
|
|
24
25
|
from .pool import ConnectionPool, PoolClosed, PoolTimeout
|
|
25
26
|
|
|
26
|
-
__version__ = "0.
|
|
27
|
+
__version__ = "0.4.0"
|
|
27
28
|
|
|
28
29
|
# DB-API 2.0 module globals
|
|
29
30
|
apilevel = "2.0"
|
|
@@ -38,7 +39,7 @@ def connect(conninfo: str = "", **kwargs: Any) -> Connection:
|
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
__all__ = [
|
|
41
|
-
"connect", "Connection", "ConnectionInfo", "ConnectionPool", "Cursor", "ServerCursor", "Notify", "Transaction",
|
|
42
|
+
"connect", "Connection", "ConnectionInfo", "ConnectionPool", "Cursor", "ServerCursor", "Notify", "Interval", "Transaction",
|
|
42
43
|
"TransactionStatus", "PoolClosed", "PoolTimeout", "QueryInfo", "add_query_hook",
|
|
43
44
|
"Warning", "Error", "InterfaceError", "DatabaseError", "DataError", "OperationalError",
|
|
44
45
|
"IntegrityError", "InternalError", "ProgrammingError", "NotSupportedError",
|