Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update docs regarding connections and update related tests #258

Merged
merged 3 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading