Skip to content

Commit

Permalink
update connection docs
Browse files Browse the repository at this point in the history
Changes:

- improve connection docs
- test harder that the not connected warnings are appropiate.
  • Loading branch information
devkral committed Jan 8, 2025
1 parent 080031a commit 77fe3ed
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 43 deletions.
30 changes: 28 additions & 2 deletions docs/connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -34,7 +34,7 @@ framework.

with the ASGI integration:

```python hl_lines="9"
```python hl_lines="8-12"
{!> ../docs_src/connections/asgi.py !}
```

Expand All @@ -59,6 +59,32 @@ 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 !}
```

## `DatabaseNotConnectedWarning` warning

This warning appears, when an unconnected Database object is used for an operation.

Despite bailing out the warning `DatabaseNotConnectedWarning` is raised.
You should connect correctly like shown above.

!!! Note
When passing Database objects via using, make sure they are connected. They are not necessarily connected
when not in extra.

## Querying other schemas

Edgy supports that as well. Have a look at the [tenancy](./tenancy/edgy.md) section for more details.
Expand Down
3 changes: 3 additions & 0 deletions docs_src/connections/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
2 changes: 2 additions & 0 deletions docs_src/connections/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
15 changes: 15 additions & 0 deletions docs_src/connections/manual.py
Original file line number Diff line number Diff line change
@@ -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__()
17 changes: 17 additions & 0 deletions docs_src/connections/manual_esmerald.py
Original file line number Diff line number Diff line change
@@ -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))
133 changes: 92 additions & 41 deletions tests/integration/test_esmerald_create_and_return_model_list_fk.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,31 @@
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])


@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():
Expand Down Expand Up @@ -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

Expand All @@ -93,43 +89,75 @@ 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",
"language": "EN",
"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")
Expand All @@ -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,
}
11 changes: 11 additions & 0 deletions tests/test_db_connected_warnings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import warnings

import pytest

import edgy
Expand Down Expand Up @@ -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__()

0 comments on commit 77fe3ed

Please sign in to comment.