tortoise / tortoise-orm

Familiar asyncio ORM for python, built with relations in mind
https://tortoise.github.io
Apache License 2.0
4.67k stars 390 forks source link

turtoise pydantic model .from_queryset() NoneType object is not callable #425

Open timothyjlaurent opened 4 years ago

timothyjlaurent commented 4 years ago

Describe the bug When following the example for integrating tortoise-orm with fastapi, I see an error when trying to use pydantic_model.from_queryset: TypeError: 'NoneType' is not callable.

To Reproduce

models.py

from tortoise import fields, models
from tortoise.contrib.pydantic import pydantic_model_creator

class Atom(models.Model):

    CUI = fields.CharField(max_length=8, description="Unique identifier for concept")
    LAT = fields.CharField(max_length=3, description="Language of term")
    TS = fields.CharField(max_length=1, description="Term status")
    LUI = fields.CharField(max_length=10, description="Unique identifier for term")
    STT = fields.CharField(max_length=3, description="String type")
    SUI = fields.CharField(max_length=10, description="Unique identifier for string")
    ISPREF = fields.CharField(max_length=1, description="Atom status - preferred (Y) or not (N) for this string within this concept")
    AUI = fields.CharField(max_length=9, pk=True, description="Unique identifier for atom - variable length field, 8 or 9 characters")
    SAUI = fields.CharField(max_length=50, null=True, description="Source asserted atom identifier [optional]")
    SCUI = fields.CharField(max_length=100, null=True, description="Source asserted descriptor identifier [optional]")
    SDUI = fields.CharField(max_length=100, null=True, description="Source asserted descriptor identifier [optional]")
    SAB = fields.CharField(max_length=40, description=    """
    Abbreviated source name (SAB). Maximum field length is 20 alphanumeric characters. Two source abbreviations are assigned:
Root Source Abbreviation (RSAB) — short form, no version information, for example, AI/RHEUM, 1993, has an RSAB of "AIR"
Versioned Source Abbreviation (VSAB) — includes version information, for example, AI/RHEUM, 1993, has an VSAB of "AIR93"
Official source names, RSABs, and VSABs are included on the UMLS Source Vocabulary Documentation page.
    """)
    TTY = fields.CharField(max_length=40, description="Abbreviation for term type in source vocabulary, for example PN (Metathesaurus Preferred Name) or CD (Clinical Drug). Possible values are listed on the Abbreviations Used in Data Elements page.")
    CODE = fields.CharField(max_length=100, description="Most useful source asserted identifier (if the source vocabulary has more than one identifier), or a Metathesaurus-generated source entry identifier (if the source vocabulary has none)")
    STR = fields.TextField(description="String")
    SRL = fields.IntField(description="Source restriction level")
    SUPPRESS = fields.CharField(max_length=1, description="""
    Suppressible flag. Values = O, E, Y, or N
O: All obsolete content, whether they are obsolesced by the source or by NLM. These will include all atoms having obsolete TTYs, and other atoms becoming obsolete that have not acquired an obsolete TTY (e.g. RxNorm SCDs no longer associated with current drugs, LNC atoms derived from obsolete LNC concepts).
E: Non-obsolete content marked suppressible by an editor. These do not have a suppressible SAB/TTY combination.
Y: Non-obsolete content deemed suppressible during inversion. These can be determined by a specific SAB/TTY combination explicitly listed in MRRANK.
N: None of the above
Default suppressibility as determined by NLM (i.e., no changes at the Suppressibility tab in MetamorphoSys) should be used by most users, but may not be suitable in some specialized applications. See the MetamorphoSys Help page for information on how to change the SAB/TTY suppressibility to suit your requirements. NLM strongly recommends that users not alter editor-assigned suppressibility, and MetamorphoSys cannot be used for this purpose.
    """)
    CVF = fields.IntField(null=True, description="Content View Flag. Bit field used to flag rows included in Content View. This field is a varchar field to maximize the number of bits available for use.")

    class Meta:
        table = "MRCONSO"

MRCONSO_pydantic = pydantic_model_creator(Atom)

main.py

# pylint: disable=E0611
from typing import List, Optional

from fastapi import FastAPI
from fastapi.params import Query
from pydantic import BaseModel

from tortoise.contrib.fastapi import HTTPNotFoundError, register_tortoise

from omnimasticon.models import MRCONSO_pydantic, Atom, Atom_pydantic

app = FastAPI(title="UMLS Search")

class Status(BaseModel):
    message: str

@app.get("/atoms", response_model=List[MRCONSO_pydantic])
async def get_atoms(
        cuis: List[str] = Query(..., min_items=1),
        sabs: Optional[List[str]] = Query(None),
        limit: int = 1000):

    qs = Atom.filter(CUI__in=cuis)
    if sabs is not None:
        qs = qs.filter(SAB__in=sabs)
    qs.limit(limit)
    return await MRCONSO_pydantic.from_queryset(qs)  # The error comes from here

Expected behavior

I expect it to serialize the Atom models and make a response

Additional context By changing my code I'm able get responses like this:

@app.get("/atoms", response_model=List[MRCONSO_pydantic])
async def get_atoms(
        cuis: List[str] = Query(..., min_items=1),
        sabs: Optional[List[str]] = Query(None),
        limit: int = 1000):

    qs = Atom.filter(CUI__in=cuis)
    if sabs is not None:
        qs = qs.filter(SAB__in=sabs)
    qs.limit(limit)

    models = await qs
    ret = [await MRCONSO_pydantic.from_tortoise_orm(data) for data in models]
    return ret

or I can change the .from_queryset method from:

    @classmethod
    async def from_queryset(cls, queryset: "QuerySet") -> "List[PydanticModel]":
        """
        Returns a serializable pydantic model instance that contains a list of models,
        from the provided queryset.

        This will prefetch all the relations automatically.

        :param queryset: a queryset on the model this PydanticModel is based on.
        """

        fetch_fields = _get_fetch_fields(cls, getattr(cls.__config__, "orig_model"))
        return [cls.from_orm(e) for e in await queryset.prefetch_related(*fetch_fields)]

to :

    @classmethod
    async def from_queryset(cls, queryset: "QuerySet") -> "List[PydanticModel]":
        """
        Returns a serializable pydantic model instance that contains a list of models,
        from the provided queryset.

        This will prefetch all the relations automatically.

        :param queryset: a queryset on the model this PydanticModel is based on.
        """

        fetch_fields = _get_fetch_fields(cls, getattr(cls.__config__, "orig_model"))
        data = await queryset.prefetch_related(*fetch_fields)
        return [cls.from_orm(e) for e in data]
grigi commented 4 years ago

Thank you for this detailed bug report (It even has some proposals to fix it). I'll look at it in detail the weekend.

heylenz commented 3 years ago

Has it been fixed now?