Skip to content

Commit

Permalink
update docs regarding connections and update related tests (#258)
Browse files Browse the repository at this point in the history
* update connection docs

Changes:

- improve connection docs
- test harder that the not connected warnings are appropiate.

* modernize integration tests

* fix py < 3.11 compatibility
  • Loading branch information
devkral authored Jan 9, 2025
1 parent 080031a commit c236738
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 66 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))
2 changes: 1 addition & 1 deletion tests/integration/test_esmerald_create_and_return_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

@pytest.fixture(autouse=True, scope="function")
async def create_test_database():
async with database:
async with models:
await models.create_all()
yield
if not database.drop:
Expand Down
135 changes: 94 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,76 @@ 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():
warnings.simplefilter("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 +170,27 @@ 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():
warnings.simplefilter("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,
}
13 changes: 6 additions & 7 deletions tests/integration/test_esmerald_fk_reference_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,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()
await models.create_all()
yield
if not database.drop:
await models.drop_all()


@pytest.fixture(autouse=True, scope="function")
Expand Down Expand Up @@ -73,8 +72,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 Down
15 changes: 7 additions & 8 deletions tests/integration/test_esmerald_tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,15 @@ async def __call__(

@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()
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:
async with models:
yield


Expand Down Expand Up @@ -113,8 +112,8 @@ def app():
def another_app():
app = Esmerald(
routes=[Gateway("/no-tenant", handler=get_products)],
on_startup=[database.connect],
on_shutdown=[database.disconnect],
on_startup=[models.__aenter__],
on_shutdown=[models.__aexit__],
)
return app

Expand Down
12 changes: 5 additions & 7 deletions tests/integration/test_esmerald_tenant_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,10 @@ async def __call__(

@pytest.fixture(autouse=True, scope="module")
async def create_test_database():
try:
await models.create_all()
yield
await models.create_all()
yield
if not database.drop:
await models.drop_all()
except Exception:
pytest.skip("No database available")


@pytest.fixture(autouse=True)
Expand All @@ -114,8 +112,8 @@ def app():
app = Esmerald(
routes=[Gateway(handler=get_products)],
middleware=[TenantMiddleware],
on_startup=[database.connect],
on_shutdown=[database.disconnect],
on_startup=[models.__aenter__],
on_shutdown=[models.__aexit__],
)
return app

Expand Down
Loading

0 comments on commit c236738

Please sign in to comment.