ormar
ormar copied to clipboard
Allow nesting ormar models in pydantic models
Describe the bug
Getting AttributeError: _orm
while building pydantic response model
Traceback
Traceback (most recent call last):
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 394, in run_asgi
result = await app(self.scope, self.receive, self.send)
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 45, in __call__
return await self.app(scope, receive, send)
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\fastapi\applications.py", line 199, in __call__
await super().__call__(scope, receive, send)
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\starlette\applications.py", line 111, in __call__
await self.middleware_stack(scope, receive, send)
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\starlette\middleware\errors.py", line 181, in __call__
raise exc from None
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\starlette\middleware\errors.py", line 159, in __call__
await self.app(scope, receive, _send)
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\starlette\exceptions.py", line 82, in __call__
raise exc from None
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\starlette\exceptions.py", line 71, in __call__
await self.app(scope, receive, sender)
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\starlette\routing.py", line 566, in __call__
await route.handle(scope, receive, send)
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\starlette\routing.py", line 227, in handle
await self.app(scope, receive, send)
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\starlette\routing.py", line 41, in app
response = await func(request)
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\fastapi\routing.py", line 218, in app
is_coroutine=is_coroutine,
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\fastapi\routing.py", line 113, in serialize_response
exclude_none=exclude_none,
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\fastapi\routing.py", line 70, in _prepare_response_content
exclude_none=exclude_none,
File "pydantic\main.py", line 459, in pydantic.main.BaseModel.dict
File "pydantic\main.py", line 802, in _iter
File "pydantic\main.py", line 697, in pydantic.main.BaseModel._get_value
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\ormar\models\newbasemodel.py", line 637, in dict
exclude=exclude, # type: ignore
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\ormar\models\newbasemodel.py", line 561, in _extract_nested_models
nested_model = getattr(self, field)
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\ormar\models\newbasemodel.py", line 269, in __getattribute__
)(item)
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\ormar\models\newbasemodel.py", line 306, in _extract_related_model_instead_of_field
if item in self._orm:
File "C:\Users\usr\AppData\Local\Programs\Python\Python37\lib\site-packages\ormar\models\newbasemodel.py", line 263, in __getattribute__
return object.__getattribute__(self, item)
AttributeError: _orm
To Reproduce
Application code
import databases
import fastapi
import ormar
import pydantic
import sqlalchemy
from uvicorn import run
from random import randint
uri: str = 'postgresql://xxx:xxx@localhost/xxx'
metadata = sqlalchemy.MetaData()
engine = sqlalchemy.create_engine(uri)
database = databases.Database(uri)
app = fastapi.FastAPI()
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 UserMeta(ormar.Model):
class Meta:
metadata = metadata
database = database
tablename = 'user_metas'
id: int = ormar.Integer(primary_key=True, autoincrement=True)
username: str = ormar.String(max_length=64, nullable=True, default=None)
title: str = ormar.String(max_length=64, nullable=True, default=None)
class User(ormar.Model):
class Meta:
metadata = metadata
database = database
tablename = 'users' # noqa
id: int = ormar.Integer(primary_key=True, autoincrement=True)
email: str = ormar.String(max_length=120, unique=True)
meta: UserMeta = ormar.ForeignKey(UserMeta)
class SomeResponse(pydantic.BaseModel):
authorized: bool = True
me: User
@app.get('/test', response_model=SomeResponse)
async def get():
meta: UserMeta = UserMeta(username='es3n1n')
await meta.save()
u: User = User(
email=f'me+{randint(555, 999)}@es3n.in',
meta=meta
)
await u.save()
return SomeResponse(me=u)
if __name__ == '__main__':
run(app, host='0.0.0.0', port=2525)
Expected behavior
Response should be {"authorized": true, "me": {...}}
Versions (please complete the following information):
- Database backend used (mysql/sqlite/postgress):
psql (PostgreSQL) 10.16 (Ubuntu 10.16-0ubuntu0.18.04.1)
- Python version:
Python 3.7.0
-
ormar
version:0.10.18
-
pydantic
version:1.8.2
- if applicable
fastapi
version:0.68.1
Additional context
Works fine only if i'm returning just u
, but if i'm wrapping it in another pydantic model, then getting this error.
Well, as a fix you could use something like return instance.__dict__.get(self.name)
instead of returning None
in RelationDescriptor::__get__
. But then it will include users
attr to model too, :shrug:.
Nesting ormar in pydantic is not really supported (another way around should be fine).
So quick fix/workaround would be:
class SomeResponse(pydantic.BaseModel):
authorized: bool = True
me: User.get_pydantic() # generate pydantic from ormar
@app.get('/test', response_model=SomeResponse)
async def get():
meta: UserMeta = UserMeta(username='es3n1n')
await meta.save()
u: User = User(
email=f'me+{randint(555, 999)}@es3n.in',
meta=meta
)
await u.save()
return SomeResponse(me=u.dict()) # pass dict not the ormar model
I will check if I can fix the descriptor in a meaningful way.
So quick fix/workaround would be:
class SomeResponse(pydantic.BaseModel): authorized: bool = True me: User.get_pydantic() # generate pydantic from ormar @app.get('/test', response_model=SomeResponse) async def get(): meta: UserMeta = UserMeta(username='es3n1n') await meta.save() u: User = User( email=f'me+{randint(555, 999)}@es3n.in', meta=meta ) await u.save() return SomeResponse(me=u.dict()) # pass dict not the ormar model
Great. Thanks, works like a charm even when i pass model instance instead of a dict
Hey collerek! This plugin is great!
My project is interested in having this functionality--is there anything I can do to help implement it?
Maybe there could be a way to take an Ormar model and return a Pydantic model (strip out all the database specific stuff and return that). That way, we could build a Pydantic-only response.
You have get_pydantic method for that https://collerek.github.io/ormar/models/methods/#get_pydantic
I am running into a similar issue, not sure if I should create another issue or re-use this one. I am using fastapi-pagination.
I have the following code - adapted from the fastapi-pagination ormar example. I just added another class and added a ForeignKey to the User table.
from typing import Any, Optional
import sqlalchemy
import uvicorn
from databases import Database
from faker import Faker
from fastapi import FastAPI
from fastapi_pagination import LimitOffsetPage, Page, add_pagination
from fastapi_pagination.ext.ormar import paginate
from ormar import ForeignKey, Integer, Model, String
faker = Faker()
metadata = sqlalchemy.MetaData()
db = Database("sqlite:///.db")
class Parent(Model):
class Meta:
tablename = "parent"
database = db
metadata = metadata
id = Integer(primary_key=True)
name = String(max_length=100)
class User(Model):
class Meta:
tablename = "users"
database = db
metadata = metadata
id = Integer(primary_key=True)
name = String(max_length=100)
email = String(max_length=100)
parent_id: Optional[Parent] = ForeignKey(Parent, related_name="user")
app = FastAPI()
@app.on_event("startup")
async def on_startup() -> None:
engine = sqlalchemy.create_engine(str(db.url))
metadata.drop_all(engine)
metadata.create_all(engine)
await db.connect()
for _ in range(10):
parent = await Parent.objects.create(
name=faker.name()
)
for _ in range(10):
await User.objects.create(
name=faker.name(),
email=faker.email(),
parent_id=parent.id
)
@app.on_event("shutdown")
async def on_shutdown() -> None:
await db.disconnect()
@app.post("/users", response_model=User)
async def create_user(user_in: User) -> Any:
return await user_in.save()
@app.get("/users/default", response_model=Page[User])
@app.get("/users/limit-offset", response_model=LimitOffsetPage[User])
async def get_users() -> Any:
return await paginate(User.objects)
add_pagination(app)
if __name__ == "__main__":
uvicorn.run("test:app")
And when trying to get users paginated, I get the following error.
Traceback (most recent call last):
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 429, in run_asgi
result = await app( # type: ignore[func-returns-value]
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
return await self.app(scope, receive, send)
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/fastapi/applications.py", line 276, in __call__
await super().__call__(scope, receive, send)
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/starlette/applications.py", line 122, in __call__
await self.middleware_stack(scope, receive, send)
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/starlette/middleware/errors.py", line 184, in __call__
raise exc
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/starlette/middleware/errors.py", line 162, in __call__
await self.app(scope, receive, _send)
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
raise exc
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
await self.app(scope, receive, sender)
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
raise e
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
await self.app(scope, receive, send)
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/starlette/routing.py", line 718, in __call__
await route.handle(scope, receive, send)
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/starlette/routing.py", line 276, in handle
await self.app(scope, receive, send)
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/starlette/routing.py", line 66, in app
response = await func(request)
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/fastapi/routing.py", line 255, in app
content = await serialize_response(
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/fastapi/routing.py", line 124, in serialize_response
response_content = _prepare_response_content(
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/fastapi/routing.py", line 79, in _prepare_response_content
return res.dict(
File "pydantic/main.py", line 450, in pydantic.main.BaseModel.dict
File "pydantic/main.py", line 869, in _iter
File "pydantic/main.py", line 795, in pydantic.main.BaseModel._get_value
File "pydantic/main.py", line 780, in genexpr
File "pydantic/main.py", line 744, in pydantic.main.BaseModel._get_value
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/ormar/models/newbasemodel.py", line 820, in dict
dict_instance = self._extract_nested_models(
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/ormar/models/newbasemodel.py", line 706, in _extract_nested_models
nested_model = getattr(self, field)
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/ormar/models/newbasemodel.py", line 211, in __getattr__
return super().__getattribute__(item)
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/ormar/models/descriptors/descriptors.py", line 105, in __get__
if self.name in instance._orm:
File "/Developing/backend.nosync/virtualenv/lib/python3.9/site-packages/ormar/models/newbasemodel.py", line 211, in __getattr__
return super().__getattribute__(item)
AttributeError: _orm