Skip to content

Commit 9560139

Browse files
authored
Merge pull request #1 from dapper91/dev
initial version.
2 parents 3361b00 + 767f255 commit 9560139

27 files changed

+3585
-0
lines changed

.flake8

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[flake8]
2+
max-line-length = 120
3+
per-file-ignores =
4+
generic_connection_pool/*__init__.py: F401

.github/workflows/release.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: release
2+
3+
on:
4+
release:
5+
types:
6+
- released
7+
8+
jobs:
9+
release:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v2
13+
- name: Set up Python
14+
uses: actions/setup-python@v2
15+
with:
16+
python-version: '3.x'
17+
- name: Install dependencies
18+
run: |
19+
python -m pip install --upgrade pip poetry
20+
poetry install
21+
- name: Build and publish
22+
run: |
23+
poetry build
24+
poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }}

.github/workflows/test.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: test
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- dev
7+
- master
8+
push:
9+
branches:
10+
- master
11+
12+
jobs:
13+
test:
14+
runs-on: ubuntu-latest
15+
strategy:
16+
matrix:
17+
python-version: ['3.9', '3.10', '3.11']
18+
steps:
19+
- uses: actions/checkout@v2
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v2
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install poetry
28+
poetry install --no-root
29+
- name: Run pre-commit hooks
30+
run: poetry run pre-commit run --hook-stage merge-commit --all-files
31+
- name: Run tests
32+
run: PYTHONPATH="$(pwd):$PYTHONPATH" poetry run py.test --cov=generic_connection_pool --cov-report=xml tests
33+
- name: Upload coverage to Codecov
34+
uses: codecov/codecov-action@v1
35+
with:
36+
token: ${{ secrets.CODECOV_TOKEN }}
37+
files: ./coverage.xml
38+
flags: unittests
39+
fail_ci_if_error: true

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,6 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
131+
# poetry
132+
poetry.lock

.pre-commit-config.yaml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
default_stages:
2+
- commit
3+
- merge-commit
4+
5+
repos:
6+
- repo: https://github.com/pre-commit/pre-commit-hooks
7+
rev: v4.4.0
8+
hooks:
9+
- id: check-yaml
10+
- id: check-toml
11+
- id: trailing-whitespace
12+
- id: end-of-file-fixer
13+
stages:
14+
- commit
15+
- id: mixed-line-ending
16+
name: fix line ending
17+
stages:
18+
- commit
19+
args:
20+
- --fix=lf
21+
- id: mixed-line-ending
22+
name: check line ending
23+
stages:
24+
- merge-commit
25+
args:
26+
- --fix=no
27+
- repo: https://github.com/asottile/add-trailing-comma
28+
rev: v2.4.0
29+
hooks:
30+
- id: add-trailing-comma
31+
stages:
32+
- commit
33+
- repo: https://github.com/pre-commit/mirrors-autopep8
34+
rev: v2.0.2
35+
hooks:
36+
- id: autopep8
37+
stages:
38+
- commit
39+
args:
40+
- --diff
41+
- repo: https://github.com/pycqa/flake8
42+
rev: 6.0.0
43+
hooks:
44+
- id: flake8
45+
- repo: https://github.com/pycqa/isort
46+
rev: 5.12.0
47+
hooks:
48+
- id: isort
49+
name: fix import order
50+
stages:
51+
- commit
52+
args:
53+
- --line-length=120
54+
- --multi-line=9
55+
- --project=generic_connection_pool
56+
- id: isort
57+
name: check import order
58+
stages:
59+
- merge-commit
60+
args:
61+
- --check-only
62+
- --line-length=120
63+
- --multi-line=9
64+
- --project=generic_connection_pool
65+
- repo: https://github.com/pre-commit/mirrors-mypy
66+
rev: v1.1.1
67+
hooks:
68+
- id: mypy
69+
stages:
70+
- commit
71+
name: mypy
72+
pass_filenames: false
73+
additional_dependencies:
74+
- types-psycopg2
75+
args: ["--package", "generic_connection_pool"]

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Changelog
2+
=========
3+
4+
5+
0.1.0 (2021-03-15)
6+
------------------
7+
8+
- Initial release

README.rst

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
=======================
2+
generic-connection-pool
3+
=======================
4+
5+
.. image:: https://static.pepy.tech/personalized-badge/generic-connection-pool?period=month&units=international_system&left_color=grey&right_color=orange&left_text=Downloads/month
6+
:target: https://pepy.tech/project/generic-connection-pool
7+
:alt: Downloads/month
8+
.. image:: https://github.com/dapper91/generic-connection-pool/actions/workflows/test.yml/badge.svg?branch=master
9+
:target: https://github.com/dapper91/generic-connection-pool/actions/workflows/test.yml
10+
:alt: Build status
11+
.. image:: https://img.shields.io/pypi/l/generic-connection-pool.svg
12+
:target: https://pypi.org/project/generic-connection-pool
13+
:alt: License
14+
.. image:: https://img.shields.io/pypi/pyversions/generic-connection-pool.svg
15+
:target: https://pypi.org/project/generic-connection-pool
16+
:alt: Supported Python versions
17+
.. image:: https://codecov.io/gh/dapper91/generic-connection-pool/branch/master/graph/badge.svg
18+
:target: https://codecov.io/gh/dapper91/generic-connection-pool
19+
:alt: Code coverage
20+
21+
22+
``generic-connection-pool`` is a connection pool that can be used for TCP, http, database connections.
23+
24+
Features:
25+
26+
- **generic nature**: can be used for any connection you desire (TCP, http, database)
27+
- **runtime agnostic**: synchronous and asynchronous pool supported
28+
- **flexibility**: flexable connection retention and recycling policy
29+
- **fully-typed**: mypy type-checker compatible
30+
31+
32+
Installation
33+
------------
34+
35+
You can install generic-connection-pool with pip:
36+
37+
.. code-block:: console
38+
39+
$ pip install generic-connection-pool
40+
41+
42+
Quickstart
43+
----------
44+
45+
The following example illustrates how to create asynchronous ssl socket pool:
46+
47+
.. code-block:: python
48+
49+
import asyncio
50+
from typing import Tuple
51+
52+
from generic_connection_pool.asyncio import ConnectionPool
53+
from generic_connection_pool.contrib.socket_async import TcpStreamConnectionManager
54+
55+
Hostname = str
56+
Port = int
57+
Endpoint = Tuple[Hostname, Port]
58+
Connection = Tuple[asyncio.StreamReader, asyncio.StreamWriter]
59+
60+
61+
async def main() -> None:
62+
pool = ConnectionPool[Endpoint, Connection](
63+
TcpStreamConnectionManager(ssl=True),
64+
idle_timeout=30.0,
65+
max_lifetime=600.0,
66+
min_idle=3,
67+
max_size=20,
68+
total_max_size=100,
69+
background_collector=True,
70+
)
71+
72+
async with pool.connection(endpoint=('www.wikipedia.org', 443), timeout=5.0) as (reader, writer):
73+
request = (
74+
'GET / HTTP/1.0\n'
75+
'Host: www.wikipedia.org\n'
76+
'\n'
77+
'\n'
78+
)
79+
writer.write(request.encode())
80+
await writer.drain()
81+
response = await reader.read()
82+
83+
print(response.decode())
84+
85+
asyncio.run(main())
86+
87+
88+
Configuration
89+
-------------
90+
91+
Synchronous and asynchronous pools supports the following parameters:
92+
93+
* **connection_manager**: connection manager instance
94+
* **acquire_timeout**: connection acquiring default timeout
95+
* **dispose_batch_size**: number of connections to be disposed at once
96+
(if background collector is started the parameter is ignored)
97+
* **dispose_timeout**: connection disposal timeout
98+
* **background_collector**: start worker that disposes timed-out connections in background maintain provided pool state
99+
otherwise they will be disposed on each connection release
100+
* **idle_timeout**: number of seconds after which a connection will be closed respecting min_idle parameter
101+
(the connection will be closed only if the connection number exceeds min_idle)
102+
* **max_lifetime**: number of seconds after which a connection will be closed (min_idle parameter will be ignored)
103+
* **min_idle**: minimum number of connections the pool tries to hold (for each endpoint)
104+
* **max_size**: maximum number of connections (for each endpoint)
105+
* **total_max_size**: maximum number of connections (for all endpoints)
106+
107+
108+
Generic nature
109+
--------------
110+
111+
Since the pool has generic nature is can be used for database connections as well:
112+
113+
.. code-block:: python
114+
115+
import psycopg2.extensions
116+
117+
from generic_connection_pool.contrib.psycopg2 import DbConnectionManager
118+
from generic_connection_pool.threding import ConnectionPool
119+
120+
Endpoint = str
121+
Connection = psycopg2.extensions.connection
122+
123+
124+
def main() -> None:
125+
dsn_params = dict(dbname='postgres', user='postgres', password='secret')
126+
127+
pool = ConnectionPool[Endpoint, Connection](
128+
DbConnectionManager(
129+
dsn_params={
130+
'master': dict(dsn_params, host='db-master.local'),
131+
'replica-1': dict(dsn_params, host='db-replica-1.local'),
132+
'replica-2': dict(dsn_params, host='db-replica-2.local'),
133+
},
134+
),
135+
acquire_timeout=2.0,
136+
idle_timeout=60.0,
137+
max_lifetime=600.0,
138+
min_idle=3,
139+
max_size=10,
140+
total_max_size=15,
141+
background_collector=True,
142+
)
143+
144+
with pool.connection(endpoint='master') as conn:
145+
cur = conn.cursor()
146+
cur.execute("SELECT * FROM pg_stats;")
147+
print(cur.fetchone())
148+
149+
with pool.connection(endpoint='replica-1') as conn:
150+
cur = conn.cursor()
151+
cur.execute("SELECT * FROM pg_stats;")
152+
print(cur.fetchone())
153+
154+
pool.close()
155+
156+
157+
main()
158+
159+
160+
Extendability
161+
-------------
162+
163+
If built-in connection managers are not suitable for your task the one can be easily created by yourself:
164+
165+
.. code-block:: python
166+
167+
import socket
168+
from ssl import SSLContext, SSLSocket
169+
from typing import Optional, Tuple
170+
171+
from generic_connection_pool.threding import BaseConnectionManager, ConnectionPool
172+
173+
Hostname = str
174+
Port = int
175+
SslEndpoint = Tuple[Hostname, Port]
176+
Connection = SSLSocket
177+
178+
179+
class SslSocketConnectionManager(BaseConnectionManager[SslEndpoint, Connection]):
180+
"""
181+
SSL socket connection manager.
182+
"""
183+
184+
def __init__(self, ssl: SSLContext):
185+
self._ssl = ssl
186+
187+
def create(self, endpoint: SslEndpoint, timeout: Optional[float] = None) -> Connection:
188+
hostname, port = endpoint
189+
190+
sock = self._ssl.wrap_socket(socket.socket(type=socket.SOCK_STREAM), server_hostname=hostname)
191+
sock.settimeout(timeout)
192+
sock.connect((hostname, port))
193+
194+
return sock
195+
196+
def dispose(self, endpoint: SslEndpoint, conn: Connection, timeout: Optional[float] = None) -> None:
197+
conn.settimeout(timeout)
198+
try:
199+
conn.shutdown(socket.SHUT_RDWR)
200+
except OSError:
201+
pass
202+
203+
conn.close()
204+
205+
206+
def main() -> None:
207+
pool = ConnectionPool[SslEndpoint, Connection](
208+
SslSocketConnectionManager(ssl=SSLContext()),
209+
idle_timeout=30.0,
210+
max_lifetime=600.0,
211+
min_idle=3,
212+
max_size=20,
213+
total_max_size=100,
214+
background_collector=True,
215+
)
216+
217+
with pool.connection(endpoint=('www.wikipedia.org', 443), timeout=5.0) as sock:
218+
request = (
219+
'GET / HTTP/1.0\n'
220+
'Host: www.wikipedia.org\n'
221+
'\n'
222+
'\n'
223+
)
224+
sock.write(request.encode())
225+
response = []
226+
while chunk := sock.recv():
227+
response.append(chunk)
228+
229+
print(b''.join(response).decode())
230+
231+
pool.close()
232+
233+
234+
main()

0 commit comments

Comments
 (0)