From cdb518c62106ed2685730a9cd7da27199a07f8e3 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 8 Jan 2025 23:39:18 +0100 Subject: [PATCH] update docs Changes: - improve connection docs - test harder that the not connected warnings are appropiate. --- docs/connection.md | 19 ++- docs_src/connections/asgi.py | 3 + docs_src/connections/django.py | 2 + docs_src/connections/manual.py | 15 ++ docs_src/connections/manual_esmerald.py | 17 +++ ...smerald_create_and_return_model_list_fk.py | 133 ++++++++++++------ tests/test_db_connected_warnings.py | 11 ++ 7 files changed, 157 insertions(+), 43 deletions(-) create mode 100644 docs_src/connections/manual.py create mode 100644 docs_src/connections/manual_esmerald.py diff --git a/docs/connection.md b/docs/connection.md index 1b0dfc35..df8a9576 100644 --- a/docs/connection.md +++ b/docs/connection.md @@ -22,7 +22,7 @@ The common lifecycle events are the following: * **on_shutdown** * **lifespan** -This document will focus on the two more commonly used, `on_startup` and `on_shutdown`. +This document will focus on the one more commonly used, `lifespan`. ## Hooking your database connection into your application @@ -34,7 +34,7 @@ framework. with the ASGI integration: -```python hl_lines="9" +```python hl_lines="8-12" {!> ../docs_src/connections/asgi.py !} ``` @@ -59,6 +59,21 @@ Django currently doesn't support the lifespan protocol. So we have a keyword par {!> ../docs_src/connections/django.py !} ``` +## Manual integration + +The `__aenter__` and `__aexit__` methods support also being called like `connect` and `disconnect`. +It is however not recommended as contextmanagers have advantages in simpler error handling. + +```python +{!> ../docs_src/connections/manual.py !} +``` + +You can use this however for an integration via `on_startup` & `on_shutdown`. + +```python +{!> ../docs_src/connections/manual_esmerald.py !} +``` + ## Querying other schemas Edgy supports that as well. Have a look at the [tenancy](./tenancy/edgy.md) section for more details. diff --git a/docs_src/connections/asgi.py b/docs_src/connections/asgi.py index 300b0f6e..998930f4 100644 --- a/docs_src/connections/asgi.py +++ b/docs_src/connections/asgi.py @@ -10,5 +10,8 @@ routes=[...], ) ) + +# check if settings are loaded +monkay.evaluate_settings_once(ignore_import_errors=False) # monkey-patch app so you can use edgy shell monkay.set_instance(Instance(registry=registry, app=app)) diff --git a/docs_src/connections/django.py b/docs_src/connections/django.py index 87b7805e..87301874 100644 --- a/docs_src/connections/django.py +++ b/docs_src/connections/django.py @@ -8,5 +8,7 @@ application = models.asgi(handle_lifespan=True)(get_asgi_application()) +# check if settings are loaded +monkay.evaluate_settings_once(ignore_import_errors=False) # monkey-patch app so you can use edgy shell monkay.set_instance(Instance(registry=registry, app=app)) diff --git a/docs_src/connections/manual.py b/docs_src/connections/manual.py new file mode 100644 index 00000000..3b2f2680 --- /dev/null +++ b/docs_src/connections/manual.py @@ -0,0 +1,15 @@ +from edgy import Registry, Instance, monkay + +models = Registry(database="sqlite:///db.sqlite", echo=True) + + +async def main(): + # check if settings are loaded + monkay.evaluate_settings_once(ignore_import_errors=False) + # monkey-patch app so you can use edgy shell + monkay.set_instance(Instance(app=app, registry=registry)) + await models.__aenter__() + try: + ... + finally: + await models.__aexit__() diff --git a/docs_src/connections/manual_esmerald.py b/docs_src/connections/manual_esmerald.py new file mode 100644 index 00000000..7903ba12 --- /dev/null +++ b/docs_src/connections/manual_esmerald.py @@ -0,0 +1,17 @@ +from contextlib import asynccontextmanager +from esmerald import Esmerald + +from edgy import Registry, Instance, monkay + +models = Registry(database="sqlite:///db.sqlite", echo=True) + + +app = Esmerald( + routes=[...], + on_startup=[models.__aenter__], + on_shutdown=[models.__aexit__], +) +# check if settings are loaded +monkay.evaluate_settings_once(ignore_import_errors=False) +# monkey-patch app so you can use edgy shell +monkay.set_instance(Instance(app=app, registry=registry)) diff --git a/tests/integration/test_esmerald_create_and_return_model_list_fk.py b/tests/integration/test_esmerald_create_and_return_model_list_fk.py index 534b6375..1b06d08e 100644 --- a/tests/integration/test_esmerald_create_and_return_model_list_fk.py +++ b/tests/integration/test_esmerald_create_and_return_model_list_fk.py @@ -1,17 +1,20 @@ -from collections.abc import AsyncGenerator +import warnings +from collections.abc import AsyncGenerator, Generator import pytest from anyio import from_thread, sleep, to_thread from esmerald import Esmerald, Gateway, post +from esmerald.testclient import EsmeraldTestClient from httpx import ASGITransport, AsyncClient from pydantic import __version__, field_validator import edgy +from edgy.exceptions import DatabaseNotConnectedWarning from edgy.testclient import DatabaseTestClient from tests.settings import DATABASE_URL database = DatabaseTestClient(DATABASE_URL) -models = edgy.Registry(database=edgy.Database(database, force_rollback=True)) +models = edgy.Registry(database=edgy.Database(database, force_rollback=False)) pytestmark = pytest.mark.anyio pydantic_version = ".".join(__version__.split(".")[:2]) @@ -19,17 +22,10 @@ @pytest.fixture(autouse=True, scope="module") async def create_test_database(): - async with database: - await models.create_all() - yield - if not database.drop: - await models.drop_all() - - -@pytest.fixture(autouse=True, scope="function") -async def rollback_transactions(): - async with models.database: - yield + await models.create_all() + yield + if not database.drop: + await models.drop_all() def blocking_function(): @@ -80,8 +76,8 @@ async def create_user(data: User) -> User: def app(): app = Esmerald( routes=[Gateway(handler=create_user)], - on_startup=[database.connect], - on_shutdown=[database.disconnect], + on_startup=[models.__aenter__], + on_shutdown=[models.__aexit__], ) return app @@ -93,35 +89,66 @@ async def async_client(app) -> AsyncGenerator: yield ac +@pytest.fixture() +def esmerald_client(app) -> Generator: + with EsmeraldTestClient(app, base_url="http://test") as ac: + yield ac + + async def test_creates_a_user_raises_value_error(async_client): - data = { - "name": "Edgy", - "email": "edgy@esmerald.dev", - "language": "EN", - "description": "A description", - } - response = await async_client.post("/create", json=data) - assert response.status_code == 400 # default from Esmerald POST - assert response.json() == { - "detail": "Validation failed for http://test/create with method POST.", - "errors": [ - { - "type": "missing", - "loc": ["posts"], - "msg": "Field required", - "input": { - "name": "Edgy", - "email": "edgy@esmerald.dev", - "language": "EN", - "description": "A description", - }, - "url": f"https://errors.pydantic.dev/{pydantic_version}/v/missing", - } - ], - } + with warnings.catch_warnings(action="error"): + data = { + "name": "Edgy", + "email": "edgy@esmerald.dev", + "language": "EN", + "description": "A description", + } + async with models: + response = await async_client.post("/create", json=data) + assert response.status_code == 400 # default from Esmerald POST + assert response.json() == { + "detail": "Validation failed for http://test/create with method POST.", + "errors": [ + { + "type": "missing", + "loc": ["posts"], + "msg": "Field required", + "input": { + "name": "Edgy", + "email": "edgy@esmerald.dev", + "language": "EN", + "description": "A description", + }, + "url": f"https://errors.pydantic.dev/{pydantic_version}/v/missing", + } + ], + } async def test_creates_a_user(async_client): + async with models: + data = { + "name": "Edgy", + "email": "edgy@esmerald.dev", + "language": "EN", + "description": "A description", + "posts": [{"comment": "A comment"}], + } + response = await async_client.post("/create", json=data) + assert response.status_code == 201 # default from Esmerald POST + reponse_json = response.json() + reponse_json.pop("id") + assert reponse_json == { + "name": "Edgy", + "email": "edgy@esmerald.dev", + "language": "EN", + "description": "A description", + "comment": "A COMMENT", + "total_posts": 1, + } + + +async def test_creates_a_user_warnings(async_client): data = { "name": "Edgy", "email": "edgy@esmerald.dev", @@ -129,7 +156,8 @@ async def test_creates_a_user(async_client): "description": "A description", "posts": [{"comment": "A comment"}], } - response = await async_client.post("/create", json=data) + with pytest.warns(DatabaseNotConnectedWarning): + response = await async_client.post("/create", json=data) assert response.status_code == 201 # default from Esmerald POST reponse_json = response.json() reponse_json.pop("id") @@ -141,3 +169,26 @@ async def test_creates_a_user(async_client): "comment": "A COMMENT", "total_posts": 1, } + + +def test_creates_a_user_sync(esmerald_client): + with warnings.catch_warnings(action="error"): + data = { + "name": "Edgy", + "email": "edgy@esmerald.dev", + "language": "EN", + "description": "A description", + "posts": [{"comment": "A comment"}], + } + response = esmerald_client.post("/create", json=data) + assert response.status_code == 201 # default from Esmerald POST + reponse_json = response.json() + reponse_json.pop("id") + assert reponse_json == { + "name": "Edgy", + "email": "edgy@esmerald.dev", + "language": "EN", + "description": "A description", + "comment": "A COMMENT", + "total_posts": 1, + } diff --git a/tests/test_db_connected_warnings.py b/tests/test_db_connected_warnings.py index 310cb8c8..c3e2054f 100644 --- a/tests/test_db_connected_warnings.py +++ b/tests/test_db_connected_warnings.py @@ -1,3 +1,5 @@ +import warnings + import pytest import edgy @@ -60,3 +62,12 @@ async def test_multiple_operations_user_warning(): with pytest.warns(UserWarning): await User.query.delete() + + +async def test_no_warning_manual_way(): + await models.__aenter__() + with warnings.catch_warnings(action="error"): + await User.query.create(name="Adam", language="EN") + await User.query.filter() + await User.query.delete() + await models.__aexit__()