django-ninja icon indicating copy to clipboard operation
django-ninja copied to clipboard

Async reverse relationship

Open carvalhochris opened this issue 9 months ago • 5 comments

Expectation: To be able to run this view async, should this be possible?

Output:

Traceback (most recent call last):
  File "/Users/christophercarvalho/ninja/env/lib/python3.11/site-packages/ninja/operation.py", line 282, in run
    return self._result_to_response(request, result, temporal_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/christophercarvalho/ninja/env/lib/python3.11/site-packages/ninja/operation.py", line 208, in _result_to_response
    validated_object = response_model.model_validate(
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/christophercarvalho/ninja/env/lib/python3.11/site-packages/pydantic/main.py", line 532, in model_validate
    return cls.__pydantic_validator__.validate_python(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for NinjaResponseSchema
response
  Error extracting attribute: SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async. [type=get_attribute_error, input_value=<DjangoGetter: <ninja.ope... object at 0x10a4fbe10>>, input_type=DjangoGetter]
    For further information visit https://errors.pydantic.dev/2.7/v/get_attribute_error

Schema

class CardOut(Schema):
    id: int
    title: str
    uuid: uuid.UUID
    price: float | None
    ext_file: str | None
    comp_file: str | None
    # image: str | None
    image_file: str | None
    quantity: int | None
    # includes: str | None
    inc_list: list[str]
    desc: str | None
    song: SongOut | None
    cardhold_set: list[CardToCardHoldOut] | None
    artist_set: List[ArtistOut] | None

View

@sync_to_async
def get_cards():
    queryset = (
        Card.objects.prefetch_related("cardhold_set")
    )
    return queryset


@api.get("/get-cards", response=List[CardOut])
@decorate_view(cache_page(7 * 24 * 60 * 60))
async def list_cards(request):
    cards = await get_cards()
    return cards

Models

class Card(models.Model):
    title = models.CharField(max_length=128)
    uuid = models.UUIDField(default=uuid.uuid4, editable=False)
    image = models.FileField(null=True, blank=True)
    desc = models.TextField(null=True, blank=True)
    includes = models.CharField(max_length=256, null=True, blank=True)
    song = models.ForeignKey(Song, null=True, on_delete=models.CASCADE)
    inc_media = models.URLField(null=True, blank=True)
    g_folder_id = models.CharField(max_length=42, blank=True)
    price = models.FloatField(null=True, blank=True)
    quantity = models.IntegerField(default=1)
    ext_file = models.URLField(blank=True, null=True)
    comp_file = models.URLField(blank=True, null=True)

    def __str__(self):
        return self.title

class Artist(models.Model):
    name = models.CharField(max_length=128)
    slug = models.SlugField(unique=True)
    about = models.TextField(null=True, blank=True)
    email = models.EmailField(null=True)
    instagram = models.CharField(max_length=30, blank=True)
    cards = models.ManyToManyField(Card, null=True, blank=True)

    def __str__(self):
        return self.name

class CardHold(models.Model):
    collector = models.ForeignKey(Collector, on_delete=models.CASCADE)
    card = models.ForeignKey(Card, on_delete=models.CASCADE)
    updated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f"{self.collector.name} collected {self.card.title} at {self.updated}"

carvalhochris avatar Apr 26 '24 09:04 carvalhochris

@carvalhochris

try pre fetching also artist_set

Card.objects.prefetch_related("cardhold_set", "artist_set")

vitalik avatar Apr 26 '24 10:04 vitalik

@carvalhochris

try pre fetching also artist_set

Card.objects.prefetch_related("cardhold_set", "artist_set")

Hi @vitalik thanks for your response.

I tried that and get the following:

Traceback (most recent call last):
  File "/Users/christophercarvalho/ninja/env/lib/python3.11/site-packages/ninja/operation.py", line 282, in run
    return self._result_to_response(request, result, temporal_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/christophercarvalho/ninja/env/lib/python3.11/site-packages/ninja/operation.py", line 208, in _result_to_response
    validated_object = response_model.model_validate(
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/christophercarvalho/ninja/env/lib/python3.11/site-packages/pydantic/main.py", line 532, in model_validate
    return cls.__pydantic_validator__.validate_python(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for NinjaResponseSchema
response
  Error extracting attribute: SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async. [type=get_attribute_error, input_value=<DjangoGetter: <ninja.ope... object at 0x1136c23d0>>, input_type=DjangoGetter]
    For further information visit https://errors.pydantic.dev/2.7/v/get_attribute_error

carvalhochris avatar Apr 26 '24 10:04 carvalhochris

yeah, maybe prefetch_related does not support async yet..

try:


@api.get("/get-cards", response=List[CardOut])
@decorate_view(cache_page(7 * 24 * 60 * 60))
async def list_cards(request):
    queryset = Card.objects.prefetch_related("cardhold_set", "artist_set")
    cards = await sync_to_async(list)(queryset) # force executing db query here by converting to list
    return cards

vitalik avatar Apr 26 '24 10:04 vitalik

yeah, maybe prefetch_related does not support async yet..

try:

@api.get("/get-cards", response=List[CardOut])
@decorate_view(cache_page(7 * 24 * 60 * 60))
async def list_cards(request):
    queryset = Card.objects.prefetch_related("cardhold_set", "artist_set")
    cards = await sync_to_async(list)(queryset) # force executing db query here by converting to list
    return cards

I think you might be right, it doesn't work when I include artist_set, song, or cardhold_set in the schema

carvalhochris avatar Apr 26 '24 10:04 carvalhochris

This issue is actually quite annoying, can we get it handled by django-ninja @vitalik?

metheoryt avatar Aug 27 '24 20:08 metheoryt