Skip to content

Commit 6629ba9

Browse files
committed
initial
0 parents  commit 6629ba9

File tree

19 files changed

+764
-0
lines changed

19 files changed

+764
-0
lines changed

.github/ci.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: fastapi_asyncpg
2+
3+
on: [push]
4+
5+
jobs:
6+
7+
# Job to run pre-checks
8+
pre-checks:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
matrix:
12+
python-version: [3.9]
13+
14+
steps:
15+
- name: Checkout the repository
16+
uses: actions/checkout@v2
17+
18+
- name: Setup Python
19+
uses: actions/setup-python@v1
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
23+
- name: Install package
24+
run: |
25+
pip install flake8==3.7.7
26+
pip install mypy==0.720
27+
pip install black==19.10b0
28+
pip install isort==4.3.21
29+
- name: Run pre-checks
30+
run: |
31+
flake8 fastapi_asyncpg --config=setup.cfg
32+
mypy fastapi_asyncpg/ --ignore-missing-imports
33+
isort -c -rc fastapi_asyncpg/
34+
black -l 80 --check --verbose fastapi_asyncpg
35+
# Job to run tests
36+
tests:
37+
runs-on: ubuntu-latest
38+
# Set environment variables
39+
steps:
40+
- name: Checkout the repository
41+
uses: actions/checkout@v2
42+
43+
- name: Setup Python
44+
uses: actions/setup-python@v1
45+
with:
46+
python-version: ${{ matrix.python-version }}
47+
48+
- name: Install the package
49+
run: |
50+
pip install -e .[test]
51+
52+
- name: Run tests
53+
run: |
54+
pytest -rfE --cov=fastapi_asyncpg -s --tb=native -v --cov-report xml --cov-append tests
55+
56+
- name: Upload coverage to Codecov
57+
uses: codecov/codecov-action@v1
58+
with:
59+
file: ./coverage.xml

.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
**/__pycache__
2+
env/
3+
venv/
4+
.coverage
5+
.env
6+
.idea
7+
.installed.cfg
8+
.pytest_cache/
9+
.mypy_cache/
10+
.tox/
11+
bin/
12+
coverage.xml
13+
develop-eggs/
14+
lib/
15+
lib64
16+
parts/
17+
pyvenv.cfg
18+
*.egg-info
19+
*.profraw
20+
*.py?
21+
*.swp

.isort.cfg

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[settings]
2+
force_single_line=True
3+
sections=FUTURE,THIRDPARTY,FIRSTPARTY,LOCALFOLDER,STDLIB
4+
no_lines_before=LOCALFOLDER,THIRDPARTY,FIRSTPARTY,STDLIB
5+
force_alphabetical_sort=True

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2020 Jordi collell
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.

Makefile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.PHONY: isort black flake8 mypy
2+
3+
lint: isort black flake8 mypy
4+
5+
isort:
6+
isort fastapi_asyncpg
7+
8+
black:
9+
black fastapi_asyncpg/ -l 80
10+
11+
flake8:
12+
flake8 fastapi_asyncpg
13+
14+
mypy:
15+
mypy fastapi_asyncpg

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# FastAPI AsyncPG
2+
3+
## Authors
4+
5+
`fastapi_asyncpg` was written by `Jordi collell <jordic@gmail.com>`\_.

examples/__init__.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from fastapi import FastAPI
2+
from fastapi import Depends
3+
from fastapi_asyncpg import configure_asyncpg
4+
5+
import pydantic as pd
6+
7+
app = FastAPI()
8+
9+
10+
db = configure_asyncpg(app, "postgresql://postgres:postgres@localhost/db")
11+
12+
13+
class Demo(pd.BaseModel):
14+
key: str
15+
value: str
16+
17+
18+
class DemoObj(Demo):
19+
demo_id: int
20+
21+
22+
@db.on_init
23+
async def initialize_db(db):
24+
await db.execute(
25+
"""
26+
CREATE TABLE IF NOT EXISTS demo (
27+
demo_id serial primary key,
28+
key varchar not null,
29+
value varchar not null,
30+
UNIQUE(key)
31+
);
32+
"""
33+
)
34+
35+
36+
@app.post("/", response_model=DemoObj)
37+
async def add_resource(data: Demo, db=Depends(db.connection)):
38+
"""
39+
Add a resource to db:
40+
curl -X POST -d '{"key": "test", "value": "asdf"}' \
41+
http://localhost:8000/
42+
"""
43+
result = await db.fetchrow(
44+
"""
45+
INSERT into demo values (default, $1, $2) returning *
46+
""",
47+
data.key,
48+
data.value,
49+
)
50+
return dict(result)
51+
52+
53+
@app.get("/{key:str}", response_model=DemoObj)
54+
async def get_resouce(key: str, db=Depends(db.connection)):
55+
result = await db.fetchrow(
56+
"""
57+
SELECT * from demo where key=$1
58+
""",
59+
key,
60+
)
61+
return dict(result)

