asgimod
This package includes components and utilities that makes django *usable* in async python, such as:
- Async model mixin
s (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:
- Does this support foreign relation access: YES.
- Does this allow queryset chaining: YES.
- Does this allow queryset iterating, slicing and indexing: YES.
- Does this affect default model manager functionality: NO, because it’s on another classproperty
aobjects
. - 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)
await pizza.aprice
await price.apizza
await price.abox
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()
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()
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]
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]
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
.
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