diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index f85ca99a..a46893de 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -18,7 +18,7 @@ jobs: services: mysql: - image: mysql:5.7 + image: mariadb:11 env: MYSQL_USER: username MYSQL_PASSWORD: password @@ -26,10 +26,10 @@ jobs: MYSQL_DATABASE: testsuite ports: - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + options: --health-cmd="mariadb-admin ping" --health-interval=10s --health-timeout=5s --health-retries=3 postgres: - image: postgres:14 + image: postgres:16 env: POSTGRES_USER: username POSTGRES_PASSWORD: password @@ -59,5 +59,6 @@ jobs: mysql+asyncmy://username:password@localhost:3306/testsuite, postgresql://username:password@localhost:5432/testsuite, postgresql+aiopg://username:password@127.0.0.1:5432/testsuite, - postgresql+asyncpg://username:password@localhost:5432/testsuite + postgresql+asyncpg://username:password@localhost:5432/testsuite, + postgresql+psycopg://username:password@localhost:5432/testsuite run: "scripts/test" diff --git a/README.md b/README.md index f40cd173..edf68e40 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Database drivers supported are: * [asyncpg][asyncpg] * [aiopg][aiopg] +* [psycopg3][psycopg3] * [aiomysql][aiomysql] * [asyncmy][asyncmy] * [aiosqlite][aiosqlite] @@ -42,6 +43,7 @@ You can install the required database drivers with: ```shell $ pip install databases[asyncpg] $ pip install databases[aiopg] +$ pip install databases[psycopg3] $ pip install databases[aiomysql] $ pip install databases[asyncmy] $ pip install databases[aiosqlite] @@ -105,6 +107,7 @@ for examples of how to start using databases together with SQLAlchemy core expre [pymysql]: https://github.com/PyMySQL/PyMySQL [asyncpg]: https://github.com/MagicStack/asyncpg [aiopg]: https://github.com/aio-libs/aiopg +[psycopg3]: https://github.com/psycopg/psycopg [aiomysql]: https://github.com/aio-libs/aiomysql [asyncmy]: https://github.com/long2ice/asyncmy [aiosqlite]: https://github.com/omnilib/aiosqlite diff --git a/databases/backends/common/records.py b/databases/backends/common/records.py index e963af50..f57de839 100644 --- a/databases/backends/common/records.py +++ b/databases/backends/common/records.py @@ -1,6 +1,4 @@ -import enum import typing -from datetime import date, datetime, time from sqlalchemy.engine.interfaces import Dialect from sqlalchemy.engine.row import Row as SQLRow diff --git a/docs/index.md b/docs/index.md index 7c3cebf2..c4e581d5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,6 +31,7 @@ Database drivers supported are: * [asyncpg][asyncpg] * [aiopg][aiopg] +* [psycopg3][psycopg3] * [aiomysql][aiomysql] * [asyncmy][asyncmy] * [aiosqlite][aiosqlite] @@ -40,6 +41,7 @@ You can install the required database drivers with: ```shell $ pip install databases[asyncpg] $ pip install databases[aiopg] +$ pip install databases[psycopg3] $ pip install databases[aiomysql] $ pip install databases[asyncmy] $ pip install databases[aiosqlite] @@ -103,6 +105,7 @@ for examples of how to start using databases together with SQLAlchemy core expre [pymysql]: https://github.com/PyMySQL/PyMySQL [asyncpg]: https://github.com/MagicStack/asyncpg [aiopg]: https://github.com/aio-libs/aiopg +[psycopg3]: https://github.com/psycopg/psycopg [aiomysql]: https://github.com/aio-libs/aiomysql [asyncmy]: https://github.com/long2ice/asyncmy [aiosqlite]: https://github.com/omnilib/aiosqlite diff --git a/requirements.txt b/requirements.txt index 8b05a46e..be086f77 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,9 +6,12 @@ aiomysql==0.2.0 aiopg==1.4.0 aiosqlite==0.20.0 asyncpg==0.29.0 +psycopg==3.1.18 +psycopg-binary==3.1.18 # Sync database drivers for standard tooling around setup/teardown/migrations. psycopg==3.1.18 +psycopg-binary==3.1.18 pymysql==1.1.0 # Testing diff --git a/setup.py b/setup.py index 41c0c584..9218bc1f 100644 --- a/setup.py +++ b/setup.py @@ -47,14 +47,15 @@ def get_packages(package): author_email="tom@tomchristie.com", packages=get_packages("databases"), package_data={"databases": ["py.typed"]}, - install_requires=["sqlalchemy>=2.0.7"], + install_requires=["sqlalchemy>=2.0.11"], extras_require={ - "postgresql": ["asyncpg"], - "asyncpg": ["asyncpg"], - "aiopg": ["aiopg"], "mysql": ["aiomysql"], "aiomysql": ["aiomysql"], "asyncmy": ["asyncmy"], + "postgresql": ["asyncpg"], + "aiopg": ["aiopg"], + "asyncpg": ["asyncpg"], + "psycopg3": ["psycopg"], "sqlite": ["aiosqlite"], "aiosqlite": ["aiosqlite"], }, diff --git a/tests/test_databases.py b/tests/test_databases.py index d9d9e4d6..90771b1d 100644 --- a/tests/test_databases.py +++ b/tests/test_databases.py @@ -134,6 +134,7 @@ def create_test_database(): "postgresql+aiopg", "sqlite+aiosqlite", "postgresql+asyncpg", + "postgresql+psycopg", ]: url = str(database_url.replace(driver=None)) engine = sqlalchemy.create_engine(url) @@ -151,6 +152,7 @@ def create_test_database(): "postgresql+aiopg", "sqlite+aiosqlite", "postgresql+asyncpg", + "postgresql+psycopg", ]: url = str(database_url.replace(driver=None)) engine = sqlalchemy.create_engine(url) @@ -1354,7 +1356,11 @@ async def test_queries_with_expose_backend_connection(database_url): elif database.url.scheme == "mysql+asyncmy": async with raw_connection.cursor() as cursor: await cursor.execute(insert_query, values) - elif database.url.scheme in ["postgresql", "postgresql+asyncpg"]: + elif database.url.scheme in [ + "postgresql", + "postgresql+asyncpg", + "postgresql+psycopg", + ]: await raw_connection.execute(insert_query, *values) elif database.url.scheme in ["sqlite", "sqlite+aiosqlite"]: await raw_connection.execute(insert_query, values) @@ -1392,7 +1398,11 @@ async def test_queries_with_expose_backend_connection(database_url): async with raw_connection.cursor() as cursor: await cursor.execute(select_query) results = await cursor.fetchall() - elif database.url.scheme in ["postgresql", "postgresql+asyncpg"]: + elif database.url.scheme in [ + "postgresql", + "postgresql+asyncpg", + "postgresql+psycopg", + ]: results = await raw_connection.fetch(select_query) elif database.url.scheme in ["sqlite", "sqlite+aiosqlite"]: results = await raw_connection.execute_fetchall(select_query) @@ -1407,7 +1417,11 @@ async def test_queries_with_expose_backend_connection(database_url): assert results[2][2] == True # fetch_one() - if database.url.scheme in ["postgresql", "postgresql+asyncpg"]: + if database.url.scheme in [ + "postgresql", + "postgresql+asyncpg", + "postgresql+psycopg", + ]: result = await raw_connection.fetchrow(select_query) elif database.url.scheme == "mysql+asyncmy": async with raw_connection.cursor() as cursor: diff --git a/tests/test_integration.py b/tests/test_integration.py index 139f8ffe..0605529f 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,7 +1,10 @@ +import contextlib + import pytest import sqlalchemy from starlette.applications import Starlette from starlette.responses import JSONResponse +from starlette.routing import Route from starlette.testclient import TestClient from databases import Database, DatabaseURL @@ -29,6 +32,7 @@ def create_test_database(): "postgresql+aiopg", "sqlite+aiosqlite", "postgresql+asyncpg", + "postgresql+psycopg", ]: url = str(database_url.replace(driver=None)) engine = sqlalchemy.create_engine(url) @@ -45,6 +49,7 @@ def create_test_database(): "postgresql+aiopg", "sqlite+aiosqlite", "postgresql+asyncpg", + "postgresql+psycopg", ]: url = str(database_url.replace(driver=None)) engine = sqlalchemy.create_engine(url) @@ -53,17 +58,13 @@ def create_test_database(): def get_app(database_url): database = Database(database_url, force_rollback=True) - app = Starlette() - @app.on_event("startup") - async def startup(): + @contextlib.asynccontextmanager + async def lifespan(app): await database.connect() - - @app.on_event("shutdown") - async def shutdown(): + yield await database.disconnect() - @app.route("/notes", methods=["GET"]) async def list_notes(request): query = notes.select() results = await database.fetch_all(query) @@ -73,14 +74,18 @@ async def list_notes(request): ] return JSONResponse(content) - @app.route("/notes", methods=["POST"]) async def add_note(request): data = await request.json() query = notes.insert().values(text=data["text"], completed=data["completed"]) await database.execute(query) return JSONResponse({"text": data["text"], "completed": data["completed"]}) - return app + routes = [ + Route("/notes", list_notes, methods=["GET"]), + Route("/notes", add_note, methods=["POST"]), + ] + + return Starlette(routes=routes, lifespan=lifespan) @pytest.mark.parametrize("database_url", DATABASE_URLS)