fastapi_asyncpg/__init__.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
from __future__ import annotations
2+
3+
from fastapi import FastAPI
4+
5+
import asyncpg
6+
import typing
7+
8+
9+
async def noop(db: asyncpg.Connection):
10+
return
11+
12+
13+
class configure_asyncpg:
14+
def __init__(
15+
self,
16+
app: FastAPI,
17+
dsn: str,
18+
*,
19+
init_db: typing.Callable = None, # callable for running sql on init
20+
pool=None, # usable on testing
21+
**options,
22+
):
23+
self.app = app
24+
self.dsn = dsn
25+
self.init_db = init_db
26+
self.con_opts = options
27+
self._pool = pool
28+
self.app.router.add_event_handler("startup", self.on_connect)
29+
self.app.router.add_event_handler("shutdown", self.on_disconnect)
30+
31+
async def on_connect(self):
32+
if self._pool:
33+
self.app.state.pool = self._pool
34+
return
35+
pool = await asyncpg.create_pool(dsn=self.dsn, **self.con_opts)
36+
async with pool.acquire() as db:
37+
await self.init_db(db)
38+
self.app.state.pool = pool
39+
40+
async def on_disconnect(self):
41+
await self.app.state.pool.close()
42+
43+
def on_init(self, func):
44+
self.init_db = func
45+
return func
46+
47+
@property
48+
def pool(self):
49+
return self.app.state.pool
50+
51+
async def connection(self):
52+
async with self.pool.acquire() as db:
53+
yield db
54+
55+
async def transaction(self):
56+
async with self.pool.acquire() as db:
57+
txn = db.transaction()
58+
await txn.start()
59+
try:
60+
yield db
61+
except:
62+
await txn.rollback()
63+
raise
64+
else:
65+
await txn.commit()
66+
67+
atomic = transaction
68+
69+
70+
class SingleConnectionTestingPool:
71+
"""A fake pool that simulates pooling, but runs on
72+
a single transaction that it's rolled back after
73+
each test.
74+
With some large schemas this seems to be faster than
75+
the other approach
76+
"""
77+
78+
def __init__(
79+
self,
80+
conn: asyncpg.Connection,
81+
initialize: typing.Callable = None,
82+
add_logger_postgres: bool = False,
83+
):
84+
self._conn = conn
85+
self.tx = None
86+
self.started = False
87+
self.add_logger_postgres = add_logger_postgres
88+
self.initialize = initialize
89+
90+
def acquire(self, *, timeout=None):
91+
return ConAcquireContext(self._conn, self)
92+
93+
async def start(self):
94+
if self.started:
95+
return
96+
97+
def log_postgresql(con, message):
98+
print(message)
99+
100+
if self.add_logger_postgres:
101+
self._conn.add_log_listener(log_postgresql)
102+
self.tx = self._conn.transaction()
103+
await self.tx.start()
104+
await self.initialize(self._conn)
105+
self.started = True
106+
107+
async def release(self):
108+
if self.tx:
109+
await self.tx.rollback()
110+
111+
def __getattr__(self, key):
112+
return getattr(self._conn, key)
113+
114+
115+
async def create_pool_test(
116+
dsn: str,
117+
*,
118+
initialize: typing.Callable = None,
119+
add_logger_postgres: bool = False,
120+
):
121+
"""This part is only used for testing,
122+
we create a fake "pool" that just starts a connecion,
123+
that does a transaction inside it"""
124+
conn = await asyncpg.connect(dsn=dsn)
125+
pool = SingleConnectionTestingPool(
126+
conn, initialize=initialize, add_logger_postgres=add_logger_postgres
127+
)
128+
return pool
129+
130+
131+
class ConAcquireContext:
132+
def __init__(self, conn, manager):
133+
self._conn = conn
134+
self.manager = manager
135+
136+
async def __aenter__(self):
137+
if not self.manager.tx:
138+
await self.manager.start()
139+
self.tr = self._conn.transaction()
140+
await self.tr.start()
141+
return self._conn
142+
143+
async def __aexit__(self, exc_type, exc, tb):
144+
if exc_type:
145+
await self.tr.rollback()
146+
else:
147+
await self.tr.commit()

0 commit comments

Comments
 (0)