fix nested dicts, add more real life fastapi tests
This commit is contained in:
14
README.md
14
README.md
@ -1,16 +1,16 @@
|
|||||||
# ORMar
|
# ORMar
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://travis-ci.com/collerek/async-orm">
|
<a href="https://travis-ci.com/collerek/ormar">
|
||||||
<img src="https://travis-ci.com/collerek/async-orm.svg?branch=master" alt="Build Status">
|
<img src="https://travis-ci.com/collerek/ormar.svg?branch=master" alt="Build Status">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://codecov.io/gh/collerek/async-orm">
|
<a href="https://codecov.io/gh/collerek/ormar">
|
||||||
<img src="https://czodecov.io/gh/collerek/async-orm/branch/master/graph/badge.svg" alt="Coverage">
|
<img src="https://codecov.io/gh/collerek/ormar/branch/master/graph/badge.svg" alt="Coverage">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.codefactor.io/repository/github/collerek/ormar">
|
<a href="https://www.codefactor.io/repository/github/collerek/ormar">
|
||||||
<img src="https://www.codefactor.io/repository/github/collerek/ormar/badge" alt="CodeFactor" />
|
<img src="https://www.codefactor.io/repository/github/collerek/ormar/badge" alt="CodeFactor" />
|
||||||
</a>
|
</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" />
|
<img src="https://api.codacy.com/project/badge/Grade/62568734f70f49cd8ea7a1a0b2d0c107" alt="Codacy" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</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
|
Because ormar is built on SQLAlchemy core, you can use [`alembic`][alembic] to provide
|
||||||
database migrations.
|
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].
|
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.
|
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`
|
**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`.
|
**Note**: Use `ipython` to try this from the console, since it supports `await`.
|
||||||
|
|||||||
@ -117,15 +117,19 @@ class FakePydantic(list, metaclass=ModelMetaclass):
|
|||||||
def pk_type(cls) -> Any:
|
def pk_type(cls) -> Any:
|
||||||
return cls.__model_fields__[cls.__pkname__].__type__
|
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()
|
dict_instance = self.values.dict()
|
||||||
for field in self._extract_related_names():
|
for field in self._extract_related_names():
|
||||||
nested_model = getattr(self, field)
|
nested_model = getattr(self, field)
|
||||||
if isinstance(nested_model, list):
|
if self.__model_fields__[field].virtual and nested:
|
||||||
dict_instance[field] = [x.dict() for x in nested_model]
|
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:
|
else:
|
||||||
dict_instance[field] = (
|
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
|
return dict_instance
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,7 @@ class Model(FakePydantic):
|
|||||||
def pk(self, value: Any) -> None:
|
def pk(self, value: Any) -> None:
|
||||||
setattr(self.values, self.__pkname__, value)
|
setattr(self.values, self.__pkname__, value)
|
||||||
|
|
||||||
async def save(self) -> int:
|
async def save(self) -> "Model":
|
||||||
self_fields = self._extract_model_db_fields()
|
self_fields = self._extract_model_db_fields()
|
||||||
if self.__model_fields__.get(self.__pkname__).autoincrement:
|
if self.__model_fields__.get(self.__pkname__).autoincrement:
|
||||||
self_fields.pop(self.__pkname__, None)
|
self_fields.pop(self.__pkname__, None)
|
||||||
@ -63,7 +63,7 @@ class Model(FakePydantic):
|
|||||||
expr = expr.values(**self_fields)
|
expr = expr.values(**self_fields)
|
||||||
item_id = await self.__database__.execute(expr)
|
item_id = await self.__database__.execute(expr)
|
||||||
self.pk = item_id
|
self.pk = item_id
|
||||||
return item_id
|
return self
|
||||||
|
|
||||||
async def update(self, **kwargs: Any) -> int:
|
async def update(self, **kwargs: Any) -> int:
|
||||||
if kwargs:
|
if kwargs:
|
||||||
|
|||||||
@ -160,10 +160,15 @@ class QuerySet:
|
|||||||
# substitute related models with their pk
|
# substitute related models with their pk
|
||||||
for field in self.model_cls._extract_related_names():
|
for field in self.model_cls._extract_related_names():
|
||||||
if field in new_kwargs and new_kwargs.get(field) is not None:
|
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[field] = getattr(
|
||||||
new_kwargs.get(field),
|
new_kwargs.get(field),
|
||||||
self.model_cls.__model_fields__[field].to.__pkname__,
|
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.
|
# Build the insert expression.
|
||||||
expr = self.table.insert()
|
expr = self.table.insert()
|
||||||
|
|||||||
@ -36,7 +36,7 @@ class Cover(ormar.Model):
|
|||||||
__database__ = database
|
__database__ = database
|
||||||
|
|
||||||
id = ormar.Integer(primary_key=True)
|
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)
|
title = ormar.String(length=100)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -95,13 +95,16 @@ def test_sqlalchemy_table_is_created(example):
|
|||||||
|
|
||||||
def test_no_pk_in_model_definition():
|
def test_no_pk_in_model_definition():
|
||||||
with pytest.raises(ModelDefinitionError):
|
with pytest.raises(ModelDefinitionError):
|
||||||
|
|
||||||
class ExampleModel2(Model):
|
class ExampleModel2(Model):
|
||||||
__tablename__ = "example3"
|
__tablename__ = "example3"
|
||||||
__metadata__ = metadata
|
__metadata__ = metadata
|
||||||
test_string = fields.String(length=250)
|
test_string = fields.String(length=250)
|
||||||
|
|
||||||
|
|
||||||
def test_two_pks_in_model_definition():
|
def test_two_pks_in_model_definition():
|
||||||
with pytest.raises(ModelDefinitionError):
|
with pytest.raises(ModelDefinitionError):
|
||||||
|
|
||||||
class ExampleModel2(Model):
|
class ExampleModel2(Model):
|
||||||
__tablename__ = "example3"
|
__tablename__ = "example3"
|
||||||
__metadata__ = metadata
|
__metadata__ = metadata
|
||||||
@ -111,6 +114,7 @@ def test_two_pks_in_model_definition():
|
|||||||
|
|
||||||
def test_setting_pk_column_as_pydantic_only_in_model_definition():
|
def test_setting_pk_column_as_pydantic_only_in_model_definition():
|
||||||
with pytest.raises(ModelDefinitionError):
|
with pytest.raises(ModelDefinitionError):
|
||||||
|
|
||||||
class ExampleModel2(Model):
|
class ExampleModel2(Model):
|
||||||
__tablename__ = "example4"
|
__tablename__ = "example4"
|
||||||
__metadata__ = metadata
|
__metadata__ = metadata
|
||||||
@ -119,6 +123,7 @@ def test_setting_pk_column_as_pydantic_only_in_model_definition():
|
|||||||
|
|
||||||
def test_decimal_error_in_model_definition():
|
def test_decimal_error_in_model_definition():
|
||||||
with pytest.raises(ModelDefinitionError):
|
with pytest.raises(ModelDefinitionError):
|
||||||
|
|
||||||
class ExampleModel2(Model):
|
class ExampleModel2(Model):
|
||||||
__tablename__ = "example4"
|
__tablename__ = "example4"
|
||||||
__metadata__ = metadata
|
__metadata__ = metadata
|
||||||
@ -127,6 +132,7 @@ def test_decimal_error_in_model_definition():
|
|||||||
|
|
||||||
def test_string_error_in_model_definition():
|
def test_string_error_in_model_definition():
|
||||||
with pytest.raises(ModelDefinitionError):
|
with pytest.raises(ModelDefinitionError):
|
||||||
|
|
||||||
class ExampleModel2(Model):
|
class ExampleModel2(Model):
|
||||||
__tablename__ = "example4"
|
__tablename__ = "example4"
|
||||||
__metadata__ = metadata
|
__metadata__ = metadata
|
||||||
|
|||||||
116
tests/test_more_reallife_fastapi.py
Normal file
116
tests/test_more_reallife_fastapi.py
Normal 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
|
||||||
@ -61,7 +61,7 @@ class Teacher(ormar.Model):
|
|||||||
category = ormar.ForeignKey(Category, nullable=True)
|
category = ormar.ForeignKey(Category, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope="module")
|
||||||
def event_loop():
|
def event_loop():
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
yield 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].name == "Math"
|
||||||
assert classes[0].students[0].name == "Jane"
|
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
|
# related fields of main model are only populated by pk
|
||||||
# unless there is a required foreign key somewhere along the way
|
# unless there is a required foreign key somewhere along the way
|
||||||
# since department is required for schoolclass it was pre loaded (again)
|
# since department is required for schoolclass it was pre loaded (again)
|
||||||
|
|||||||
Reference in New Issue
Block a user