connpool 1.0.2__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.
- connpool-1.0.2/LICENSE +21 -0
- connpool-1.0.2/PKG-INFO +157 -0
- connpool-1.0.2/README.md +127 -0
- connpool-1.0.2/pyproject.toml +66 -0
- connpool-1.0.2/setup.cfg +4 -0
- connpool-1.0.2/src/connpool/__init__.py +46 -0
- connpool-1.0.2/src/connpool/core/__init__.py +14 -0
- connpool-1.0.2/src/connpool/core/clock.py +45 -0
- connpool-1.0.2/src/connpool/core/connection.py +62 -0
- connpool-1.0.2/src/connpool/core/errors.py +41 -0
- connpool-1.0.2/src/connpool/core/timeouts.py +25 -0
- connpool-1.0.2/src/connpool/factories/__init__.py +6 -0
- connpool-1.0.2/src/connpool/factories/async_factory.py +68 -0
- connpool-1.0.2/src/connpool/factories/sync_factory.py +72 -0
- connpool-1.0.2/src/connpool/observability/__init__.py +5 -0
- connpool-1.0.2/src/connpool/observability/events.py +36 -0
- connpool-1.0.2/src/connpool/observability/stats.py +118 -0
- connpool-1.0.2/src/connpool/policy/__init__.py +5 -0
- connpool-1.0.2/src/connpool/policy/config.py +72 -0
- connpool-1.0.2/src/connpool/policy/lifecycle.py +75 -0
- connpool-1.0.2/src/connpool/pools/__init__.py +6 -0
- connpool-1.0.2/src/connpool/pools/async_pool.py +484 -0
- connpool-1.0.2/src/connpool/pools/registry.py +107 -0
- connpool-1.0.2/src/connpool/pools/sync_pool.py +466 -0
- connpool-1.0.2/src/connpool/py.typed +0 -0
- connpool-1.0.2/src/connpool/resilience/__init__.py +5 -0
- connpool-1.0.2/src/connpool/resilience/breaker.py +161 -0
- connpool-1.0.2/src/connpool/resilience/retry.py +44 -0
- connpool-1.0.2/src/connpool.egg-info/PKG-INFO +157 -0
- connpool-1.0.2/src/connpool.egg-info/SOURCES.txt +31 -0
- connpool-1.0.2/src/connpool.egg-info/dependency_links.txt +1 -0
- connpool-1.0.2/src/connpool.egg-info/requires.txt +5 -0
- connpool-1.0.2/src/connpool.egg-info/top_level.txt +1 -0
connpool-1.0.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mikhail Ilyushin (Jeffeek)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
connpool-1.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: connpool
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Generic connection pool for Python (sync + asyncio), standard library only
|
|
5
|
+
Author: Mikhail Ilyushin
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Jeffeek/poolyglot/python
|
|
8
|
+
Project-URL: Repository, https://github.com/Jeffeek/poolyglot
|
|
9
|
+
Project-URL: Issues, https://github.com/Jeffeek/poolyglot/issues
|
|
10
|
+
Keywords: connection pool,pool,asyncio,database,resource,circuit breaker
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Database
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
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: coverage>=7; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# connpool
|
|
32
|
+
|
|
33
|
+
A generic connection pool for Python 3.11+ — sync (threads) and `asyncio`. **Standard library only**
|
|
34
|
+
(no third-party runtime dependencies). It pools *anything*: DB drivers, HTTP sessions, SSH clients, sockets…
|
|
35
|
+
because the pool knows nothing about connections except what a small `ConnectionFactory` tells it.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from connpool import ConnectionPool, PoolConfig
|
|
39
|
+
|
|
40
|
+
pool = ConnectionPool.from_callables(make_conn, validate=is_alive, config=PoolConfig(max_size=20))
|
|
41
|
+
with pool: # optional: warm-up + background sweeper
|
|
42
|
+
with pool.connection() as conn: # acquire -> use -> release (errors handled)
|
|
43
|
+
conn.do_something()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Project layout
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
src/connpool/ the library (imports: standard library only)
|
|
50
|
+
core/ errors, clocks, PooledConnection/CloseReason, timeout helpers
|
|
51
|
+
resilience/ breaker.py (circuit breaker), retry.py (backoff policy)
|
|
52
|
+
policy/ config.py (PoolConfig), lifecycle.py (pure "when must a connection retire?" rules)
|
|
53
|
+
factories/ sync_factory.py, async_factory.py <- what YOU implement to pool anything
|
|
54
|
+
observability/ events.py (PoolListener), stats.py (metrics)
|
|
55
|
+
pools/ sync_pool.py, async_pool.py, registry.py (pool-per-key)
|
|
56
|
+
tests/ mirrors src/ (unittest, no dependencies)
|
|
57
|
+
support/ fake factories and helpers
|
|
58
|
+
examples/ client code - NOT part of the library
|
|
59
|
+
database/ generic DB-API factory (any driver), sqlite3 demo (runnable), psycopg demo
|
|
60
|
+
http_client/ ssh/ async_usage/
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Dependency direction is strictly downward (a layer only imports from layers listed above it):
|
|
64
|
+
`core → resilience → policy → factories/observability → pools`. The top-level `connpool/__init__.py` is a
|
|
65
|
+
facade re-exporting the public API, so `from connpool import ConnectionPool` keeps working even if internal
|
|
66
|
+
modules move.
|
|
67
|
+
|
|
68
|
+
## What you get
|
|
69
|
+
|
|
70
|
+
| Concern | How it works |
|
|
71
|
+
|---|---|
|
|
72
|
+
| Sizing | `min_size` kept warm, `max_size` is a *hard* cap (slot reserved before create) |
|
|
73
|
+
| Timeouts | `acquire_timeout` (per-call override), `create_timeout` (async), retries never sleep past the deadline |
|
|
74
|
+
| Rotation | `max_lifetime` (with jitter), `max_uses`, `max_idle_time`, `LIFO`/`FIFO` reuse |
|
|
75
|
+
| Health | validate on checkout if idle ≥ `validate_after_idle`; `reset()` on release; after an exception: fatal → discard, otherwise re-validate; optional `validate_idle` pings idle connections in the background sweep and reaps dead ones |
|
|
76
|
+
| Retries | exponential backoff + jitter, `retry_on` filter |
|
|
77
|
+
| Circuit breaker | closed → open → half-open, probe limits, state-change events; open = fail fast. Counts **one failure per logical creation** (a whole retry sequence), not per attempt. A create failure *or* a `create_timeout` (slow backend) counts; an acquire-timeout or cancellation mid-create does not |
|
|
78
|
+
| Backpressure | `max_waiters` caps the blocked-caller queue; excess callers fail fast with `PoolExhaustedError` |
|
|
79
|
+
| Leak detection | `leak_detection_timeout` warns (via log + `on_connection_leak`) when a borrowed connection is held too long; warn-only, never force-reclaimed |
|
|
80
|
+
| Observability | `PoolListener` hooks + `pool.stats()` snapshot (counters, **avg + p50/p95/p99** for both acquire-wait and create-latency over a recent window) |
|
|
81
|
+
| Shutdown | idempotent `close()`, wakes blocked callers, in-use connections close on release; `close(timeout=)` drains gracefully; a `ResourceWarning` fires if a pool is GC'd un-closed |
|
|
82
|
+
| Cancellation | asyncio cancellation never leaks a slot |
|
|
83
|
+
| Many backends | `PoolRegistry` / `AsyncPoolRegistry` = one lazily-created pool per key (host, tenant, DB) |
|
|
84
|
+
|
|
85
|
+
## Writing a factory
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
class MyFactory(ConnectionFactory[MyConn]):
|
|
89
|
+
def create(self): return MyConn.connect(...) # enforce your own connect timeout
|
|
90
|
+
def validate(self, c): return c.ping() # cheap liveness check
|
|
91
|
+
def reset(self, c): c.rollback() # scrub session state on release
|
|
92
|
+
def close(self, c): c.close() # default: calls c.close()
|
|
93
|
+
def is_fatal_error(self, e): return isinstance(e, ConnectionError)
|
|
94
|
+
```
|
|
95
|
+
Async: subclass `AsyncConnectionFactory`, or pass sync/async callables to `AsyncConnectionPool.from_callables`.
|
|
96
|
+
`examples/database/dbapi_factory.py` shows one factory that covers every PEP 249 database driver.
|
|
97
|
+
|
|
98
|
+
## Configuration (`PoolConfig`)
|
|
99
|
+
|
|
100
|
+
| Field | Default | Meaning |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `min_size` / `max_size` | 0 / 10 | warm connections / hard cap |
|
|
103
|
+
| `acquire_timeout` | 30 | seconds to wait (`None` = forever, `0` = don't wait) |
|
|
104
|
+
| `max_waiters` | None | backpressure: cap callers blocked in `acquire()` (excess → `PoolExhaustedError`; `None` = unlimited, `0` = never wait when full) |
|
|
105
|
+
| `create_timeout` | 10 | async only; sync factories enforce their own |
|
|
106
|
+
| `max_lifetime` / `lifetime_jitter` | 1800 / 0.1 | retire after age, shaved by up to 10% |
|
|
107
|
+
| `max_idle_time` | 600 | evict idle connections (never below `min_size`) |
|
|
108
|
+
| `max_uses` | None | retire after N checkouts |
|
|
109
|
+
| `idle_strategy` | LIFO | `FIFO` spreads load evenly |
|
|
110
|
+
| `validate_after_idle` | 30 | validate on checkout if idle this long (0 = always, None = never) |
|
|
111
|
+
| `validate_idle` | False | also validate idle connections during the background sweep and reap dead ones (needs `open()`; adds I/O) |
|
|
112
|
+
| `maintenance_interval` | 30 | background sweep period (None = off) |
|
|
113
|
+
| `leak_detection_timeout` | None | warn when a borrowed connection is held longer than this (needs `open()`; checked once per `maintenance_interval` sweep; None = off) |
|
|
114
|
+
| `retry` | 3 attempts, 0.1s base, ×2, 5s cap | `RetryPolicy` (`NO_RETRY` disables) |
|
|
115
|
+
| `breaker` | 5 failures / 30s recovery | `BreakerConfig` (`None` disables); `failure_threshold` counts failed *creations*, not retry attempts |
|
|
116
|
+
|
|
117
|
+
## Install, test, run examples
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
pip install -e . # or: export PYTHONPATH=src
|
|
121
|
+
pip install -e ".[dev]" # + coverage/pytest/mypy
|
|
122
|
+
python -m unittest discover -s tests -t . # 142 tests, ~2s
|
|
123
|
+
./run_tests.sh # same, with a compact coverage report (gate: 100%)
|
|
124
|
+
mypy # strict type check (config in pyproject.toml)
|
|
125
|
+
python examples/database/sqlite_demo.py
|
|
126
|
+
```
|
|
127
|
+
(`pytest` works too if installed; configuration is in `pyproject.toml`.)
|
|
128
|
+
Examples importing `paramiko` or `psycopg` need those packages; the library never does.
|
|
129
|
+
|
|
130
|
+
## Limits / extension points
|
|
131
|
+
|
|
132
|
+
* The breaker guards connection *creation*. To also trip on request failures wrap calls with `CircuitBreaker.call()`.
|
|
133
|
+
* The breaker counts consecutive failed creations (one per logical acquire, retries included). A real backend failure or a `create_timeout` firing (the backend is too slow to connect — a genuine fault) both count; an acquire-timeout (caller impatience) or cancellation mid-create does not. A rate-based window is a drop-in replacement of one class.
|
|
134
|
+
* Leak detection is **warn-only** by design — the pool never yanks a connection a caller may still be using. It also only runs while the pool is `open()` (the maintenance sweep drives it), so registry pools need an explicit `open()`, and a warning can lag the real leak by up to one `maintenance_interval`.
|
|
135
|
+
* **Not fork-safe.** A pool `open()`ed before `os.fork()` (e.g. a pre-forking server like gunicorn with `preload`, or `multiprocessing`) shares live connections/file descriptors with the children — close the pool before forking and create a fresh one per process (`max_size` is per-process).
|
|
136
|
+
* Pool size is fixed at construction (`PoolConfig` is immutable); there is no runtime resize. Use a new pool, or a `PoolRegistry` keyed by profile, if you need different caps at runtime.
|
|
137
|
+
* Wake-up order of blocked threads is not strictly FIFO.
|
|
138
|
+
* Sync `create()` can't be interrupted by the pool; give your client a connect timeout.
|
|
139
|
+
* Listener hooks are synchronous; push heavy work to a queue.
|
|
140
|
+
* A factory's `create()` must return a **distinct** object each call; returning a shared/singleton connection is rejected with `ConnectionCreateError` (the pool tracks borrow state by object identity).
|
|
141
|
+
* `acquire_wait` percentiles are estimated over the last 1024 samples (bounded memory), not the full history.
|
|
142
|
+
* With `validate_idle` on, idle connections being pinged by the background sweep are briefly reserved and count as `in_use` (not `idle`) in `stats()` for the duration of that check — they are genuinely unavailable while validated, not borrowed by a caller.
|
|
143
|
+
|
|
144
|
+
## Considered, not implemented
|
|
145
|
+
|
|
146
|
+
These are deliberate scope decisions, not oversights. For most, something already in the
|
|
147
|
+
library covers the real need; adding them would make the pool heavier for every user to serve
|
|
148
|
+
a minority. Listed here so the trade-off is explicit and the upgrade path is known.
|
|
149
|
+
|
|
150
|
+
| Feature | How it would be implemented | Why not (and what covers it today) |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| **Runtime resize** | Make `max_size`/`min_size` mutable behind the lock; on grow, raise the cap and `notify_all` waiters; on shrink, mark excess idle connections for disposal and let in-use ones close on release (never force-closed). | Erodes the immutable-`PoolConfig` invariant (predictability is a feature); shrink is fiddly. Use a fresh pool or a `PoolRegistry` keyed by profile — and `min_size`↔`max_size` already flexes with load. |
|
|
153
|
+
| **Adaptive sizing** | A control loop that tunes `max_size` from live metrics (saturation, acquire latency), with hysteresis to avoid oscillation. | Genuinely hard to do well and easy to make worse than static. `min_size` + `max_size` + idle eviction is already a simple, deterministic adaptive scheme (grow to max under load, shrink to min when quiet). |
|
|
154
|
+
| **Priority acquisition** | Replace the single `Condition` with a priority heap of per-waiter futures; `release()` hands the connection to the highest-priority waiter; timeouts/cancellation remove waiters from the heap. | Ordering ≠ isolation, and it complicates the hot path. Prefer **separate pools** (a `PoolRegistry` acts as bulkheads): give batch its own small pool and interactive its own. |
|
|
155
|
+
| **Create-rate limiting** | A token-bucket / min-interval gate in the create path, checked after reserving a slot and bounded by `acquire_timeout`. | Connection storms are already bounded by `max_size` (caps concurrent creates) and the circuit breaker (halts storms after failures). Add only if a backend imposes a literal connect/sec quota — this is the cheapest of these to add if ever needed. |
|
|
156
|
+
| **Lease handles (use-after-release protection)** | Return a proxy wrapping the raw connection, forwarding attribute access and invalidating itself on release. | A Python proxy is bypassable (the underlying object is always reachable), so it is a guardrail, not a guarantee, and it taxes every call. The context manager + identity tracking already give deterministic release and detect double/foreign release. |
|
|
157
|
+
| **Broken-connection quarantine** | Move suspect connections to a side store with a reason/timestamp and a debug API, instead of closing them. | Quarantine's only real value is forensic; in steady state you *want* broken connections closed — hoarding dead file descriptors is a resource leak by design (HikariCP/asyncpg discard too). Use the `on_connection_closed(conn, reason)` hook to snapshot metadata at close time if you need diagnostics. |
|
connpool-1.0.2/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# connpool
|
|
2
|
+
|
|
3
|
+
A generic connection pool for Python 3.11+ — sync (threads) and `asyncio`. **Standard library only**
|
|
4
|
+
(no third-party runtime dependencies). It pools *anything*: DB drivers, HTTP sessions, SSH clients, sockets…
|
|
5
|
+
because the pool knows nothing about connections except what a small `ConnectionFactory` tells it.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from connpool import ConnectionPool, PoolConfig
|
|
9
|
+
|
|
10
|
+
pool = ConnectionPool.from_callables(make_conn, validate=is_alive, config=PoolConfig(max_size=20))
|
|
11
|
+
with pool: # optional: warm-up + background sweeper
|
|
12
|
+
with pool.connection() as conn: # acquire -> use -> release (errors handled)
|
|
13
|
+
conn.do_something()
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Project layout
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
src/connpool/ the library (imports: standard library only)
|
|
20
|
+
core/ errors, clocks, PooledConnection/CloseReason, timeout helpers
|
|
21
|
+
resilience/ breaker.py (circuit breaker), retry.py (backoff policy)
|
|
22
|
+
policy/ config.py (PoolConfig), lifecycle.py (pure "when must a connection retire?" rules)
|
|
23
|
+
factories/ sync_factory.py, async_factory.py <- what YOU implement to pool anything
|
|
24
|
+
observability/ events.py (PoolListener), stats.py (metrics)
|
|
25
|
+
pools/ sync_pool.py, async_pool.py, registry.py (pool-per-key)
|
|
26
|
+
tests/ mirrors src/ (unittest, no dependencies)
|
|
27
|
+
support/ fake factories and helpers
|
|
28
|
+
examples/ client code - NOT part of the library
|
|
29
|
+
database/ generic DB-API factory (any driver), sqlite3 demo (runnable), psycopg demo
|
|
30
|
+
http_client/ ssh/ async_usage/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Dependency direction is strictly downward (a layer only imports from layers listed above it):
|
|
34
|
+
`core → resilience → policy → factories/observability → pools`. The top-level `connpool/__init__.py` is a
|
|
35
|
+
facade re-exporting the public API, so `from connpool import ConnectionPool` keeps working even if internal
|
|
36
|
+
modules move.
|
|
37
|
+
|
|
38
|
+
## What you get
|
|
39
|
+
|
|
40
|
+
| Concern | How it works |
|
|
41
|
+
|---|---|
|
|
42
|
+
| Sizing | `min_size` kept warm, `max_size` is a *hard* cap (slot reserved before create) |
|
|
43
|
+
| Timeouts | `acquire_timeout` (per-call override), `create_timeout` (async), retries never sleep past the deadline |
|
|
44
|
+
| Rotation | `max_lifetime` (with jitter), `max_uses`, `max_idle_time`, `LIFO`/`FIFO` reuse |
|
|
45
|
+
| Health | validate on checkout if idle ≥ `validate_after_idle`; `reset()` on release; after an exception: fatal → discard, otherwise re-validate; optional `validate_idle` pings idle connections in the background sweep and reaps dead ones |
|
|
46
|
+
| Retries | exponential backoff + jitter, `retry_on` filter |
|
|
47
|
+
| Circuit breaker | closed → open → half-open, probe limits, state-change events; open = fail fast. Counts **one failure per logical creation** (a whole retry sequence), not per attempt. A create failure *or* a `create_timeout` (slow backend) counts; an acquire-timeout or cancellation mid-create does not |
|
|
48
|
+
| Backpressure | `max_waiters` caps the blocked-caller queue; excess callers fail fast with `PoolExhaustedError` |
|
|
49
|
+
| Leak detection | `leak_detection_timeout` warns (via log + `on_connection_leak`) when a borrowed connection is held too long; warn-only, never force-reclaimed |
|
|
50
|
+
| Observability | `PoolListener` hooks + `pool.stats()` snapshot (counters, **avg + p50/p95/p99** for both acquire-wait and create-latency over a recent window) |
|
|
51
|
+
| Shutdown | idempotent `close()`, wakes blocked callers, in-use connections close on release; `close(timeout=)` drains gracefully; a `ResourceWarning` fires if a pool is GC'd un-closed |
|
|
52
|
+
| Cancellation | asyncio cancellation never leaks a slot |
|
|
53
|
+
| Many backends | `PoolRegistry` / `AsyncPoolRegistry` = one lazily-created pool per key (host, tenant, DB) |
|
|
54
|
+
|
|
55
|
+
## Writing a factory
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
class MyFactory(ConnectionFactory[MyConn]):
|
|
59
|
+
def create(self): return MyConn.connect(...) # enforce your own connect timeout
|
|
60
|
+
def validate(self, c): return c.ping() # cheap liveness check
|
|
61
|
+
def reset(self, c): c.rollback() # scrub session state on release
|
|
62
|
+
def close(self, c): c.close() # default: calls c.close()
|
|
63
|
+
def is_fatal_error(self, e): return isinstance(e, ConnectionError)
|
|
64
|
+
```
|
|
65
|
+
Async: subclass `AsyncConnectionFactory`, or pass sync/async callables to `AsyncConnectionPool.from_callables`.
|
|
66
|
+
`examples/database/dbapi_factory.py` shows one factory that covers every PEP 249 database driver.
|
|
67
|
+
|
|
68
|
+
## Configuration (`PoolConfig`)
|
|
69
|
+
|
|
70
|
+
| Field | Default | Meaning |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `min_size` / `max_size` | 0 / 10 | warm connections / hard cap |
|
|
73
|
+
| `acquire_timeout` | 30 | seconds to wait (`None` = forever, `0` = don't wait) |
|
|
74
|
+
| `max_waiters` | None | backpressure: cap callers blocked in `acquire()` (excess → `PoolExhaustedError`; `None` = unlimited, `0` = never wait when full) |
|
|
75
|
+
| `create_timeout` | 10 | async only; sync factories enforce their own |
|
|
76
|
+
| `max_lifetime` / `lifetime_jitter` | 1800 / 0.1 | retire after age, shaved by up to 10% |
|
|
77
|
+
| `max_idle_time` | 600 | evict idle connections (never below `min_size`) |
|
|
78
|
+
| `max_uses` | None | retire after N checkouts |
|
|
79
|
+
| `idle_strategy` | LIFO | `FIFO` spreads load evenly |
|
|
80
|
+
| `validate_after_idle` | 30 | validate on checkout if idle this long (0 = always, None = never) |
|
|
81
|
+
| `validate_idle` | False | also validate idle connections during the background sweep and reap dead ones (needs `open()`; adds I/O) |
|
|
82
|
+
| `maintenance_interval` | 30 | background sweep period (None = off) |
|
|
83
|
+
| `leak_detection_timeout` | None | warn when a borrowed connection is held longer than this (needs `open()`; checked once per `maintenance_interval` sweep; None = off) |
|
|
84
|
+
| `retry` | 3 attempts, 0.1s base, ×2, 5s cap | `RetryPolicy` (`NO_RETRY` disables) |
|
|
85
|
+
| `breaker` | 5 failures / 30s recovery | `BreakerConfig` (`None` disables); `failure_threshold` counts failed *creations*, not retry attempts |
|
|
86
|
+
|
|
87
|
+
## Install, test, run examples
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
pip install -e . # or: export PYTHONPATH=src
|
|
91
|
+
pip install -e ".[dev]" # + coverage/pytest/mypy
|
|
92
|
+
python -m unittest discover -s tests -t . # 142 tests, ~2s
|
|
93
|
+
./run_tests.sh # same, with a compact coverage report (gate: 100%)
|
|
94
|
+
mypy # strict type check (config in pyproject.toml)
|
|
95
|
+
python examples/database/sqlite_demo.py
|
|
96
|
+
```
|
|
97
|
+
(`pytest` works too if installed; configuration is in `pyproject.toml`.)
|
|
98
|
+
Examples importing `paramiko` or `psycopg` need those packages; the library never does.
|
|
99
|
+
|
|
100
|
+
## Limits / extension points
|
|
101
|
+
|
|
102
|
+
* The breaker guards connection *creation*. To also trip on request failures wrap calls with `CircuitBreaker.call()`.
|
|
103
|
+
* The breaker counts consecutive failed creations (one per logical acquire, retries included). A real backend failure or a `create_timeout` firing (the backend is too slow to connect — a genuine fault) both count; an acquire-timeout (caller impatience) or cancellation mid-create does not. A rate-based window is a drop-in replacement of one class.
|
|
104
|
+
* Leak detection is **warn-only** by design — the pool never yanks a connection a caller may still be using. It also only runs while the pool is `open()` (the maintenance sweep drives it), so registry pools need an explicit `open()`, and a warning can lag the real leak by up to one `maintenance_interval`.
|
|
105
|
+
* **Not fork-safe.** A pool `open()`ed before `os.fork()` (e.g. a pre-forking server like gunicorn with `preload`, or `multiprocessing`) shares live connections/file descriptors with the children — close the pool before forking and create a fresh one per process (`max_size` is per-process).
|
|
106
|
+
* Pool size is fixed at construction (`PoolConfig` is immutable); there is no runtime resize. Use a new pool, or a `PoolRegistry` keyed by profile, if you need different caps at runtime.
|
|
107
|
+
* Wake-up order of blocked threads is not strictly FIFO.
|
|
108
|
+
* Sync `create()` can't be interrupted by the pool; give your client a connect timeout.
|
|
109
|
+
* Listener hooks are synchronous; push heavy work to a queue.
|
|
110
|
+
* A factory's `create()` must return a **distinct** object each call; returning a shared/singleton connection is rejected with `ConnectionCreateError` (the pool tracks borrow state by object identity).
|
|
111
|
+
* `acquire_wait` percentiles are estimated over the last 1024 samples (bounded memory), not the full history.
|
|
112
|
+
* With `validate_idle` on, idle connections being pinged by the background sweep are briefly reserved and count as `in_use` (not `idle`) in `stats()` for the duration of that check — they are genuinely unavailable while validated, not borrowed by a caller.
|
|
113
|
+
|
|
114
|
+
## Considered, not implemented
|
|
115
|
+
|
|
116
|
+
These are deliberate scope decisions, not oversights. For most, something already in the
|
|
117
|
+
library covers the real need; adding them would make the pool heavier for every user to serve
|
|
118
|
+
a minority. Listed here so the trade-off is explicit and the upgrade path is known.
|
|
119
|
+
|
|
120
|
+
| Feature | How it would be implemented | Why not (and what covers it today) |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| **Runtime resize** | Make `max_size`/`min_size` mutable behind the lock; on grow, raise the cap and `notify_all` waiters; on shrink, mark excess idle connections for disposal and let in-use ones close on release (never force-closed). | Erodes the immutable-`PoolConfig` invariant (predictability is a feature); shrink is fiddly. Use a fresh pool or a `PoolRegistry` keyed by profile — and `min_size`↔`max_size` already flexes with load. |
|
|
123
|
+
| **Adaptive sizing** | A control loop that tunes `max_size` from live metrics (saturation, acquire latency), with hysteresis to avoid oscillation. | Genuinely hard to do well and easy to make worse than static. `min_size` + `max_size` + idle eviction is already a simple, deterministic adaptive scheme (grow to max under load, shrink to min when quiet). |
|
|
124
|
+
| **Priority acquisition** | Replace the single `Condition` with a priority heap of per-waiter futures; `release()` hands the connection to the highest-priority waiter; timeouts/cancellation remove waiters from the heap. | Ordering ≠ isolation, and it complicates the hot path. Prefer **separate pools** (a `PoolRegistry` acts as bulkheads): give batch its own small pool and interactive its own. |
|
|
125
|
+
| **Create-rate limiting** | A token-bucket / min-interval gate in the create path, checked after reserving a slot and bounded by `acquire_timeout`. | Connection storms are already bounded by `max_size` (caps concurrent creates) and the circuit breaker (halts storms after failures). Add only if a backend imposes a literal connect/sec quota — this is the cheapest of these to add if ever needed. |
|
|
126
|
+
| **Lease handles (use-after-release protection)** | Return a proxy wrapping the raw connection, forwarding attribute access and invalidating itself on release. | A Python proxy is bypassable (the underlying object is always reachable), so it is a guardrail, not a guarantee, and it taxes every call. The context manager + identity tracking already give deterministic release and detect double/foreign release. |
|
|
127
|
+
| **Broken-connection quarantine** | Move suspect connections to a side store with a reason/timestamp and a debug API, instead of closing them. | Quarantine's only real value is forensic; in steady state you *want* broken connections closed — hoarding dead file descriptors is a resource leak by design (HikariCP/asyncpg discard too). Use the `on_connection_closed(conn, reason)` hook to snapshot metadata at close time if you need diagnostics. |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"] # >=77 for the SPDX `license` string form (PEP 639)
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "connpool"
|
|
7
|
+
version = "1.0.2"
|
|
8
|
+
description = "Generic connection pool for Python (sync + asyncio), standard library only"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Mikhail Ilyushin" }]
|
|
14
|
+
keywords = ["connection pool", "pool", "asyncio", "database", "resource", "circuit breaker"]
|
|
15
|
+
dependencies = [] # runtime: standard library only
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Database",
|
|
26
|
+
"Topic :: Software Development :: Libraries",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/Jeffeek/poolyglot/python"
|
|
32
|
+
Repository = "https://github.com/Jeffeek/poolyglot"
|
|
33
|
+
Issues = "https://github.com/Jeffeek/poolyglot/issues"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
# Dev tooling only - the library itself stays dependency-free.
|
|
37
|
+
# pip install -e ".[dev]"
|
|
38
|
+
dev = [
|
|
39
|
+
"coverage>=7", # used by run_tests.sh
|
|
40
|
+
"pytest>=8", # optional: `pytest` also works (see [tool.pytest.ini_options])
|
|
41
|
+
"mypy>=1.8", # static type checking (the package ships py.typed)
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.package-data]
|
|
48
|
+
connpool = ["py.typed"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options] # optional: `pytest` also works if you prefer it
|
|
51
|
+
pythonpath = ["src"]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
|
|
54
|
+
[tool.coverage.run]
|
|
55
|
+
source = ["src"]
|
|
56
|
+
|
|
57
|
+
[tool.coverage.report]
|
|
58
|
+
precision = 1
|
|
59
|
+
show_missing = true
|
|
60
|
+
# The library targets 100% line coverage; CI fails below it.
|
|
61
|
+
fail_under = 100
|
|
62
|
+
|
|
63
|
+
[tool.mypy]
|
|
64
|
+
python_version = "3.11"
|
|
65
|
+
strict = true
|
|
66
|
+
files = ["src"]
|
connpool-1.0.2/setup.cfg
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""connpool - a generic connection pool for Python (sync + asyncio), standard library only.
|
|
2
|
+
|
|
3
|
+
Layers (each only depends on the ones above it):
|
|
4
|
+
|
|
5
|
+
core errors, clocks, connection bookkeeping
|
|
6
|
+
resilience circuit breaker, retry/backoff
|
|
7
|
+
policy PoolConfig + the pure retirement rules
|
|
8
|
+
factories ConnectionFactory: the seam users implement to pool anything
|
|
9
|
+
observability listeners + stats
|
|
10
|
+
pools ConnectionPool, AsyncConnectionPool, PoolRegistry
|
|
11
|
+
|
|
12
|
+
This module is the *public API facade*: everything users need can be imported straight
|
|
13
|
+
from ``connpool`` so that internal modules can move without breaking anyone.
|
|
14
|
+
"""
|
|
15
|
+
from connpool.core import (
|
|
16
|
+
CircuitOpenError, Clock, CloseReason, ConnectionCreateError, InvalidReleaseError,
|
|
17
|
+
ManualClock, PoolClosedError, PoolConfigError, PoolError, PoolExhaustedError,
|
|
18
|
+
PoolTimeoutError, PooledConnection, SystemClock,
|
|
19
|
+
)
|
|
20
|
+
from connpool.factories import (
|
|
21
|
+
AsyncCallableFactory, AsyncConnectionFactory, CallableFactory, ConnectionFactory,
|
|
22
|
+
)
|
|
23
|
+
from connpool.observability import PoolListener, PoolStats
|
|
24
|
+
from connpool.policy import IdleStrategy, PoolConfig
|
|
25
|
+
from connpool.pools import AsyncConnectionPool, AsyncPoolRegistry, ConnectionPool, PoolRegistry
|
|
26
|
+
from connpool.resilience import (
|
|
27
|
+
NO_RETRY, BreakerConfig, BreakerState, CircuitBreaker, RetryPolicy,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__version__ = "1.0.0"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# pools
|
|
34
|
+
"AsyncConnectionPool", "AsyncPoolRegistry", "ConnectionPool", "PoolRegistry",
|
|
35
|
+
# configuration
|
|
36
|
+
"BreakerConfig", "IdleStrategy", "NO_RETRY", "PoolConfig", "RetryPolicy",
|
|
37
|
+
# what you implement
|
|
38
|
+
"AsyncCallableFactory", "AsyncConnectionFactory", "CallableFactory", "ConnectionFactory",
|
|
39
|
+
# observability
|
|
40
|
+
"BreakerState", "CloseReason", "PoolListener", "PoolStats", "PooledConnection",
|
|
41
|
+
# errors
|
|
42
|
+
"CircuitOpenError", "ConnectionCreateError", "InvalidReleaseError", "PoolClosedError",
|
|
43
|
+
"PoolConfigError", "PoolError", "PoolExhaustedError", "PoolTimeoutError",
|
|
44
|
+
# standalone building blocks / testing
|
|
45
|
+
"CircuitBreaker", "Clock", "ManualClock", "SystemClock",
|
|
46
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Foundation layer: errors, clocks, connection bookkeeping, timeout helpers.
|
|
2
|
+
Depends on nothing else in connpool."""
|
|
3
|
+
from connpool.core.clock import Clock, ManualClock, SystemClock
|
|
4
|
+
from connpool.core.connection import CloseReason, PooledConnection
|
|
5
|
+
from connpool.core.errors import (
|
|
6
|
+
CircuitOpenError, ConnectionCreateError, InvalidReleaseError, PoolClosedError,
|
|
7
|
+
PoolConfigError, PoolError, PoolExhaustedError, PoolTimeoutError,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CircuitOpenError", "Clock", "CloseReason", "ConnectionCreateError", "InvalidReleaseError",
|
|
12
|
+
"ManualClock", "PoolClosedError", "PoolConfigError", "PoolError", "PoolExhaustedError",
|
|
13
|
+
"PoolTimeoutError", "PooledConnection", "SystemClock",
|
|
14
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Time source abstraction so every time-dependent rule is deterministic in tests."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import time
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Clock(Protocol):
|
|
10
|
+
def monotonic(self) -> float: ...
|
|
11
|
+
def sleep(self, seconds: float) -> None: ...
|
|
12
|
+
async def async_sleep(self, seconds: float) -> None: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SystemClock:
|
|
16
|
+
"""Real time. The default."""
|
|
17
|
+
|
|
18
|
+
def monotonic(self) -> float:
|
|
19
|
+
return time.monotonic()
|
|
20
|
+
|
|
21
|
+
def sleep(self, seconds: float) -> None:
|
|
22
|
+
time.sleep(seconds)
|
|
23
|
+
|
|
24
|
+
async def async_sleep(self, seconds: float) -> None:
|
|
25
|
+
await asyncio.sleep(seconds)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ManualClock:
|
|
29
|
+
"""Fake clock for tests: time moves only when you say so; sleeping just advances it."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, start: float = 1000.0) -> None:
|
|
32
|
+
self._now = start
|
|
33
|
+
|
|
34
|
+
def monotonic(self) -> float:
|
|
35
|
+
return self._now
|
|
36
|
+
|
|
37
|
+
def advance(self, seconds: float) -> None:
|
|
38
|
+
self._now += seconds
|
|
39
|
+
|
|
40
|
+
def sleep(self, seconds: float) -> None:
|
|
41
|
+
self._now += seconds
|
|
42
|
+
|
|
43
|
+
async def async_sleep(self, seconds: float) -> None:
|
|
44
|
+
self._now += seconds
|
|
45
|
+
await asyncio.sleep(0)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Bookkeeping wrapper around a raw connection."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import enum
|
|
5
|
+
import itertools
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Generic, Optional, TypeVar
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
_ids = itertools.count(1)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CloseReason(str, enum.Enum):
|
|
14
|
+
"""Why a connection left the pool."""
|
|
15
|
+
|
|
16
|
+
EXPIRED = "expired" # max_lifetime reached (rotation)
|
|
17
|
+
MAX_USES = "max_uses" # max_uses reached (rotation)
|
|
18
|
+
IDLE = "idle" # sat idle longer than max_idle_time
|
|
19
|
+
UNHEALTHY = "unhealthy" # failed validation
|
|
20
|
+
ERROR = "error" # caller's block raised and the connection is suspect
|
|
21
|
+
RESET_FAILED = "reset_failed" # factory.reset() raised on release
|
|
22
|
+
DISCARDED = "discarded" # caller asked for it
|
|
23
|
+
POOL_CLOSED = "pool_closed"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(eq=False)
|
|
27
|
+
class PooledConnection(Generic[T]):
|
|
28
|
+
"""A raw connection plus the metadata the pool needs to manage it."""
|
|
29
|
+
|
|
30
|
+
raw: T
|
|
31
|
+
created_at: float
|
|
32
|
+
expires_at: Optional[float] = None
|
|
33
|
+
last_returned_at: Optional[float] = None
|
|
34
|
+
checked_out_at: Optional[float] = None
|
|
35
|
+
use_count: int = 0
|
|
36
|
+
leak_reported: bool = False
|
|
37
|
+
id: int = field(default_factory=lambda: next(_ids))
|
|
38
|
+
|
|
39
|
+
def __post_init__(self) -> None:
|
|
40
|
+
if self.last_returned_at is None:
|
|
41
|
+
self.last_returned_at = self.created_at
|
|
42
|
+
|
|
43
|
+
def age(self, now: float) -> float:
|
|
44
|
+
return now - self.created_at
|
|
45
|
+
|
|
46
|
+
def idle_time(self, now: float) -> float:
|
|
47
|
+
return now - (self.last_returned_at or self.created_at)
|
|
48
|
+
|
|
49
|
+
def hold_time(self, now: float) -> float:
|
|
50
|
+
return 0.0 if self.checked_out_at is None else now - self.checked_out_at
|
|
51
|
+
|
|
52
|
+
def mark_checked_out(self, now: float) -> None:
|
|
53
|
+
self.checked_out_at = now
|
|
54
|
+
self.use_count += 1
|
|
55
|
+
self.leak_reported = False
|
|
56
|
+
|
|
57
|
+
def mark_returned(self, now: float) -> None:
|
|
58
|
+
self.checked_out_at = None
|
|
59
|
+
self.last_returned_at = now
|
|
60
|
+
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
return f"<PooledConnection #{self.id} uses={self.use_count}>"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Exception hierarchy. Catch ``PoolError`` to handle anything the pool raises."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PoolError(Exception):
|
|
6
|
+
"""Base class for every error raised by connpool."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PoolConfigError(PoolError, ValueError):
|
|
10
|
+
"""Invalid pool / breaker / retry configuration."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PoolClosedError(PoolError):
|
|
14
|
+
"""The pool has been closed; no more connections can be acquired."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PoolTimeoutError(PoolError, TimeoutError):
|
|
18
|
+
"""No connection became available before the acquire deadline."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PoolExhaustedError(PoolError):
|
|
22
|
+
"""The pool is full and the waiter queue is at ``max_waiters``; shedding load."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConnectionCreateError(PoolError):
|
|
26
|
+
"""The factory failed to create a connection (after all retries).
|
|
27
|
+
|
|
28
|
+
The original exception is available as ``__cause__``.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CircuitOpenError(PoolError):
|
|
33
|
+
"""The circuit breaker is rejecting calls to protect a failing backend."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, retry_after: float) -> None:
|
|
36
|
+
super().__init__(f"circuit breaker is open; retry in ~{retry_after:.1f}s")
|
|
37
|
+
self.retry_after = retry_after
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class InvalidReleaseError(PoolError):
|
|
41
|
+
"""Released an object that is not checked out from this pool (double release?)."""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Tiny shared helpers (internal)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import enum
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _Unset(enum.Enum):
|
|
9
|
+
UNSET = "unset"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
UNSET = _Unset.UNSET
|
|
13
|
+
"""Sentinel: "use the configured default" (``None`` already means "wait forever")."""
|
|
14
|
+
|
|
15
|
+
Timeout = Union[float, None, _Unset]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compute_deadline(timeout: Timeout, default: Optional[float], now: float) -> Optional[float]:
|
|
19
|
+
if isinstance(timeout, _Unset):
|
|
20
|
+
timeout = default
|
|
21
|
+
return None if timeout is None else now + timeout
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def remaining(deadline: Optional[float], now: float) -> Optional[float]:
|
|
25
|
+
return None if deadline is None else deadline - now
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""The seam to the outside world: what users implement to pool *any* kind of connection.
|
|
2
|
+
Has no dependency on the rest of connpool."""
|
|
3
|
+
from connpool.factories.async_factory import AsyncCallableFactory, AsyncConnectionFactory
|
|
4
|
+
from connpool.factories.sync_factory import CallableFactory, ConnectionFactory
|
|
5
|
+
|
|
6
|
+
__all__ = ["AsyncCallableFactory", "AsyncConnectionFactory", "CallableFactory", "ConnectionFactory"]
|