ormar icon indicating copy to clipboard operation
ormar copied to clipboard

Allow nesting ormar models in pydantic models

Open es3n1n opened this issue 3 years ago • 7 comments

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.

es3n1n avatar Sep 08 '21 23:09 es3n1n

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:.

es3n1n avatar Sep 09 '21 19:09 es3n1n

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.

collerek avatar Sep 10 '21 07:09 collerek

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

es3n1n avatar Sep 10 '21 16:09 es3n1n

Hey collerek! This plugin is great!

My project is interested in having this functionality--is there anything I can do to help implement it?

erichaydel avatar Nov 30 '21 05:11 erichaydel

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.

PythonCoderAS avatar Apr 14 '23 21:04 PythonCoderAS

You have get_pydantic method for that https://collerek.github.io/ormar/models/methods/#get_pydantic

collerek avatar Apr 15 '23 09:04 collerek

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

Mexflubber avatar Apr 18 '23 03:04 Mexflubber