diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 04b5f63..6ea0c4f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,13 @@ Changelog next ==== +Improvements +------------ + +* Initial support for `asynchronous queries`_. (`#99 `_) + +.. _asynchronous queries: https://docs.djangoproject.com/en/4.1/topics/db/queries/#async-queries + Maintenance ----------- diff --git a/docs/api.rst b/docs/api.rst index a0b0625..1202bf4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -124,67 +124,124 @@ Summary of Supported APIs * - |get|_ - |check| - See [1]_ for information on the ``QuerySet`` lookup: ``'#'``. + * - |aget|_ + - |xmark| + - * - |create|_ - |xmark| - Cannot be implemented in ``QuerySetSequence``. + * - |acreate|_ + - |xmark| + - Cannot be implemented in ``QuerySetSequence``. * - |get_or_create|_ - |xmark| - Cannot be implemented in ``QuerySetSequence``. + * - |aget_or_create|_ + - |xmark| + - Cannot be implemented in ``QuerySetSequence``. * - |update_or_create|_ - |xmark| - Cannot be implemented in ``QuerySetSequence``. + * - |aupdate_or_create|_ + - |xmark| + - Cannot be implemented in ``QuerySetSequence``. * - |bulk_create|_ - |xmark| - Cannot be implemented in ``QuerySetSequence``. + * - |abulk_create|_ + - |xmark| + - Cannot be implemented in ``QuerySetSequence``. * - |bulk_update|_ - |xmark| - Cannot be implemented in ``QuerySetSequence``. + * - |abulk_update|_ + - |xmark| + - Cannot be implemented in ``QuerySetSequence``. * - |count|_ - |check| - + * - |acount|_ + - |xmark| + - * - |in_bulk|_ - |xmark| - Cannot be implemented in ``QuerySetSequence``. + * - |ain_bulk|_ + - |xmark| + - Cannot be implemented in ``QuerySetSequence``. * - |iterator|_ - |check| - + * - |aiterator|_ + - |xmark| + - * - |latest|_ - |check| - If no fields are given, ``get_latest_by`` on each model is required to be identical. + * - |alatest|_ + - |xmark| + - * - |earliest|_ - |check| - See the docuemntation for ``latest()``. + * - |aearliest|_ + - |xmark| + - * - |first|_ - |check| - If no ordering is set this is essentially the same as calling ``first()`` on the first ``QuerySet``, if there is an ordering, the result of ``first()`` for each ``QuerySet`` is compared and the "first" value is returned. + * - |afirst|_ + - |xmark| + - * - |last|_ - |check| - See the documentation for ``first()``. + * - |alast|_ + - |xmark| + - * - |aggregate|_ - |xmark| - + * - |aaggregate|_ + - |xmark| + - * - |exists|_ - |check| - + * - |aexists|_ + - |xmark| + - * - |contains|_ - |check| - + * - |acontains|_ + - |xmark| + - * - |update|_ - |check| - + * - |aupdate|_ + - |xmark| + - * - |delete|_ - |check| - + * - |adelete|_ + - |xmark| + - * - |as_manager|_ - |check| - * - |explain|_ - |check| - + * - |aexplain|_ + - |xmark| + - .. list-table:: Additional methods specific to ``QuerySetSequence`` :widths: 15 30 @@ -256,44 +313,82 @@ Summary of Supported APIs .. |get| replace:: ``get()`` .. _get: https://docs.djangoproject.com/en/dev/ref/models/querysets/#get +.. |aget| replace:: ``aget()`` +.. _aget: https://docs.djangoproject.com/en/dev/ref/models/querysets/#get .. |create| replace:: ``create()`` .. _create: https://docs.djangoproject.com/en/dev/ref/models/querysets/#create +.. |acreate| replace:: ``acreate()`` +.. _acreate: https://docs.djangoproject.com/en/dev/ref/models/querysets/#create .. |get_or_create| replace:: ``get_or_create()`` .. _get_or_create: https://docs.djangoproject.com/en/dev/ref/models/querysets/#get-or-create +.. |aget_or_create| replace:: ``aget_or_create()`` +.. _aget_or_create: https://docs.djangoproject.com/en/dev/ref/models/querysets/#get-or-create .. |update_or_create| replace:: ``update_or_create()`` .. _update_or_create: https://docs.djangoproject.com/en/dev/ref/models/querysets/#update-or-create +.. |aupdate_or_create| replace:: ``aupdate_or_create()`` +.. _aupdate_or_create: https://docs.djangoproject.com/en/dev/ref/models/querysets/#update-or-create .. |bulk_create| replace:: ``bulk_create()`` .. _bulk_create: https://docs.djangoproject.com/en/dev/ref/models/querysets/#bulk-create +.. |abulk_create| replace:: ``abulk_create()`` +.. _abulk_create: https://docs.djangoproject.com/en/dev/ref/models/querysets/#bulk-create .. |bulk_update| replace:: ``bulk_update()`` .. _bulk_update: https://docs.djangoproject.com/en/dev/ref/models/querysets/#bulk-update +.. |abulk_update| replace:: ``abulk_update()`` +.. _abulk_update: https://docs.djangoproject.com/en/dev/ref/models/querysets/#bulk-update .. |count| replace:: ``count()`` .. _count: https://docs.djangoproject.com/en/dev/ref/models/querysets/#count +.. |acount| replace:: ``acount()`` +.. _acount: https://docs.djangoproject.com/en/dev/ref/models/querysets/#count .. |in_bulk| replace:: ``in_bulk()`` .. _in_bulk: https://docs.djangoproject.com/en/dev/ref/models/querysets/#in_bulk +.. |ain_bulk| replace:: ``ain_bulk()`` +.. _ain_bulk: https://docs.djangoproject.com/en/dev/ref/models/querysets/#in_bulk .. |iterator| replace:: ``iterator()`` .. _iterator: https://docs.djangoproject.com/en/dev/ref/models/querysets/#iterator +.. |aiterator| replace:: ``aiterator()`` +.. _aiterator: https://docs.djangoproject.com/en/dev/ref/models/querysets/#iterator .. |latest| replace:: ``latest()`` .. _latest: https://docs.djangoproject.com/en/dev/ref/models/querysets/#latest +.. |alatest| replace:: ``alatest()`` +.. _alatest: https://docs.djangoproject.com/en/dev/ref/models/querysets/#latest .. |earliest| replace:: ``earliest()`` .. _earliest: https://docs.djangoproject.com/en/dev/ref/models/querysets/#earliest +.. |aearliest| replace:: ``aearliest()`` +.. _aearliest: https://docs.djangoproject.com/en/dev/ref/models/querysets/#earliest .. |first| replace:: ``first()`` .. _first: https://docs.djangoproject.com/en/dev/ref/models/querysets/#first +.. |afirst| replace:: ``afirst()`` +.. _afirst: https://docs.djangoproject.com/en/dev/ref/models/querysets/#first .. |last| replace:: ``last()`` .. _last: https://docs.djangoproject.com/en/dev/ref/models/querysets/#last +.. |alast| replace:: ``alast()`` +.. _alast: https://docs.djangoproject.com/en/dev/ref/models/querysets/#last .. |aggregate| replace:: ``aggregate()`` .. _aggregate: https://docs.djangoproject.com/en/dev/ref/models/querysets/#aggregate +.. |aaggregate| replace:: ``aaggregate()`` +.. _aaggregate: https://docs.djangoproject.com/en/dev/ref/models/querysets/#aggregate .. |exists| replace:: ``exists()`` .. _exists: https://docs.djangoproject.com/en/dev/ref/models/querysets/#exists +.. |aexists| replace:: ``aexists()`` +.. _aexists: https://docs.djangoproject.com/en/dev/ref/models/querysets/#exists .. |contains| replace:: ``contains()`` .. _contains: https://docs.djangoproject.com/en/dev/ref/models/querysets/#contains +.. |acontains| replace:: ``acontains()`` +.. _acontains: https://docs.djangoproject.com/en/dev/ref/models/querysets/#contains .. |update| replace:: ``update()`` .. _update: https://docs.djangoproject.com/en/dev/ref/models/querysets/#update +.. |aupdate| replace:: ``aupdate()`` +.. _aupdate: https://docs.djangoproject.com/en/dev/ref/models/querysets/#update .. |delete| replace:: ``delete()`` .. _delete: https://docs.djangoproject.com/en/dev/ref/models/querysets/#delete +.. |adelete| replace:: ``adelete()`` +.. _adelete: https://docs.djangoproject.com/en/dev/ref/models/querysets/#delete .. |as_manager| replace:: ``as_manager()`` .. _as_manager: https://docs.djangoproject.com/en/dev/ref/models/querysets/#as-manager .. |explain| replace:: ``explain()`` .. _explain: https://docs.djangoproject.com/en/dev/ref/models/querysets/#explain +.. |aexplain| replace:: ``aexplain()`` +.. _aexplain: https://docs.djangoproject.com/en/dev/ref/models/querysets/#explain .. |get_querysets| replace:: ``get_querysets()`` diff --git a/queryset_sequence/__init__.py b/queryset_sequence/__init__.py index 10d2263..d9686d2 100644 --- a/queryset_sequence/__init__.py +++ b/queryset_sequence/__init__.py @@ -943,32 +943,94 @@ def get(self, **kwargs): # Return the only result found. return result + if django.VERSION >= (4, 1): + + async def aget(self, **kwargs): + raise NotImplementedError() + def create(self, **kwargs): raise NotImplementedError() + if django.VERSION >= (4, 1): + + async def acreate(self, **kwargs): + raise NotImplementedError() + def get_or_create(self, defaults=None, **kwargs): raise NotImplementedError() + if django.VERSION >= (4, 1): + + async def aget_or_create(self, defaults=None, **kwargs): + raise NotImplementedError() + def update_or_create(self, defaults=None, **kwargs): raise NotImplementedError() + if django.VERSION >= (4, 1): + + async def aupdate_or_create(self, defaults=None, **kwargs): + raise NotImplementedError() + def bulk_create(self, objs, batch_size=None, ignore_conflicts=False): raise NotImplementedError() + # Django 4.1 added additional parameters. + if django.VERSION >= (4, 1): + + def bulk_create( + self, + objs, + batch_size=None, + ignore_conflicts=False, + update_conflicts=False, + update_fields=None, + unique_fields=None, + ): + raise NotImplementedError() + + async def abulk_create(self, objs, batch_size=None, ignore_conflicts=False): + raise NotImplementedError() + + else: + + def bulk_create(self, objs, batch_size=None, ignore_conflicts=False): + raise NotImplementedError() + def bulk_update(self, objs, fields, batch_size=None): raise NotImplementedError() + if django.VERSION >= (4, 1): + + async def abulk_update(self, objs, fields, batch_size=None): + raise NotImplementedError() + def count(self): return sum(qs.count() for qs in self._querysets) - self._low_mark + if django.VERSION >= (4, 1): + + async def acount(self): + raise NotImplementedError() + def in_bulk(self, id_list=None, *, field_name="pk"): raise NotImplementedError() + if django.VERSION >= (4, 1): + + async def ain_bulk(self, id_list=None, *, field_name="pk"): + raise NotImplementedError() + def iterator(self): clone = self._clone() clone._querysets = [qs.iterator() for qs in self._querysets] return clone + if django.VERSION >= (4, 1): + + async def aiterator(self): + raise NotImplementedError() + def _get_latest_by(self): """Process get_latest_by Meta on each QuerySet and return the value.""" # Get each QuerySet's get_latest_by (ignore unset values). @@ -1022,6 +1084,11 @@ def latest(self, *fields): # Return the latest. return self._get_first_or_last(objs, fields, True) + if django.VERSION >= (4, 1): + + async def alatest(self, *fields): + raise NotImplementedError() + def earliest(self, *fields): # If fields are given, fallback to get_latest_by. if not fields: @@ -1041,6 +1108,11 @@ def earliest(self, *fields): # Return the latest. return self._get_first_or_last(objs, fields, False) + if django.VERSION >= (4, 1): + + async def aearliest(self, *fields): + raise NotImplementedError() + def first(self): # If there's no QuerySets, return None. If the QuerySets are unordered, # use the first item of first QuerySet. If ordered, compare the first @@ -1057,6 +1129,11 @@ def first(self): [qs.first() for qs in self._querysets], self._order_by, False ) + if django.VERSION >= (4, 1): + + async def afirst(self): + raise NotImplementedError() + def last(self): # See the comments for first(). if not self._querysets: @@ -1071,19 +1148,44 @@ def last(self): [qs.last() for qs in self._querysets], self._order_by, True ) + if django.VERSION >= (4, 1): + + async def alast(self): + raise NotImplementedError() + def aggregate(self, *args, **kwargs): raise NotImplementedError() + if django.VERSION >= (4, 1): + + async def aaggregate(self, *args, **kwargs): + raise NotImplementedError() + def exists(self): return any(qs.exists() for qs in self._querysets) + if django.VERSION >= (4, 1): + + async def aexists(self): + raise NotImplementedError() + def contains(self, obj): return any(qs.contains(obj) for qs in self._querysets) + if django.VERSION >= (4, 1): + + async def acontains(self): + raise NotImplementedError() + def update(self, **kwargs): with transaction.atomic(): return sum(qs.update(**kwargs) for qs in self._querysets) + if django.VERSION >= (4, 1): + + async def aupdate(self, **kwargs): + raise NotImplementedError() + def delete(self): deleted_count = 0 deleted_objects = defaultdict(int) @@ -1098,12 +1200,22 @@ def delete(self): return deleted_count, dict(deleted_objects) + if django.VERSION >= (4, 1): + + async def adelete(self): + raise NotImplementedError() + def as_manager(self): raise NotImplementedError() def explain(self, format=None, **options): return "\n".join(qs.explain(format=format, **options) for qs in self._querysets) + if django.VERSION >= (4, 1): + + async def aexplain(self, format=None, **options): + raise NotImplementedError() + # Public attributes @property def ordered(self): diff --git a/tests/test_querysetsequence.py b/tests/test_querysetsequence.py index e0935d2..b6874a0 100644 --- a/tests/test_querysetsequence.py +++ b/tests/test_querysetsequence.py @@ -25,6 +25,11 @@ Publisher, ) +if django.VERSION >= (4, 1): + ImplementedIn41 = NotImplementedError +else: + ImplementedIn41 = AttributeError + class TestBase(TestCase): # The title of each Book, followed by each Article; each ordered by primary @@ -1655,26 +1660,50 @@ def test_create(self): with self.assertRaises(NotImplementedError): self.all.create() + async def test_acreate(self): + with self.assertRaises(ImplementedIn41): + await self.all.acreate() + def test_get_or_create(self): with self.assertRaises(NotImplementedError): self.all.get_or_create() + async def test_aget_or_create(self): + with self.assertRaises(ImplementedIn41): + await self.all.aget_or_create() + def test_update_or_create(self): with self.assertRaises(NotImplementedError): self.all.update_or_create() + async def test_aupdate_or_create(self): + with self.assertRaises(ImplementedIn41): + await self.all.aupdate_or_create() + def test_bulk_create(self): with self.assertRaises(NotImplementedError): self.all.bulk_create([]) + async def test_abulk_create(self): + with self.assertRaises(ImplementedIn41): + await self.all.abulk_create([]) + def test_bulk_update(self): with self.assertRaises(NotImplementedError): self.all.bulk_update([], []) + async def test_abulk_update(self): + with self.assertRaises(ImplementedIn41): + await self.all.abulk_update([], []) + def test_in_bulk(self): with self.assertRaises(NotImplementedError): self.all.in_bulk() + async def test_ain_bulk(self): + with self.assertRaises(ImplementedIn41): + await self.all.ain_bulk() + class TestNotImplemented(TestCase): """The following methods have not been implemented in QuerySetSequence.""" @@ -1710,10 +1739,62 @@ def test_raw(self): with self.assertRaises(NotImplementedError): self.all.raw("") + async def test_aget(self): + with self.assertRaises(ImplementedIn41): + await self.all.aget() + + async def test_acount(self): + with self.assertRaises(ImplementedIn41): + await self.all.acount() + + async def test_aiterator(self): + with self.assertRaises(ImplementedIn41): + await self.all.aiterator() + + async def test_alatest(self): + with self.assertRaises(ImplementedIn41): + await self.all.alatest() + + async def test_aearliest(self): + with self.assertRaises(ImplementedIn41): + await self.all.aearliest() + + async def test_afirst(self): + with self.assertRaises(ImplementedIn41): + await self.all.afirst() + + async def test_alast(self): + with self.assertRaises(ImplementedIn41): + await self.all.alast() + def test_aggregate(self): with self.assertRaises(NotImplementedError): self.all.aggregate() + async def test_aaggregate(self): + with self.assertRaises(ImplementedIn41): + await self.all.aaggregate() + + async def test_aexists(self): + with self.assertRaises(ImplementedIn41): + await self.all.aexists() + + async def test_acontains(self): + with self.assertRaises(ImplementedIn41): + await self.all.acontains() + + async def test_aupdate(self): + with self.assertRaises(ImplementedIn41): + await self.all.aupdate() + + async def test_adelete(self): + with self.assertRaises(ImplementedIn41): + await self.all.adelete() + + async def test_aexplain(self): + with self.assertRaises(ImplementedIn41): + await self.all.aexplain() + def test_alias(self): if django.VERSION > (3, 2): with self.assertRaises(NotImplementedError):