asgimod

Package to make Django *usable* in async Python


Keywords
Django, asyncio, async
License
MIT
Install
pip install asgimod==0.1.1

Documentation

asgimod

MIT Licensed

This package includes components and utilities that makes django *usable* in async python, such as:

  • Async model mixins (fully typed), asgimod.mixins.
  • Async managers and querysets (fully typed), asgimod.db.
  • Typed sync_to_async and async_to_sync wrappers, asgimod.sync.

Package FAQ:

  1. Does this support foreign relation access: YES.
  2. Does this allow queryset chaining: YES.
  3. Does this allow queryset iterating, slicing and indexing: YES.
  4. Does this affect default model manager functionality: NO, because it’s on another classproperty aobjects.
  5. Is everything TYPED: YES, with the only exception of function parameters specification on Python<3.10 since PEP 612 is being released on 3.10.

Requirements:

  • Django >= 3.0
  • Python >= 3.8

Installation:

pip install asgimod

The documentation uses references from these model definitions:

class Topping(AsyncMixin, models.Model):
    name = models.CharField(max_length=30)


class Box(AsyncMixin, models.Model):
    name = models.CharField(max_length=50)


class Price(AsyncMixin, models.Model):
    amount = models.DecimalField(decimal_places=2, max_digits=10)
    currency = models.CharField(max_length=16, default="usd")


class Pizza(AsyncMixin, models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)
    box = models.ForeignKey(Box, null=True, on_delete=models.SET_NULL)
    price = models.OneToOneField(Price, on_delete=models.CASCADE)

and the following TypeVar:

T = TypeVar("T", bound=models.Model)


Async model mixins

This mixin adds async capabilities to the model class and instances:

  • aobjects full featured async manager.
  • asave, adelete async equivalents of save and delete.
  • a(.*) async foreign relations access.

Import:

from asgimod.mixins import AsyncMixin

Usage:

class SampleModel(AsyncMixin, models.Model):
    sample_field = models.CharField(max_length=50)

API Reference:

Extends from models.Model, uses metaclass AsyncMixinMeta (extended from models.ModelBase).


classproperty aobjects -> AsyncManager

Returns an instance of AsyncManager. Async equivalent of Model.objects.


asyncmethod asave(force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None) -> None

Async equivalent of Model.save


asyncmethod adelete(using=DEFAULT_DB_ALIAS, keep_parents=False) -> None

Async equivalent of Model.delete


getattr a(.*) -> Awaitable[T] | AsyncManyToOneRelatedManager | AsyncManyToManyRelatedManager

There are 3 possible returns from an async foreign relation access.

  • AsyncManyToOneRelatedManager: Result of a reverse many to one relation access.
  • AsyncManyToManyRelatedManager: Result of a many to many relation access (both forward and reverse access).
  • Awaitable[T]: Result of a one to one relation access or a forward many to one relation access. Returns an awaitable with T return (T being the type of the foreign object).

To access a foreign relation in async mode, add the a prefix to your sync access attribute. Using the models defined for this documentation, examples:

price = await Price.aobjects.get(id=1)
pizza = await Pizza.aobjects.get(id=1)
weird_pizza = await Pizza.aobjects.get(id=2)
bacon = await Topping.aobjects.get(id=1)
mushroom = await Topping.aobjects.get(id=2)
medium_box = await Box.aobjects.get(id=1)

# one to one rel & forward many to one rel
await pizza.aprice
await price.apizza
await price.abox

# reverse many to one rel
await medium_box.apizza_set.all().get(id=1)
await medium_box.apizza_set.filter(id__gt=1).order_by("name").count()
await medium_box.apizza_set.add(weird_pizza)
await medium_box.apizza_set.clear()

# forward many to many rel
await pizza.atoppings.all().exists()
await pizza.atoppings.add(bacon, mushroom)
await bacon.atoppings.filter(name__startswith="b").exists()
await pizza.atoppings.remove(bacon)
await pizza.atoppings.clear()

# reverse many to many rel
await mushroom.apizza_set.all().exists()
await mushroom.apizza_set.add(pizza)
await mushroom.apizza_set.set([pizza, weird_pizza])

