Skip to content

Commit 0954fc5

Browse files
v0.0.1 🍀
1 parent 0218fc2 commit 0954fc5

21 files changed

Lines changed: 1007 additions & 1 deletion

.flake8

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[flake8]
2+
exclude = __pycache__,built,build,venv
3+
ignore = E203, E266, W503
4+
max-line-length = 88
5+
max-complexity = 18
6+
select = B,C,E,F,W,T4,B9
7+
per-file-ignores =
8+
tests/app.py:E402
9+
tests/migrations/env.py:E402

.github/FUNDING.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# These are supported funding model platforms
2+
3+
github: RobertoPrevato

.github/workflows/build.yml

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
name: Build
2+
3+
on:
4+
release:
5+
types: [published]
6+
push:
7+
branches:
8+
- main
9+
- dev
10+
pull_request:
11+
branches:
12+
- "*"
13+
14+
env:
15+
PROJECT_NAME: blacksheepsqlalchemy
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-18.04
20+
strategy:
21+
matrix:
22+
python-version: [3.7, 3.8, 3.9]
23+
24+
steps:
25+
- uses: actions/checkout@v1
26+
with:
27+
fetch-depth: 9
28+
submodules: false
29+
30+
- name: Use Python ${{ matrix.python-version }}
31+
uses: actions/setup-python@v1
32+
with:
33+
python-version: ${{ matrix.python-version }}
34+
35+
- uses: actions/cache@v1
36+
id: depcache
37+
with:
38+
path: deps
39+
key: requirements-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
40+
41+
- name: Download dependencies
42+
if: steps.depcache.outputs.cache-hit != 'true'
43+
run: |
44+
pip download --dest=deps -r dev-requirements.txt
45+
46+
- name: Install dependencies
47+
run: |
48+
pip install -U --no-index --find-links=deps deps/*
49+
50+
- name: Prepare test database
51+
run: |
52+
cd tests
53+
alembic upgrade head
54+
cp example.db ../
55+
56+
- name: Run tests
57+
run: |
58+
flake8 && pytest --doctest-modules --junitxml=junit/pytest-results-${{ matrix.python-version }}.xml --cov=$PROJECT_NAME --cov-report=xml --ignore=tests/migrations tests/
59+
60+
- name: Upload pytest test results
61+
uses: actions/upload-artifact@master
62+
with:
63+
name: pytest-results-${{ matrix.python-version }}
64+
path: junit/pytest-results-${{ matrix.python-version }}.xml
65+
if: always()
66+
67+
- name: Codecov
68+
run: |
69+
bash <(curl -s https://codecov.io/bash)
70+
71+
- name: Install distribution dependencies
72+
run: pip install --upgrade twine setuptools wheel
73+
if: matrix.python-version == 3.8 || matrix.python-version == 3.9
74+
75+
- name: Create distribution package
76+
run: python setup.py sdist bdist_wheel
77+
if: matrix.python-version == 3.8 || matrix.python-version == 3.9
78+
79+
- name: Upload distribution package
80+
uses: actions/upload-artifact@master
81+
with:
82+
name: dist-package-${{ matrix.python-version }}
83+
path: dist
84+
if: matrix.python-version == 3.8 || matrix.python-version == 3.9
85+
86+
publish:
87+
runs-on: ubuntu-18.04
88+
needs: build
89+
if: github.event_name == 'release'
90+
steps:
91+
- name: Download a distribution artifact
92+
uses: actions/download-artifact@v2
93+
with:
94+
name: dist-package-3.9
95+
path: dist
96+
- name: Publish distribution 📦 to Test PyPI
97+
uses: pypa/gh-action-pypi-publish@master
98+
with:
99+
skip_existing: true
100+
user: __token__
101+
password: ${{ secrets.test_pypi_password }}
102+
repository_url: https://test.pypi.org/legacy/
103+
- name: Publish distribution 📦 to PyPI
104+
uses: pypa/gh-action-pypi-publish@master
105+
with:
106+
user: __token__
107+
password: ${{ secrets.pypi_password }}

.gitignore

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

128128
# Pyre type checker
129129
.pyre/
130+
/tests/example.db

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.0.1] - 2021-05-22 :four_leaf_clover:
9+
- First working implementation, with integration offering injection of
10+
db connections and ORM sessions provided by SQLAlchemy

Makefile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.PHONY: release test
2+
3+
4+
artifacts: test
5+
python setup.py sdist bdist_wheel
6+
7+
8+
clean:
9+
rm -rf dist/
10+
11+
12+
prepforbuild:
13+
pip install --upgrade twine setuptools wheel
14+
15+
16+
uploadtest:
17+
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
18+
19+
20+
release: clean artifacts
21+
twine upload --repository-url https://upload.pypi.org/legacy/ dist/*
22+
23+
24+
test:
25+
flake8 blacksheepsqlalchemy && pytest tests/
26+
27+
28+
testcov:
29+
pytest --cov-report html --cov=blacksheepsqlalchemy tests/

README.md

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,82 @@
1+
[![Build](https://github.com/Neoteroi/BlackSheep-SQLAlchemy/actions/workflows/build.yml/badge.svg)](https://github.com/Neoteroi/BlackSheep-SQLAlchemy/actions/workflows/build.yml)
2+
[![pypi](https://img.shields.io/pypi/v/BlackSheep-SQLAlchemy.svg?color=blue)](https://pypi.org/project/BlackSheep-SQLAlchemy/)
3+
[![versions](https://img.shields.io/pypi/pyversions/blacksheep-sqlalchemy.svg)](https://github.com/Neoteroi/BlackSheep-SQLAlchemy/)
4+
[![license](https://img.shields.io/github/license/Neoteroi/blacksheep-sqlalchemy.svg)](https://github.com/Neoteroi/BlackSheep-SQLAlchemy/blob/main/LICENSE)
5+
16
# BlackSheep-SQLAlchemy
2-
Extension for BlackSheep that simplifies the use of SQLAlchemy in the web framework.
7+
Extension for [BlackSheep](https://github.com/Neoteroi/BlackSheep) that
8+
simplifies the use of SQLAlchemy in the web framework.
9+
10+
```bash
11+
pip install blacksheep-sqlalchemy
12+
```
13+
14+
## How to use
15+
16+
```python
17+
from blacksheep.server import Application
18+
from blacksheepsqlalchemy import use_sqlalchemy
19+
20+
app = Application()
21+
22+
use_sqlalchemy(app, connection_string="<CONNECTION_STRING>")
23+
24+
```
25+
26+
After registering SQLAlchemy, services are configured in the application, so
27+
they are automatically resolved in any request handler requiring a SQLAlchemy
28+
db connections or db sessions; for example:
29+
30+
```python
31+
32+
@get("/api/countries")
33+
async def get_countries(db_connection) -> List[CountryData]:
34+
"""
35+
Fetches the countries using a database connection.
36+
"""
37+
result = []
38+
async with db_connection:
39+
items = await db_connection.execute(text("SELECT * FROM country"))
40+
for item in items.fetchall():
41+
result.append(CountryData(item["id"], item["name"]))
42+
return result
43+
44+
```
45+
46+
Services can be injected at any level of the resolution graph, so `BlackSheep`
47+
and `rodi` support out of the box the scenario of db connections or db sessions
48+
referenced in the business logic but not directly by the front-end layer
49+
(depending on programmers' preference and their notion of best practices when
50+
building web apps).
51+
52+
Services can be injected in the following ways:
53+
54+
| By alias | By type annotation | Value |
55+
| ------------- | ------------------ | --------------------------------------------------- |
56+
| db_connection | AsyncConnection | instance of AsyncConnection (scoped to web request) |
57+
| db_session | AsyncSession | instance of AsyncSession (scoped to web request) |
58+
| db_engine | AsyncEngine | instance of AsyncEngine (singleton) |
59+
60+
---
61+
62+
For example, using SQLite:
63+
64+
* requires driver: `pip install aiosqlite`
65+
* connection string: `sqlite+aiosqlite:///example.db`
66+
67+
See the `tests` folder for a working example using database migrations applied
68+
with `Alembic`, and a documented API that offers methods to fetch, create,
69+
delete countries objects.
70+
71+
---
72+
73+
### Note
74+
BlackSheep is designed to be used in `async` way, therefore this library
75+
requires the use of an asynchronous driver.
76+
77+
## References
78+
79+
* [SQLAlchemy - support for asyncio](https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html)
80+
81+
## Documentation
82+
Please refer to the [documentation website](https://www.neoteroi.dev/blacksheep/).

blacksheepsqlalchemy/__init__.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from typing import Optional
2+
3+
from sqlalchemy.ext.asyncio import (
4+
create_async_engine,
5+
AsyncEngine,
6+
AsyncSession,
7+
AsyncConnection,
8+
)
9+
from blacksheep.server import Application
10+
11+
12+
def __configure_services(
13+
app: Application,
14+
engine: AsyncEngine,
15+
db_engine_alias: str,
16+
db_connection_alias: str,
17+
db_session_alias: str,
18+
) -> None:
19+
# Note: pytest-cov generates false negatives for the following three functions
20+
# defined locally; they work and this is verified by tests
21+
22+
async def dispose_engine(_):
23+
nonlocal engine
24+
await engine.dispose()
25+
26+
app.on_stop += dispose_engine
27+
28+
def connection_factory() -> AsyncConnection:
29+
return engine.connect()
30+
31+
def session_factory() -> AsyncSession:
32+
return AsyncSession(engine, expire_on_commit=False)
33+
34+
app.services.add_instance(engine)
35+
app.services.add_alias(db_engine_alias, AsyncEngine)
36+
37+
app.services.add_scoped_by_factory(connection_factory)
38+
app.services.add_alias(db_connection_alias, AsyncConnection)
39+
40+
app.services.add_scoped_by_factory(session_factory)
41+
app.services.add_alias(db_session_alias, AsyncSession)
42+
43+
# TODO: configure an exception handler for
44+
# sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed,
45+
# to return an HTTP Conflict response in such case!
46+
47+
48+
def use_sqlalchemy(
49+
app: Application,
50+
*,
51+
connection_string: Optional[str] = None,
52+
echo: bool = False,
53+
engine: Optional[AsyncEngine] = None,
54+
db_engine_alias: str = "db_engine",
55+
db_connection_alias: str = "db_connection",
56+
db_session_alias: str = "db_session",
57+
) -> None:
58+
"""
59+
Configures the given application to use SQLAlchemy and provide services that can be
60+
injected in request handlers.
61+
"""
62+
if engine is None:
63+
if not connection_string:
64+
raise TypeError(
65+
"Either pass a connection_string or an instance of "
66+
"sqlalchemy.ext.asyncio.AsyncEngine"
67+
)
68+
engine = create_async_engine(connection_string, echo=echo)
69+
70+
assert engine is not None
71+
__configure_services(
72+
app, engine, db_engine_alias, db_connection_alias, db_session_alias
73+
)

dev-requirements.txt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
aiosqlite==0.17.0
2+
alembic==1.6.3
3+
appdirs==1.4.4
4+
asyncpg==0.23.0
5+
attrs==21.2.0
6+
black==21.5b1
7+
blacksheep==1.0.5
8+
cchardet==2.1.7
9+
certifi==2020.12.5
10+
click==7.1.2
11+
coverage==5.5
12+
essentials==1.1.4
13+
essentials-openapi==0.1.2
14+
flake8==3.9.2
15+
greenlet==1.1.0
16+
guardpost==0.0.7
17+
h11==0.12.0
18+
httptools==0.2.0
19+
iniconfig==1.1.1
20+
itsdangerous==1.1.0
21+
Jinja2==2.11.3
22+
Mako==1.1.4
23+
MarkupSafe==2.0.1
24+
mccabe==0.6.1
25+
mypy==0.812
26+
mypy-extensions==0.4.3
27+
packaging==20.9
28+
pathspec==0.8.1
29+
pluggy==0.13.1
30+
py==1.10.0
31+
pycodestyle==2.7.0
32+
pyflakes==2.3.1
33+
pyparsing==2.4.7
34+
pytest==6.2.4
35+
pytest-asyncio==0.15.1
36+
pytest-cov==2.12.0
37+
python-dateutil==2.8.1
38+
python-editor==1.0.4
39+
PyYAML==5.4.1
40+
regex==2021.4.4
41+
rodi==1.1.1
42+
six==1.16.0
43+
SQLAlchemy==1.4.15
44+
sqlalchemy-stubs==0.4
45+
sqlalchemy2-stubs==0.0.2a1
46+
toml==0.10.2
47+
typed-ast==1.4.3
48+
typing-extensions==3.10.0.0
49+
uvicorn==0.13.4

0 commit comments

Comments
 (0)