I am using Tortoise-ORM with FastAPI based application. All python files are in multiple folders under src/backend/, like src/backend/db/, src/backend/schemas/, src/backend/common etc. and my PYTHONPATH env variable is set to src/.
Following are from my DB models module:
from typing import Optional
from datetime import datetime
from pydantic import Json
from tortoise.fields import (
IntField, DecimalField, CharField, DatetimeField, BooleanField,
BinaryField, JSONField, ManyToManyField)
from tortoise.fields.relational import (ForeignKeyRelation, ForeignKeyField)
from tortoise.fields import RESTRICT, ReverseRelation, ManyToManyRelation
from tortoise.models import Model
class StatusCheckMixin:
def is_active(self):
"""
Function to check if the object is in active state.
"""
return self.status == "A"
def is_inactive(self):
"""
Function to check if the object is in inactive state.
"""
return self.status == "I"
def is_deleted(self):
"""
Function to check if the object is in deleted state.
"""
return self.status == "D"
class IdentityMixin(Model, StatusCheckMixin):
"""
Provides numerical identifier and status fields to any model.
"""
id: int = IntField(
pk=True, index=True, null=False,
description="The primary identifier of this entity.")
status: str = CharField(1, default='A', index=True,
description="Status of this entity: A, I, or D.")
class Meta:
abstract = True
ordering = ("-id",) # Sort in reverse order of value of attribute id
class EntityMixin(Model, StatusCheckMixin):
"""
Entity (common entity) class for other entities to inherit from. Provides
numerical identifier, name, and status fields to any model.
"""
id: int = IntField(
pk=True, index=True, null=False,
description="The primary identifier of this entity.")
name: str = CharField(64, index=True, null=False,
description="The name of this entity.")
status: str = CharField(1, default='A', index=True,
description="Status of this entity: A, I, or D.")
def __str__(self):
return self.name
class Meta:
abstract = True
class CreatorMixin():
"""
Mixin class to define creator and creation time related attributes for
Entity type objects.
"""
created_at: Optional[datetime] = DatetimeField(
auto_now_add=True, null=False)
# TODO: Need to pull logged-in user-info to populate it automatically
# creator_id: Optional[int] = ForeignKeyField(
# "models.Authuser", to_field="id", null=True, on_delete=RESTRICT)
class Meta:
abstract = True
class LastModifierMixin():
"""
Mixin class to define modifier and modification time related attributes for
Entity type objects.
"""
last_modified_at: Optional[datetime] = DatetimeField(
null=True, index=True, auto_now=True)
# TODO: Need to pull logged-in user-info to populate it automatically
# last_modifier_id: Optional[int] = ForeignKeyField(
# "models.Authuser", to_field=id, null=True, on_delete=RESTRICT)
# last_modifier: Optional["Authuser"] = Relationship()
class Meta:
abstract = True
class RemarksMixin():
"""
Mixin class to define remarks field for an Entity type object.
"""
remarks: Optional[str] = CharField(
256, default=None, index=False, null=True)
class Meta:
abstract = True
class Authuser(EntityMixin, CreatorMixin, LastModifierMixin):
"""
Authuser data model.
"""
login_name: Optional[str] = CharField(
80, description="Login identifier string.", unique=True, null=False)
email_address: Optional[str] = CharField(
80, description="E-mail address of the user.", unique=True, null=False)
password: Optional[str] = CharField(
512, description="Password associated with the user.", null=False)
is_superuser: bool = BooleanField(
description="Indicates if the user is super-user.", null=False)
user_realms: ReverseRelation["AuthuserRealm"]
class Meta:
table = "authusers"
table_description = "Stores records of users who can authenticate."
ordering = ["name", "created_at"]
class Realm(EntityMixin, CreatorMixin, LastModifierMixin, RemarksMixin):
"""
Class to represent a realm in a multi-tenant application.
"""
code: Optional[str] = CharField(
48, description="Realm Code", unique=True, null=False)
support_pin: Optional[str] = CharField(
8, index=True, null=True,
description="PIN to authenticate with support staff.")
website_url: Optional[str] = CharField(
80, default=None, description="Website URL", null=True)
logo_url: Optional[str] = CharField(
80, default=None, description="Logo URL", null=True)
class Meta:
table = 'realms'
table_description = "Stores records of realms."
ordering = ["name"]
unique_together = (("code", "support_pin"), )
indexes = (("code", "support_pin"), )
class AuthuserRealm(IdentityMixin, RemarksMixin):
"""
The association class to hold relationship between a user and a realm.
"""
authuser: ForeignKeyRelation[Authuser] = ForeignKeyField(
"models.Authuser", related_name="user_realms", on_delete=RESTRICT,
to_field="id",
description="The authuser who is mapped to one or more realms.")
realm: ForeignKeyRelation[Realm] = ForeignKeyField(
"models.Realm", related_name="user_realms", on_delete=RESTRICT,
to_field="id",
description="The associated realm.")
profile_image_path: Optional[str] = CharField(
max_length=128, index=True, null=True,
description="The relative path of profile image.")
contact_number: Optional[str] = CharField(
max_length=16, index=True, null=True,
description="The contact number associated with this mapping.")
registered_at: Optional[datetime] = DatetimeField(
auto_now_add=True, description="Timestamp when this mapping was made.")
async def name(self) -> str:
return f"{self.authuser.name or ''} ({self.realm.code or ''})".strip()
class Meta:
table = 'authuser_realm'
table_description = "Stores records of authuser-realm mappings."
ordering = ["id"]
unique_together = (("authuser_id", "realm_id"), )
indexes = (("authuser_id", "realm_id", "contact_number", "status"), )
Following are from my schema models module:
from pydantic.main import ModelMetaclass, BaseModel
from tortoise.contrib.pydantic.base import PydanticModel
from typing import Any, Dict, Optional, Tuple
from tortoise.contrib.pydantic import pydantic_model_creator
from datetime import datetime
from tortoise import Tortoise
from backend.db import models as m
Tortoise.init_models(["backend.db.models"], "models")
class AllOptional(ModelMetaclass):
"""
Fix to enable all optional fields in models used in PUT and PATCH
HTTP methods.
"""
def __new__(mcs, name: str, bases: Tuple[type],
namespaces: Dict[str, Any], **kwargs):
annotations: dict = namespaces.get('__annotations__', {})
for base in bases:
for base_ in base.__mro__:
if base_ is BaseModel or base_ is PydanticModel:
break
annotations.update(base_.__annotations__)
for field, value in annotations.items():
if not field.startswith('__'):
annotations[field] = Optional[value]
namespaces['__annotations__'] = annotations
return super().__new__(mcs, name, bases, namespaces, **kwargs)
AuthuserIn = pydantic_model_creator(
m.Authuser, name="AuthuserIn", include=(
"name", "login_name", "email_address", "password", "is_superuser",
"status"))
AuthuserOut = pydantic_model_creator(
m.Authuser, name="AuthuserOut", exclude=("password", "user_realms"),
exclude_readonly=False)
AuthuserMod_ = pydantic_model_creator(
m.Authuser, name="AuthuserMod_", include=(
"name", "email_address", "password", "is_superuser", "status"))
class AuthuserMod(AuthuserMod_, metaclass=AllOptional):
pass
RealmIn = pydantic_model_creator(
m.Realm, name="RealmIn", include=(
"name", "code", "status", "logo_url", "website_url", "remarks"))
RealmOut = pydantic_model_creator(
m.Realm, name="RealmOut", exclude=("support_pin", "user_realms"))
RealmMod_ = pydantic_model_creator(
m.Realm, name="RealmMod_", exclude=(
"id", "code", "support_pin", "creator_id", "created_at",
"last_modifier_id", "last_modified_at", "support_pin"))
class RealmMod(RealmMod_, metaclass=AllOptional):
pass
class AuthuserRealmOut(BaseModel):
id: int
authuser_id: int
realm_id: int
registered_at: Optional[datetime]
profile_image_path: Optional[str]
contact_number: Optional[str]
remarks: Optional[str]
status: Optional[str]
class Config:
orm_mode = True
AuthuserRealmMod_ = pydantic_model_creator(
m.AuthuserRealm, name="AuthuserRealmMod_", include=(
"profile_image_path", "contact_number", "remarks", "status"))
class AuthuserRealmMod(AuthuserRealmMod_, metaclass=AllOptional):
pass
I am trying to create an API end point for /realms/ for HTTP POST request in backend/routers/realms.py and following is part of it:
from typing import List
from datetime import datetime, timezone
from fastapi import APIRouter, Query, HTTPException # , Depends
from ..schemas.models import RealmIn, RealmOut, RealmMod
from ..core.common import get_logger
from ..db import models as m
router = APIRouter(
prefix='/realms',
tags=['Realms'],
responses={404: {"description": "Realm not found."}}
)
@router.post("/", response_model=RealmOut)
async def create_realm(
realm_in: RealmIn) -> RealmOut:
"""
Create a new realm from supplied RealmIn object, persist it in DB,
and then return its Out model along with its id.
"""
attrs = realm_in.dict()
realm = await m.Realm.create(**attrs)
realmout = await RealmOut.from_orm(realm)
return realmout
I invoked a POST request to this end point with an equivalent command:
curl -X 'POST' \
'https://my.server:8000/realms/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"name": "Mina Hotels",
"status": "A",
"remarks": "Mina Hotels is a Three Star hotel.",
"code": "mina",
"website_url": "http://mina.example.in",
"logo_url": "http://mina.example.in/logo.jpg"
}'
I am getting following exception, when pydantic model's from_orm() method is invoked. I was expecting that the RealmOut model would have been received as response.
...
<snipped>
INFO: Application startup complete.
Application startup complete.
INFO: Uvicorn running on http://my.server:8000 (Press CTRL+C to quit)
Uvicorn running on http://my.server:8000 (Press CTRL+C to quit)
INSERT INTO "realms" ("name","status","created_at","last_modified_at","remarks","code","support_pin","website_url","logo_url") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING "id": ['Mina Hotels', 'A', datetime.datetime(2022, 8, 31, 11, 13, 45, 322861, tzinfo=<UTC>), datetime.datetime(2022, 8, 31, 11, 13, 45, 322997, tzinfo=<UTC>), 'Mina Hotels is a Three Start hotel.', 'mina', None, 'http://mina.example.in', 'http://images.mina.example.in/logo.jpg']
INFO: 172.18.0.2:57232 - "POST /realms/ HTTP/1.1" 500 Internal Server Error
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/anyio/streams/memory.py", line 94, in receive
return self.receive_nowait()
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/anyio/streams/memory.py", line 89, in receive_nowait
raise WouldBlock
anyio.WouldBlock
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 43, in call_next
message = await recv_stream.receive()
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/anyio/streams/memory.py", line 114, in receive
raise EndOfStream
anyio.EndOfStream
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 366, in run_asgi
result = await app(self.scope, self.receive, self.send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
return await self.app(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/applications.py", line 269, in __call__
await super().__call__(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/applications.py", line 124, in __call__
await self.middleware_stack(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/errors.py", line 184, in __call__
raise exc
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/errors.py", line 162, in __call__
await self.app(scope, receive, _send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 68, in __call__
response = await self.dispatch_func(request, call_next)
File "/config/workspace/my_project/src/backend/main.py", line 73, in set_structlog_contextvars
response = await call_next(request)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 46, in call_next
raise app_exc
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 36, in coro
await self.app(scope, request.receive, send_stream.send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/gzip.py", line 24, in __call__
await responder(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/gzip.py", line 43, in __call__
await self.app(scope, receive, self.send_with_gzip)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/cors.py", line 92, in __call__
await self.simple_response(scope, receive, send, request_headers=headers)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/cors.py", line 147, in simple_response
await self.app(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/exceptions.py", line 93, in __call__
raise exc
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/exceptions.py", line 82, in __call__
await self.app(scope, receive, sender)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
raise e
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
await self.app(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/routing.py", line 670, in __call__
await route.handle(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/routing.py", line 266, in handle
await self.app(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/routing.py", line 65, in app
response = await func(request)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/routing.py", line 227, in app
raw_response = await run_endpoint_function(
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/routing.py", line 160, in run_endpoint_function
return await dependant.call(**values)
File "/config/workspace/my_project/src/backend/routers/realms.py", line 88, in create_realm
realmout = await RealmOut.from_orm(realm)
File "pydantic/main.py", line 562, in pydantic.main.BaseModel.from_orm
File "pydantic/main.py", line 1022, in pydantic.main.validate_model
File "pydantic/fields.py", line 837, in pydantic.fields.ModelField.validate
File "pydantic/fields.py", line 1118, in pydantic.fields.ModelField._apply_validators
File "pydantic/class_validators.py", line 280, in pydantic.class_validators._generic_validator_cls.lambda3
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/tortoise/contrib/pydantic/base.py", line 59, in _tortoise_convert
return list(value)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/tortoise/fields/relational.py", line 104, in __len__
self._raise_if_not_fetched()
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/tortoise/fields/relational.py", line 163, in _raise_if_not_fetched
raise NoValuesFetched(
tortoise.exceptions.NoValuesFetched: No values were fetched for this relation, first use .fetch_related()
Exception in ASGI application
Traceback (most recent call last):
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/anyio/streams/memory.py", line 94, in receive
return self.receive_nowait()
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/anyio/streams/memory.py", line 89, in receive_nowait
raise WouldBlock
anyio.WouldBlock
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 43, in call_next
message = await recv_stream.receive()
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/anyio/streams/memory.py", line 114, in receive
raise EndOfStream
anyio.EndOfStream
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 366, in run_asgi
result = await app(self.scope, self.receive, self.send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
return await self.app(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/applications.py", line 269, in __call__
await super().__call__(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/applications.py", line 124, in __call__
await self.middleware_stack(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/errors.py", line 184, in __call__
raise exc
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/errors.py", line 162, in __call__
await self.app(scope, receive, _send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 68, in __call__
response = await self.dispatch_func(request, call_next)
File "/config/workspace/my_project/src/backend/main.py", line 73, in set_structlog_contextvars
response = await call_next(request)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 46, in call_next
raise app_exc
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 36, in coro
await self.app(scope, request.receive, send_stream.send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/gzip.py", line 24, in __call__
await responder(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/gzip.py", line 43, in __call__
await self.app(scope, receive, self.send_with_gzip)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/cors.py", line 92, in __call__
await self.simple_response(scope, receive, send, request_headers=headers)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/cors.py", line 147, in simple_response
await self.app(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/exceptions.py", line 93, in __call__
raise exc
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/exceptions.py", line 82, in __call__
await self.app(scope, receive, sender)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
raise e
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
await self.app(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/routing.py", line 670, in __call__
await route.handle(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/routing.py", line 266, in handle
await self.app(scope, receive, send)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/routing.py", line 65, in app
response = await func(request)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/routing.py", line 227, in app
raw_response = await run_endpoint_function(
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/routing.py", line 160, in run_endpoint_function
return await dependant.call(**values)
File "/config/workspace/my_project/src/backend/routers/realms.py", line 88, in create_realm
realmout = await RealmOut.from_orm(realm)
File "pydantic/main.py", line 562, in pydantic.main.BaseModel.from_orm
File "pydantic/main.py", line 1022, in pydantic.main.validate_model
File "pydantic/fields.py", line 837, in pydantic.fields.ModelField.validate
File "pydantic/fields.py", line 1118, in pydantic.fields.ModelField._apply_validators
File "pydantic/class_validators.py", line 280, in pydantic.class_validators._generic_validator_cls.lambda3
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/tortoise/contrib/pydantic/base.py", line 59, in _tortoise_convert
return list(value)
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/tortoise/fields/relational.py", line 104, in __len__
self._raise_if_not_fetched()
File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/tortoise/fields/relational.py", line 163, in _raise_if_not_fetched
raise NoValuesFetched(
tortoise.exceptions.NoValuesFetched: No values were fetched for this relation, first use .fetch_related()
Trying out solutions given for similar errors, like invoking fetch_related() on realm object has not fixed the issue. I am not sure if it is a bug in pydantic model creator code of Tortoise, or I have been doing something incorrectly. I have tried to fix the issues as suggestted below, but the error is still coming up:
I am using Tortoise-ORM with FastAPI based application. All python files are in multiple folders under src/backend/, like src/backend/db/, src/backend/schemas/, src/backend/common etc. and my PYTHONPATH env variable is set to src/.
Following are from my DB models module:
Following are from my schema models module:
I am trying to create an API end point for /realms/ for HTTP POST request in backend/routers/realms.py and following is part of it:
I invoked a POST request to this end point with an equivalent command:
I am getting following exception, when pydantic model's from_orm() method is invoked. I was expecting that the RealmOut model would have been received as response.
Trying out solutions given for similar errors, like invoking fetch_related() on realm object has not fixed the issue. I am not sure if it is a bug in pydantic model creator code of Tortoise, or I have been doing something incorrectly. I have tried to fix the issues as suggestted below, but the error is still coming up:
https://stackoverflow.com/questions/63754830/python-tortoise-orm-use-related-model-field-in-str
https://github.com/tortoise/tortoise-orm/issues/1069
I am using tortoise-orm 0.19.0 with fastapi 0.78.0.