As you have guessed, these attributes are not defined in code, and thus they are not typed, well, here's the fix:

class Topping(AsyncMixin, models.Model):
    name = models.CharField(max_length=30)
    apizza_set: AsyncManyToManyRelatedManager["Pizza"]

class Box(AsyncMixin, models.Model):
    name = models.CharField(max_length=50)
    apizza_set: AsyncManyToOneRelatedManager["Pizza"]

class Price(AsyncMixin, models.Model):
    amount = models.DecimalField(decimal_places=2, max_digits=10)
    currency = models.CharField(max_length=16, default="usd")
    apizza: "Pizza"


Async managers and querysets

Async equivalent managers and querysets. All async managers classes are only alias to their respective querysets classes. Such alias exists for user friendliness and better field typings. If you need other methods and attributes unique to managers, use objects instead.

Import:

from asgimod.db import (
    AsyncQuerySet,
    AsyncManyToOneRelatedQuerySet,
    AsyncManyToManyRelatedQuerySet,
    AsyncManager,
    AsyncManyToOneRelatedManager,
    AsyncManyToManyRelatedManager
)

API Reference:

class AsyncQuerySet[T] (alias: AsyncManager[T])


Magic methods - Iterators & Iterables:


asynciterator __aiter__ -> Iterable[T | Tuple | datetime | date | Any]

Async iterator over an AsyncQuerySet[T] using async for syntax. The type of the item evaluated queryset depends on the query made, for return type of each query please refer to the official Django QuerySet API references.

async for price in Price.aobjects.filter(currency="usd"):
    self.assertEqual(price.currency, "usd")

getitem __getitem__ -> AsyncQuerySet[T] | Awaitable[T | Tuple | datetime | date | Any]

Slicing and indexing over an AsyncQuerySet[T] using [] syntax.

Slicing an AsyncQuerySet[T] will return a new AsyncQuerySet[T], slicing using steps is not allowed, as it would evaluate the internal sync QuerySet and raises SynchronousOnlyOperation.

prices = await Price.aobjects.all()[:2].eval()
prices = await Price.aobjects.all()[1:2].eval()
prices = await Price.aobjects.all().order_by("-amount")[1:].eval()

Indexing an AsyncQuerySet[T] will return an Awaitable[T | Tuple | datetime | date | Any] (return of the awaitable depends on the query, for return type of each query please refer to the official Django QuerySet API references).

price = await Price.aobjects.all()[0]
price = await Price.aobjects.all()[:5][0]
price = await Price.aobjects.filter(amount__gte=Decimal("9.99"))[0]

Magic methods - General


builtin __repr__

Returns f"<AsyncQuerySet [...{self._cls}]>".


builtin __str__

Returns f"<AsyncQuerySet [...{self._cls}]>".


builtin __len__

Raises NotImplementedError, AsyncQuerySet does not support __len__(), use .count() instead.


builtin __bool__

Raises NotImplementedError, AsyncQuerySet does not support __bool__(), use .exists() instead.


Magic methods - Operators


operator __and__ (&)

AsyncQuerySet[T] & AsyncQuerySet[T] -> AsyncQuerySet[T]

# async qs for prices amount > 19.99
qa = Price.aobjects.filter(amount__gt=Decimal("2.99"))
qb = Price.aobjects.filter(amount__gt=Decimal("19.99"))
qs = qa & qb

operator __or__ (|)

AsyncQuerySet[T] | AsyncQuerySet[T] -> AsyncQuerySet[T]

# async qs for prices with usd and eur currency
qa = Price.aobjects.filter(currency="usd")
qb = Price.aobjects.filter(currency="eur")
qs = qa | qb

Methods for explicit evaluation of querysets


asyncmethod item(val: Union[int, Any]) -> T | Tuple | datetime | date | Any

Returns the item on index val of an AsyncQuerySet[T]. This method is used by __getitem__ internally. The return type depends on the query, for return type of each query please refer to the official Django QuerySet API references.


asyncmethod eval() -> List[T | Tuple | datetime | date | Any]

Returns the evaluated AsyncQuerySet[T] in a list. Equivalent of sync_to_async(list)(qs: QuerySet[T]). The item type of the list depends on the query, for return type of each query please refer to the official Django QuerySet API references.

