fix nested dicts, add more real life fastapi tests

This commit is contained in:
collerek
2020-08-15 12:37:48 +02:00
parent 3232c99fca
commit a0ad85811b
9 changed files with 161 additions and 26 deletions

BIN
.coverage

Binary file not shown.

View File

@ -1,16 +1,16 @@
# ORMar
<p>
<a href="https://travis-ci.com/collerek/async-orm">
<img src="https://travis-ci.com/collerek/async-orm.svg?branch=master" alt="Build Status">
<a href="https://travis-ci.com/collerek/ormar">
<img src="https://travis-ci.com/collerek/ormar.svg?branch=master" alt="Build Status">
</a>
<a href="https://codecov.io/gh/collerek/async-orm">
<img src="https://czodecov.io/gh/collerek/async-orm/branch/master/graph/badge.svg" alt="Coverage">
<a href="https://codecov.io/gh/collerek/ormar">
<img src="https://codecov.io/gh/collerek/ormar/branch/master/graph/badge.svg" alt="Coverage">
</a>
<a href="https://www.codefactor.io/repository/github/collerek/ormar">
<img src="https://www.codefactor.io/repository/github/collerek/ormar/badge" alt="CodeFactor" />
</a>
<a href="https://app.codacy.com/manual/collerek/async-orm?utm_source=github.com&utm_medium=referral&utm_content=collerek/async-orm&utm_campaign=Badge_Grade_Dashboard">
<a href="https://app.codacy.com/manual/collerek/ormar?utm_source=github.com&utm_medium=referral&utm_content=collerek/oramr&utm_campaign=Badge_Grade_Dashboard">
<img src="https://api.codacy.com/project/badge/Grade/62568734f70f49cd8ea7a1a0b2d0c107" alt="Codacy" />
</a>
</p>
@ -25,10 +25,12 @@ MySQL, and SQLite. Ormar is built with:
Because ormar is built on SQLAlchemy core, you can use [`alembic`][alembic] to provide
database migrations.
The goal was to create a simple ormar that can be used directly with [`fastapi`][fastapi] that bases it's data validation on pydantic.
The goal was to create a simple ORM that can be used directly with [`fastapi`][fastapi] that bases it's data validation on pydantic.
Initial work was inspired by [`encode/orm`][encode/orm].
The encode package was too simple (i.e. no ability to join two times to the same table) and used typesystem for data checks.
To avoid too high coupling with pydantic and sqlalchemy ormar uses them by **composition** rather than by **inheritance**.
**ormar is still under development:** We recommend pinning any dependencies with `ormar~=0.1.1`
**Note**: Use `ipython` to try this from the console, since it supports `await`.

View File

@ -117,15 +117,19 @@ class FakePydantic(list, metaclass=ModelMetaclass):
def pk_type(cls) -> Any:
return cls.__model_fields__[cls.__pkname__].__type__
def dict(self) -> Dict: # noqa: A003
def dict(self, nested=False) -> Dict: # noqa: A003
dict_instance = self.values.dict()
for field in self._extract_related_names():
nested_model = getattr(self, field)
if isinstance(nested_model, list):
dict_instance[field] = [x.dict() for x in nested_model]
if self.__model_fields__[field].virtual and nested:
continue
if isinstance(nested_model, list) and not isinstance(
nested_model, ormar.Model
):
dict_instance[field] = [x.dict(nested=True) for x in nested_model]
else:
dict_instance[field] = (
nested_model.dict() if nested_model is not None else {}
nested_model.dict(nested=True) if nested_model is not None else {}
)
return dict_instance

View File

@ -55,7 +55,7 @@ class Model(FakePydantic):
def pk(self, value: Any) -> None:
setattr(self.values, self.__pkname__, value)
async def save(self) -> int:
async def save(self) -> "Model":
self_fields = self._extract_model_db_fields()
if self.__model_fields__.get(self.__pkname__).autoincrement:
self_fields.pop(self.__pkname__, None)
@ -63,7 +63,7 @@ class Model(FakePydantic):
expr = expr.values(**self_fields)
item_id = await self.__database__.execute(expr)
self.pk = item_id
return item_id
return self
async def update(self, **kwargs: Any) -> int:
if kwargs:

View File

@ -160,10 +160,15 @@ class QuerySet:
# substitute related models with their pk
for field in self.model_cls._extract_related_names():
if field in new_kwargs and new_kwargs.get(field) is not None:
if isinstance(new_kwargs.get(field), ormar.Model):
new_kwargs[field] = getattr(
new_kwargs.get(field),
self.model_cls.__model_fields__[field].to.__pkname__,
)
else:
new_kwargs[field] = new_kwargs.get(field).get(
self.model_cls.__model_fields__[field].to.__pkname__
)
# Build the insert expression.
expr = self.table.insert()

View File