toppings = await Topping.aobjects.all().eval()
toppings_start_with_B = await Topping.aobjects.filter(name__startswith="B").eval()

Methods that returns a new AsyncQuerySet[T] containing the new internal QuerySet[T].

Used for building queries. These methods are NOT async, it will not connect to the database unless evaluated by other methods or iterations. For return type and in-depth info of each method please refer to the official Django QuerySet API references.


method filter(*args, **kwargs)

Equivalent of models.Manager.filter and QuerySet.filter.


method exclude(*args, **kwargs)

Equivalent of models.Manager.exclude and QuerySet.exclude.


method annotate(*args, **kwargs)

Equivalent of models.Manager.annotate and QuerySet.annotate.


method alias(*args, **kwargs)

Equivalent of models.Manager.alias and QuerySet.alias.


method order_by(*fields)

Equivalent of models.Manager.order_by and QuerySet.order_by.


method reverse()

Equivalent of models.Manager.reverse and QuerySet.reverse.


method distinct(*fields)

Equivalent of models.Manager.distinct and QuerySet.distinct.


method values(*fields, **expressions)

Equivalent of models.Manager.values and QuerySet.values.


method values_list(*fields, flat=False, named=False)

Equivalent of models.Manager.values_list and QuerySet.values_list.


method dates(field, kind, order='ASC')

Equivalent of models.Manager.dates and QuerySet.dates.


method datetimes(field_name, kind, order='ASC', tzinfo=None, is_dst=None)

Equivalent of models.Manager.datetimes and QuerySet.datetimes.


method none()

Equivalent of models.Manager.none and QuerySet.none.


method all()

Equivalent of models.Manager.all and QuerySet.all.


method union(*other_qs: "AsyncQuerySet[T]", all=False)

Equivalent of models.Manager.union and QuerySet.union.


method intersection(*other_qs: "AsyncQuerySet[T]")

Equivalent of models.Manager.intersection and QuerySet.intersection.


method difference(*other_qs: "AsyncQuerySet[T]")

Equivalent of models.Manager.difference and QuerySet.difference.


method select_related(*fields)

Equivalent of models.Manager.select_related and QuerySet.select_related.


method prefetch_related(*lookups)

Equivalent of models.Manager.prefetch_related and QuerySet.prefetch_related.


method extra(select=None, where=None, params=None, tables=None, order_by=None, select_params=None)

Equivalent of models.Manager.extra and QuerySet.extra.


method defer(*fields)

Equivalent of models.Manager.defer and QuerySet.defer.


method only(*fields)

Equivalent of models.Manager.only and QuerySet.only.


method using(alias)

Equivalent of models.Manager.using and QuerySet.using.


method select_for_update(nowait=False, skip_locked=False, of=(), no_key=False)

Equivalent of models.Manager.select_for_update and QuerySet.select_for_update.


method raw(raw_query, params=(), translations=None, using=None)

Equivalent of models.Manager.raw and QuerySet.raw.


Methods that does NOT return a new AsyncQuerySet[T].

These methods are async and will connect to the database. For return type and in-depth info of each method please refer to the official Django QuerySet API references.


asyncmethod get(**kwargs)

Async equivalent of models.Manager.get and QuerySet.get.


asyncmethod create(**kwargs)

Async equivalent of models.Manager.create and QuerySet.create.


asyncmethod get_or_create(**kwargs)

Async equivalent of models.Manager.get_or_create and QuerySet.get_or_create.


asyncmethod update_or_create(defaults=None, **kwargs)

Async equivalent of models.Manager.update_or_create and QuerySet.update_or_create.


asyncmethod bulk_create(objs, batch_size=None, ignore_conflicts=False)

Async equivalent of models.Manager.bulk_create and QuerySet.bulk_create.


asyncmethod bulk_update(objs, fields, batch_size=None)

Async equivalent of models.Manager.bulk_update and QuerySet.bulk_update.


asyncmethod count()

Async equivalent of models.Manager.count and QuerySet.count.


asyncmethod in_bulk(id_list=None, *, field_name='pk')

Async equivalent of models.Manager.in_bulk and QuerySet.in_bulk.


asyncmethod iterator(chunk_size=2000)

Async equivalent of models.Manager.iterator and QuerySet.iterator.


asyncmethod latest(*fields)

Async equivalent of models.Manager.latest and QuerySet.latest.


asyncmethod earliest(*fields)

Async equivalent of models.Manager.earliest and QuerySet.earliest.


asyncmethod first()

Async equivalent of models.Manager.first and QuerySet.first.


asyncmethod last()

Async equivalent of models.Manager.last and QuerySet.last.


asyncmethod aggregate(*args, **kwargs)

Async equivalent of models.Manager.aggregate and QuerySet.aggregate.


asyncmethod exists()

Async equivalent of models.Manager.exists and QuerySet.exists.


asyncmethod update(**kwargs)

Async equivalent of models.Manager.update and QuerySet.update.


asyncmethod delete()

Async equivalent of models.Manager.delete and QuerySet.delete.


asyncmethod explain(format=None, **options)

Async equivalent of models.Manager.explain and QuerySet.explain.


class AsyncManyToOneRelatedQuerySet[T] (alias: AsyncManyToOneRelatedManager[T])

Extends AsyncQuerySet[T]. Manager returned for reverse many-to-one foreign relation access.


asyncmethod add(*objs, bulk=True) -> None

Async equivalent of models.fields.related_descriptors.create_reverse_many_to_one_manager.RelatedManager.add.


asyncmethod remove(*objs, bulk=True) -> None

Async equivalent of models.fields.related_descriptors.create_reverse_many_to_one_manager.RelatedManager.remove.


asyncmethod clear(*, bulk=True) -> None

Async equivalent of models.fields.related_descriptors.create_reverse_many_to_one_manager.RelatedManager.clear.


asyncmethod set(objs, *, bulk=True, clear=False) -> None

Async equivalent of models.fields.related_descriptors.create_reverse_many_to_one_manager.RelatedManager.set.


class AsyncManyToManyRelatedQuerySet[T] (alias: AsyncManyToManyRelatedManager[T])

Extends AsyncQuerySet[T]. Manager returned for many-to-many foreign relation access.


asyncmethod add(*objs, through_defaults=None) -> None

Async equivalent of models.fields.related_descriptors.create_forward_many_to_many_manager.RelatedManager.add.


asyncmethod create(*, through_defaults=None, **kwargs) -> T

Async equivalent of models.fields.related_descriptors.create_forward_many_to_many_manager.RelatedManager.create.


asyncmethod get_or_create(*, through_defaults=None, **kwargs) -> T

Async equivalent of models.fields.related_descriptors.create_forward_many_to_many_manager.RelatedManager.get_or_create.


asyncmethod update_or_create(*, through_defaults=None, **kwargs) -> T

Async equivalent of models.fields.related_descriptors.create_forward_many_to_many_manager.RelatedManager.update_or_create.


asyncmethod remove(*objs) -> None

Async equivalent of models.fields.related_descriptors.create_forward_many_to_many_manager.RelatedManager.remove.


asyncmethod clear() -> None

Async equivalent of models.fields.related_descriptors.create_forward_many_to_many_manager.RelatedManager.clear.


asyncmethod set(objs, *, clear=False, through_defaults=None) -> None

Async equivalent of models.fields.related_descriptors.create_forward_many_to_many_manager.RelatedManager.set.



Typed async and sync wrappers

As of the release of this package the sync_to_async and async_to_sync wrappers on asgiref.sync are not typed, this package provides the typed equivalent of these wrappers:

  • If project is on python<3.10, only the return type will be typed.
  • If project is on python>=3.10, both the return type and param specs will be typed (PEP 612).

Import:

from asgimod.sync import sync_to_async, async_to_sync

Usage: Same as asgiref.sync



Contribution & Development

Contributions are welcomed, there are uncovered test cases and probably missing features.

Typing the missing things in sync Django

Django itself is not doing well at typing, for example the sync managers are not typed, but please keep those out of the scope of this project as it's not related to async and asgi.

Running the tests

A django test project was used for testing, simply run

python manage.py shell