@ -36,7 +36,7 @@ class Cover(ormar.Model):
__database__ = database
id = ormar.Integer(primary_key=True)
album = ormar.ForeignKey(Album, related_name='cover_pictures')
album = ormar.ForeignKey(Album, related_name="cover_pictures")
title = ormar.String(length=100)

View File

@ -95,13 +95,16 @@ def test_sqlalchemy_table_is_created(example):
def test_no_pk_in_model_definition():
with pytest.raises(ModelDefinitionError):
class ExampleModel2(Model):
__tablename__ = "example3"
__metadata__ = metadata
test_string = fields.String(length=250)
def test_two_pks_in_model_definition():
with pytest.raises(ModelDefinitionError):
class ExampleModel2(Model):
__tablename__ = "example3"
__metadata__ = metadata
@ -111,6 +114,7 @@ def test_two_pks_in_model_definition():
def test_setting_pk_column_as_pydantic_only_in_model_definition():
with pytest.raises(ModelDefinitionError):
class ExampleModel2(Model):
__tablename__ = "example4"
__metadata__ = metadata
@ -119,6 +123,7 @@ def test_setting_pk_column_as_pydantic_only_in_model_definition():
def test_decimal_error_in_model_definition():
with pytest.raises(ModelDefinitionError):
class ExampleModel2(Model):
__tablename__ = "example4"
__metadata__ = metadata
@ -127,6 +132,7 @@ def test_decimal_error_in_model_definition():
def test_string_error_in_model_definition():
with pytest.raises(ModelDefinitionError):
class ExampleModel2(Model):
__tablename__ = "example4"
__metadata__ = metadata

View File

@ -0,0 +1,116 @@
from typing import List
import databases
import pytest
import sqlalchemy
from fastapi import FastAPI
from starlette.testclient import TestClient
import ormar
from tests.settings import DATABASE_URL
app = FastAPI()
metadata = sqlalchemy.MetaData()
database = databases.Database(DATABASE_URL, force_rollback=True)
app.state.database = database
@app.on_event("startup")
async def startup() -> None:
database_ = app.state.database
if not database_.is_connected:
await database_.connect()
@app.on_event("shutdown")
async def shutdown() -> None:
database_ = app.state.database
if database_.is_connected:
await database_.disconnect()
class Category(ormar.Model):
__tablename__ = "categories"
__metadata__ = metadata
__database__ = database
id = ormar.Integer(primary_key=True)
name = ormar.String(length=100)
class Item(ormar.Model):
__tablename__ = "items"
__metadata__ = metadata
__database__ = database
id = ormar.Integer(primary_key=True)
name = ormar.String(length=100)
category = ormar.ForeignKey(Category, nullable=True)
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@app.get("/items/", response_model=List[Item])
async def get_items():
items = await Item.objects.select_related("category").all()
return [item.dict() for item in items]
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
item = await Item.objects.create(**item.dict())
return item.dict()
@app.post("/categories/", response_model=Category)
async def create_category(category: Category):
category = await Category.objects.create(**category.dict())
return category.dict()
@app.put("/items/{item_id}")
async def get_item(item_id: int, item: Item):
item_db = await Item.objects.get(pk=item_id)
return {"updated_rows": await item_db.update(**item.dict())}
@app.delete("/items/{item_id}")
async def delete_item(item_id: int, item: Item):
item_db = await Item.objects.get(pk=item_id)
return {"deleted_rows": await item_db.delete()}
def test_all_endpoints():
client = TestClient(app)
with client as client:
response = client.post("/categories/", json={"name": "test cat"})
category = response.json()
response = client.post(
"/items/", json={"name": "test", "id": 1, "category": category}
)
item = Item(**response.json())
assert item.pk is not None
response = client.get("/items/")
items = [Item(**item) for item in response.json()]
assert items[0] == item
item.name = "New name"
response = client.put(f"/items/{item.pk}", json=item.dict())
assert response.json().get("updated_rows") == 1
response = client.get("/items/")
items = [Item(**item) for item in response.json()]
assert items[0].name == "New name"
response = client.delete(f"/items/{item.pk}", json=item.dict())
assert response.json().get("deleted_rows") == 1
response = client.get("/items/")
items = response.json()
assert len(items) == 0

View File

@ -61,7 +61,7 @@ class Teacher(ormar.Model):
category = ormar.ForeignKey(Category, nullable=True)
@pytest.fixture(scope='module')
@pytest.fixture(scope="module")
def event_loop():
loop = asyncio.get_event_loop()
yield loop
@ -92,6 +92,8 @@ async def test_model_multiple_instances_of_same_table_in_schema():
assert classes[0].name == "Math"
assert classes[0].students[0].name == "Jane"
assert len(classes[0].dict().get("students")) == 2
# related fields of main model are only populated by pk
# unless there is a required foreign key somewhere along the way
# since department is required for schoolclass it was pre loaded (again)