├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── django_async_orm ├── __init__.py ├── apps.py ├── iter.py ├── manager.py ├── query.py ├── utils.py └── wrappers.py ├── manage.py ├── poetry.lock ├── pyproject.toml ├── tests ├── __init__.py ├── models.py ├── requirements.txt ├── settings.py ├── test_django_async_orm.py └── urls.py └── tox.ini /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ['3.10'] 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Cleanup pre-installed tools 16 | run: | 17 | # This is a fix for https://github.com/actions/virtual-environments/issues/1918 18 | sudo rm -rf /usr/share/dotnet 19 | sudo rm -rf /opt/ghc 20 | sudo rm -rf "/usr/local/share/boost" 21 | sudo rm -rf "$AGENT_TOOLSDIRECTORY" 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | python -m pip install -r tests/requirements.txt 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Lint with flake8 32 | run: | 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | - name: Black fmt 38 | run: | 39 | black --check . 40 | - name: Check imports 41 | run: | 42 | isort --check . 43 | 44 | test: 45 | 46 | runs-on: ubuntu-latest 47 | strategy: 48 | matrix: 49 | python-version: ['3.8', '3.9', '3.10'] 50 | steps: 51 | - uses: actions/checkout@v2 52 | 53 | - name: Cleanup pre-installed tools 54 | run: | 55 | # This is a fix for https://github.com/actions/virtual-environments/issues/1918 56 | sudo rm -rf /usr/share/dotnet 57 | sudo rm -rf /opt/ghc 58 | sudo rm -rf "/usr/local/share/boost" 59 | sudo rm -rf "$AGENT_TOOLSDIRECTORY" 60 | - name: Set up Python ${{ matrix.python-version }} 61 | uses: actions/setup-python@v2 62 | with: 63 | python-version: ${{ matrix.python-version }} 64 | - name: Install dependencies 65 | run: | 66 | python -m pip install --upgrade pip 67 | python -m pip install -r tests/requirements.txt 68 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 69 | - name: Test with pytest 70 | run: | 71 | tox -- --tag=ci 72 | - name: Coverage Report 73 | run: | 74 | coverage report 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Skander Ben Mahmoud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Downloads](https://static.pepy.tech/badge/django-async-orm)](https://pepy.tech/project/django-async-orm) 2 | 3 | ## Disclaimer: Don't use this module in production it's still in active development. 4 | 5 | # Django Async Orm 6 | 7 | Django module that brings async to django ORM. 8 | 9 | # Installing 10 | 11 | ``` 12 | python -m pip install django-async-orm 13 | ``` 14 | 15 | then add `django_async_orm` to your `INSTALLED_APPS` list: 16 | 17 | ```python 18 | INSTALLED_APPS = [ 19 | ..., 20 | 'django_async_orm' 21 | ] 22 | ``` 23 | 24 | # Usage 25 | 26 | Django Async Orm will patch all your existing models to add `async_*` prefixed methods. 27 | 28 | _note:_ Only non-existing methods will be patched. 29 | 30 | example: 31 | 32 | ```python 33 | class MyModel(models.Model): 34 | name = models.CharField(max_length=250) 35 | 36 | ``` 37 | 38 | you can use it as follow: 39 | 40 | ```python 41 | async def get_model(): 42 | return await MyModel.objects.aget(name="something") 43 | ``` 44 | 45 | you can also iterate over a query set with `async for`: 46 | 47 | ```python 48 | async def all_models(): 49 | all_result_set = await MyModel.objects.aall() 50 | async for obj in all_result_set: 51 | print(obj) 52 | ``` 53 | 54 | Some wrappers are also available for template rendering, form validation and login/logout 55 | 56 | #### Async login 57 | 58 | ```python 59 | from django_async_orm.wrappers import alogin 60 | 61 | async def my_async_view(request): 62 | await alogin(request) 63 | ... 64 | ``` 65 | 66 | #### Form validation 67 | 68 | ```python 69 | 70 | from django_async_orm.wrappers import aform_is_valid 71 | async def a_view(request): 72 | form = MyForm(request.POST) 73 | is_valid_form = await aform_is_valid(form) 74 | if is_valid_form: 75 | ... 76 | 77 | ``` 78 | 79 | # Django ORM support: 80 | 81 | This is an on going projects, not all model methods are ported. 82 | 83 | ### Manager: 84 | 85 | | methods | supported | comments | 86 | | ----------------------------------- | --------- | -------- | 87 | | `Model.objects.aget` | ✅ | | 88 | | `Model.objects.acreate` | ✅ | | 89 | | `Model.objects.acount` | ✅ | | 90 | | `Model.objects.anone` | ✅ | | 91 | | `Model.objects.abulk_create` | ✅ | | 92 | | `Model.objects.abulk_update` | ✅ | | 93 | | `Model.objects.aget_or_create` | ✅ | | 94 | | `Model.objects.aupdate_or_create` | ✅ | | 95 | | `Model.objects.aearliest` | ✅ | | 96 | | `Model.objects.alatest` | ✅ | | 97 | | `Model.objects.afirst` | ✅ | | 98 | | `Model.objects.alast` | ✅ | | 99 | | `Model.objects.ain_bulk` | ✅ | | 100 | | `Model.objects.adelete` | ✅ | | 101 | | `Model.objects.aupdate` | ✅ | | 102 | | `Model.objects.aexists` | ✅ | | 103 | | `Model.objects.aexplain` | ✅ | | 104 | | `Model.objects.araw` | ✅ | | 105 | | `Model.objects.aall` | ✅ | | 106 | | `Model.objects.afilter` | ✅ | | 107 | | `Model.objects.aexclude` | ✅ | | 108 | | `Model.objects.acomplex_filter` | ✅ | | 109 | | `Model.objects.aunion` | ✅ | | 110 | | `Model.objects.aintersection` | ✅ | | 111 | | `Model.objects.adifference` | ✅ | | 112 | | `Model.objects.aselect_for_update` | ✅ | | 113 | | `Model.objects.aprefetch_related` | ✅ | | 114 | | `Model.objects.aannotate` | ✅ | | 115 | | `Model.objects.aorder_by` | ✅ | | 116 | | `Model.objects.adistinct` | ✅ | | 117 | | `Model.objects.adifference` | ✅ | | 118 | | `Model.objects.aextra` | ✅ | | 119 | | `Model.objects.areverse` | ✅ | | 120 | | `Model.objects.adefer` | ✅ | | 121 | | `Model.objects.aonly` | ✅ | | 122 | | `Model.objects.ausing` | ✅ | | 123 | | `Model.objects.aresolve_expression` | ✅ | | 124 | | `Model.objects.aordered` | ✅ | | 125 | | `__aiter__` | ✅ | | 126 | | `__repr__` | ✅ | | 127 | | `__len__` | ✅ | | 128 | | `__getitem__` | ✅ | | 129 | | `Model.objects.aiterator` | ❌ | | 130 | 131 | ### RawQuerySet 132 | 133 | Not supported ❌ 134 | 135 | You can still call `Model.object.araw()` but you will be unable to access the results. 136 | 137 | ### Model: 138 | 139 | | methods | supported | comments | 140 | | --------------- | --------- | -------- | 141 | | `Model.asave` | ❌ | | 142 | | `Model.aupdate` | ❌ | | 143 | | `Model.adelete` | ❌ | | 144 | | `...` | ❌ | | 145 | 146 | ### User Model / Manager 147 | 148 | | methods | supported | comments | 149 | | --------------------------- | --------- | -------- | 150 | | `User.is_authenticated` | ✅ | | 151 | | `User.is_super_user` | ✅ | | 152 | | `User.objects.acreate_user` | ❌ | | 153 | | `...` | ❌ | | 154 | 155 | ### Foreign object lazy loading: 156 | 157 | Not supported ❌ 158 | 159 | ### Wrappers: 160 | 161 | | methods | supported | comments | 162 | | --------- | --------- | -------- | 163 | | `arender` | ✅ | | 164 | | `alogin` | ✅ | | 165 | | `alogout` | ✅ | | 166 | -------------------------------------------------------------------------------- /django_async_orm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednaks/django-async-orm/453d74dd3e762ce23720ae4e2eee8c961ef186a6/django_async_orm/__init__.py -------------------------------------------------------------------------------- /django_async_orm/apps.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.apps import AppConfig, apps 4 | 5 | from django_async_orm.utils import patch_manager 6 | 7 | 8 | class AsyncOrmConfig(AppConfig): 9 | name = "django_async_orm" 10 | 11 | def ready(self): 12 | logging.info("Patching models to add async ORM capabilities...") 13 | for model in apps.get_models(include_auto_created=True): 14 | patch_manager(model) 15 | # TODO: patch_model(model) 16 | -------------------------------------------------------------------------------- /django_async_orm/iter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | 4 | class AsyncIter: 5 | def __init__(self, iterable): 6 | self._iter = iter(iterable) 7 | 8 | def __aiter__(self): 9 | return self 10 | 11 | async def __anext__(self): 12 | try: 13 | element = next(self._iter) 14 | except StopIteration as e: 15 | raise StopAsyncIteration from e 16 | await asyncio.sleep(0) 17 | return element 18 | -------------------------------------------------------------------------------- /django_async_orm/manager.py: -------------------------------------------------------------------------------- 1 | from django.db.models.manager import BaseManager 2 | 3 | from django_async_orm.query import QuerySetAsync 4 | 5 | 6 | class AsyncManager(BaseManager.from_queryset(QuerySetAsync)): 7 | pass 8 | -------------------------------------------------------------------------------- /django_async_orm/query.py: -------------------------------------------------------------------------------- 1 | import concurrent 2 | import warnings 3 | 4 | from channels.db import database_sync_to_async as sync_to_async 5 | from django.db.models import QuerySet 6 | 7 | from django_async_orm.iter import AsyncIter 8 | 9 | 10 | def __deprecation_warning(): 11 | warnings.warn( 12 | "Methods starting with `async_*` are deprecated and will be " 13 | "removed in a future release. Use `a*` methods instead.", 14 | category=DeprecationWarning, 15 | stacklevel=2, 16 | ) 17 | 18 | 19 | def _prefer_django(method): 20 | """Decorator used to prioritize Django's QuerySet methods over our custom ones. 21 | 22 | This will help maintain performance when Django adds real async support.""" 23 | 24 | def _wrapper(self, *args, **kwargs): 25 | return getattr(super(QuerySet, self), method.__name__, method)( 26 | self, *args, **kwargs 27 | ) 28 | 29 | return _wrapper 30 | 31 | 32 | class QuerySetAsync(QuerySet): 33 | def __init__(self, model=None, query=None, using=None, hints=None): 34 | super().__init__(model, query, using, hints) 35 | 36 | @_prefer_django 37 | async def aget(self, *args, **kwargs): 38 | return await sync_to_async(self.get, thread_sensitive=True)(*args, **kwargs) 39 | 40 | @_prefer_django 41 | async def acreate(self, **kwargs): 42 | return await sync_to_async(self.create, thread_sensitive=True)(**kwargs) 43 | 44 | @_prefer_django 45 | async def abulk_create(self, obs, batch_size=None, ignore_conflicts=False): 46 | return await sync_to_async(self.bulk_create, thread_sensitive=True)( 47 | obs, batch_size=batch_size, ignore_conflicts=ignore_conflicts 48 | ) 49 | 50 | @_prefer_django 51 | async def abulk_update(self, objs, fields, batch_size=None): 52 | return await sync_to_async(self.bulk_update, thread_sensitive=True)( 53 | objs=objs, fields=fields, batch_size=batch_size 54 | ) 55 | 56 | @_prefer_django 57 | async def aget_or_create(self, defaults=None, **kwargs): 58 | return await sync_to_async(self.get_or_create, thread_sensitive=True)( 59 | defaults=defaults, **kwargs 60 | ) 61 | 62 | @_prefer_django 63 | async def aupdate_or_create(self, defaults=None, **kwargs): 64 | return await sync_to_async(self.update_or_create, thread_sensitive=True)( 65 | defaults=defaults, **kwargs 66 | ) 67 | 68 | @_prefer_django 69 | async def aearliest(self, *fields): 70 | return await sync_to_async(self.earliest, thread_sensitive=True)(*fields) 71 | 72 | @_prefer_django 73 | async def alatest(self, *fields): 74 | return await sync_to_async(self.latest, thread_sensitive=True)(*fields) 75 | 76 | @_prefer_django 77 | async def afirst(self): 78 | return await sync_to_async(self.first, thread_sensitive=True)() 79 | 80 | @_prefer_django 81 | async def anone(self): 82 | return await sync_to_async(self.none, thread_sensitive=True)() 83 | 84 | @_prefer_django 85 | async def alast(self): 86 | return await sync_to_async(self.last, thread_sensitive=True)() 87 | 88 | @_prefer_django 89 | async def ain_bulk(self, id_list=None, *_, field_name="pk"): 90 | return await sync_to_async(self.in_bulk, thread_sensitive=True)( 91 | id_list=id_list, *_, field_name=field_name 92 | ) 93 | 94 | @_prefer_django 95 | async def adelete(self): 96 | return await sync_to_async(self.delete, thread_sensitive=True)() 97 | 98 | @_prefer_django 99 | async def aupdate(self, **kwargs): 100 | return await sync_to_async(self.update, thread_sensitive=True)(**kwargs) 101 | 102 | @_prefer_django 103 | async def aexists(self): 104 | return await sync_to_async(self.exists, thread_sensitive=True)() 105 | 106 | @_prefer_django 107 | async def acount(self): 108 | return await sync_to_async(self.count, thread_sensitive=True)() 109 | 110 | @_prefer_django 111 | async def aexplain(self, *_, format=None, **options): 112 | return await sync_to_async(self.explain, thread_sensitive=True)( 113 | *_, format=format, **options 114 | ) 115 | 116 | @_prefer_django 117 | async def araw(self, raw_query, params=None, translations=None, using=None): 118 | return await sync_to_async(self.raw, thread_sensitive=True)( 119 | raw_query, params=params, translations=translations, using=using 120 | ) 121 | 122 | @_prefer_django 123 | def __aiter__(self): 124 | self._fetch_all() 125 | return AsyncIter(self._result_cache) 126 | 127 | def _fetch_all(self): 128 | with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: 129 | future_fetch_all = executor.submit(super(QuerySetAsync, self)._fetch_all) 130 | 131 | ################################################################## 132 | # PUBLIC METHODS THAT ALTER ATTRIBUTES AND RETURN A NEW QUERYSET # 133 | ################################################################## 134 | 135 | @_prefer_django 136 | async def aall(self): 137 | return await sync_to_async(self.all, thread_sensitive=True)() 138 | 139 | @_prefer_django 140 | async def afilter(self, *args, **kwargs): 141 | return await sync_to_async(self.filter, thread_sensitive=True)(*args, **kwargs) 142 | 143 | @_prefer_django 144 | async def aexclude(self, *args, **kwargs): 145 | return await sync_to_async(self.exclude, thread_sensitive=True)(*args, **kwargs) 146 | 147 | @_prefer_django 148 | async def acomplex_filter(self, filter_obj): 149 | return await sync_to_async(self.complex_filter, thread_sensitive=True)( 150 | filter_obj 151 | ) 152 | 153 | @_prefer_django 154 | async def aunion(self, *other_qs, all=False): 155 | return await sync_to_async(self.union, thread_sensitive=True)( 156 | *other_qs, all=all 157 | ) 158 | 159 | @_prefer_django 160 | async def aintersection(self, *other_qs): 161 | return await sync_to_async(self.intersection, thread_sensitive=True)(*other_qs) 162 | 163 | @_prefer_django 164 | async def adifference(self, *other_qs): 165 | return await sync_to_async(self.difference, thread_sensitive=True)(*other_qs) 166 | 167 | @_prefer_django 168 | async def aselect_for_update(self, nowait=False, skip_locked=False, of=()): 169 | return await sync_to_async(self.select_for_update, thread_sensitive=True)( 170 | nowait=nowait, skip_locked=skip_locked, of=of 171 | ) 172 | 173 | @_prefer_django 174 | async def aprefetch_related(self, *lookups): 175 | return await sync_to_async(self.prefetch_related, thread_sensitive=True)( 176 | *lookups 177 | ) 178 | 179 | @_prefer_django 180 | async def aannotate(self, *args, **kwargs): 181 | return await sync_to_async(self.annotate, thread_sensitive=True)( 182 | *args, **kwargs 183 | ) 184 | 185 | @_prefer_django 186 | async def aorder_by(self, *field_names): 187 | return await sync_to_async(self.order_by, thread_sensitive=True)(*field_names) 188 | 189 | @_prefer_django 190 | async def adistinct(self, *field_names): 191 | return await sync_to_async(self.distinct, thread_sensitive=True)(*field_names) 192 | 193 | @_prefer_django 194 | async def aextra( 195 | self, 196 | select=None, 197 | where=None, 198 | params=None, 199 | tables=None, 200 | order_by=None, 201 | select_params=None, 202 | ): 203 | return await sync_to_async(self.extra, thread_sensitive=True)( 204 | select, where, params, tables, order_by, select_params 205 | ) 206 | 207 | @_prefer_django 208 | async def areverse(self): 209 | return await sync_to_async(self.reverse, thread_sensitive=True)() 210 | 211 | @_prefer_django 212 | async def adefer(self, *fields): 213 | return await sync_to_async(self.defer, thread_sensitive=True)(*fields) 214 | 215 | @_prefer_django 216 | async def aonly(self, *fields): 217 | return await sync_to_async(self.only, thread_sensitive=True)(*fields) 218 | 219 | @_prefer_django 220 | async def ausing(self, alias): 221 | return await sync_to_async(self.using, thread_sensitive=True)(alias) 222 | 223 | @_prefer_django 224 | async def aresolve_expression(self, *args, **kwargs): 225 | return await sync_to_async(self.resolve_expression, thread_sensitive=True)( 226 | *args, **kwargs 227 | ) 228 | 229 | @property 230 | @_prefer_django 231 | async def aordered(self): 232 | def _ordered(): 233 | return super(QuerySetAsync, self).ordered 234 | 235 | return await sync_to_async(_ordered, thread_sensitive=True)() 236 | 237 | ################################# 238 | ### START OF DEPRECATION ZONE ### 239 | ################################# 240 | 241 | async def async_get(self, *args, **kwargs): 242 | __deprecation_warning() 243 | return await sync_to_async(self.get, thread_sensitive=True)(*args, **kwargs) 244 | 245 | async def async_create(self, **kwargs): 246 | __deprecation_warning() 247 | return await sync_to_async(self.create, thread_sensitive=True)(**kwargs) 248 | 249 | async def async_bulk_create(self, obs, batch_size=None, ignore_conflicts=False): 250 | __deprecation_warning() 251 | return await sync_to_async(self.bulk_create, thread_sensitive=True)( 252 | obs, batch_size=batch_size, ignore_conflicts=ignore_conflicts 253 | ) 254 | 255 | async def async_bulk_update(self, objs, fields, batch_size=None): 256 | __deprecation_warning() 257 | return await sync_to_async(self.bulk_update, thread_sensitive=True)( 258 | objs=objs, fields=fields, batch_size=batch_size 259 | ) 260 | 261 | async def async_get_or_create(self, defaults=None, **kwargs): 262 | __deprecation_warning() 263 | return await sync_to_async(self.get_or_create, thread_sensitive=True)( 264 | defaults=defaults, **kwargs 265 | ) 266 | 267 | async def async_update_or_create(self, defaults=None, **kwargs): 268 | __deprecation_warning() 269 | return await sync_to_async(self.update_or_create, thread_sensitive=True)( 270 | defaults=defaults, **kwargs 271 | ) 272 | 273 | async def async_earliest(self, *fields): 274 | __deprecation_warning() 275 | return await sync_to_async(self.earliest, thread_sensitive=True)(*fields) 276 | 277 | async def async_latest(self, *fields): 278 | __deprecation_warning() 279 | return await sync_to_async(self.latest, thread_sensitive=True)(*fields) 280 | 281 | async def async_first(self): 282 | __deprecation_warning() 283 | return await sync_to_async(self.first, thread_sensitive=True)() 284 | 285 | async def async_none(self): 286 | __deprecation_warning() 287 | return await sync_to_async(self.none, thread_sensitive=True)() 288 | 289 | async def async_last(self): 290 | __deprecation_warning() 291 | return await sync_to_async(self.last, thread_sensitive=True)() 292 | 293 | async def async_in_bulk(self, id_list=None, *_, field_name="pk"): 294 | __deprecation_warning() 295 | return await sync_to_async(self.in_bulk, thread_sensitive=True)( 296 | id_list=id_list, *_, field_name=field_name 297 | ) 298 | 299 | async def async_delete(self): 300 | __deprecation_warning() 301 | return await sync_to_async(self.delete, thread_sensitive=True)() 302 | 303 | async def async_update(self, **kwargs): 304 | __deprecation_warning() 305 | return await sync_to_async(self.update, thread_sensitive=True)(**kwargs) 306 | 307 | async def async_exists(self): 308 | __deprecation_warning() 309 | return await sync_to_async(self.exists, thread_sensitive=True)() 310 | 311 | async def async_count(self): 312 | __deprecation_warning() 313 | return await sync_to_async(self.count, thread_sensitive=True)() 314 | 315 | async def async_explain(self, *_, format=None, **options): 316 | __deprecation_warning() 317 | return await sync_to_async(self.explain, thread_sensitive=True)( 318 | *_, format=format, **options 319 | ) 320 | 321 | async def async_raw(self, raw_query, params=None, translations=None, using=None): 322 | __deprecation_warning() 323 | return await sync_to_async(self.raw, thread_sensitive=True)( 324 | raw_query, params=params, translations=translations, using=using 325 | ) 326 | 327 | async def async_all(self): 328 | __deprecation_warning() 329 | return await sync_to_async(self.all, thread_sensitive=True)() 330 | 331 | async def async_filter(self, *args, **kwargs): 332 | __deprecation_warning() 333 | return await sync_to_async(self.filter, thread_sensitive=True)(*args, **kwargs) 334 | 335 | async def async_exclude(self, *args, **kwargs): 336 | __deprecation_warning() 337 | return await sync_to_async(self.exclude, thread_sensitive=True)(*args, **kwargs) 338 | 339 | async def async_complex_filter(self, filter_obj): 340 | __deprecation_warning() 341 | return await sync_to_async(self.complex_filter, thread_sensitive=True)( 342 | filter_obj 343 | ) 344 | 345 | async def async_union(self, *other_qs, all=False): 346 | __deprecation_warning() 347 | return await sync_to_async(self.union, thread_sensitive=True)( 348 | *other_qs, all=all 349 | ) 350 | 351 | async def async_intersection(self, *other_qs): 352 | __deprecation_warning() 353 | return await sync_to_async(self.intersection, thread_sensitive=True)(*other_qs) 354 | 355 | async def async_difference(self, *other_qs): 356 | __deprecation_warning() 357 | return await sync_to_async(self.difference, thread_sensitive=True)(*other_qs) 358 | 359 | async def async_select_for_update(self, nowait=False, skip_locked=False, of=()): 360 | __deprecation_warning() 361 | return await sync_to_async(self.select_for_update, thread_sensitive=True)( 362 | nowait=nowait, skip_locked=skip_locked, of=of 363 | ) 364 | 365 | async def async_prefetch_related(self, *lookups): 366 | __deprecation_warning() 367 | return await sync_to_async(self.prefetch_related, thread_sensitive=True)( 368 | *lookups 369 | ) 370 | 371 | async def async_annotate(self, *args, **kwargs): 372 | __deprecation_warning() 373 | return await sync_to_async(self.annotate, thread_sensitive=True)( 374 | *args, **kwargs 375 | ) 376 | 377 | async def async_order_by(self, *field_names): 378 | __deprecation_warning() 379 | return await sync_to_async(self.order_by, thread_sensitive=True)(*field_names) 380 | 381 | async def async_distinct(self, *field_names): 382 | __deprecation_warning() 383 | return await sync_to_async(self.distinct, thread_sensitive=True)(*field_names) 384 | 385 | async def async_extra( 386 | self, 387 | select=None, 388 | where=None, 389 | params=None, 390 | tables=None, 391 | order_by=None, 392 | select_params=None, 393 | ): 394 | __deprecation_warning() 395 | return await sync_to_async(self.extra, thread_sensitive=True)( 396 | select, where, params, tables, order_by, select_params 397 | ) 398 | 399 | async def async_reverse(self): 400 | __deprecation_warning() 401 | return await sync_to_async(self.reverse, thread_sensitive=True)() 402 | 403 | async def async_defer(self, *fields): 404 | __deprecation_warning() 405 | return await sync_to_async(self.defer, thread_sensitive=True)(*fields) 406 | 407 | async def async_only(self, *fields): 408 | __deprecation_warning() 409 | return await sync_to_async(self.only, thread_sensitive=True)(*fields) 410 | 411 | async def async_using(self, alias): 412 | __deprecation_warning() 413 | return await sync_to_async(self.using, thread_sensitive=True)(alias) 414 | 415 | async def async_resolve_expression(self, *args, **kwargs): 416 | __deprecation_warning() 417 | return await sync_to_async(self.resolve_expression, thread_sensitive=True)( 418 | *args, **kwargs 419 | ) 420 | 421 | @property 422 | async def async_ordered(self): 423 | __deprecation_warning() 424 | 425 | def _ordered(): 426 | return super(QuerySetAsync, self).ordered 427 | 428 | return await sync_to_async(_ordered, thread_sensitive=True)() 429 | 430 | ############################### 431 | ### END OF DEPRECATION ZONE ### 432 | ############################### 433 | -------------------------------------------------------------------------------- /django_async_orm/utils.py: -------------------------------------------------------------------------------- 1 | from django_async_orm.manager import AsyncManager 2 | 3 | 4 | def mixin_async_manager_factory(model): 5 | """ 6 | Creates a new type a mixin between the base manager and the async manager. 7 | 8 | :param model: A django model class 9 | :type model: models.Model 10 | :return: A mixin type 11 | :rtype: object 12 | """ 13 | 14 | base_manager_cls = model.objects.__class__ 15 | if not base_manager_cls.__name__.startswith("MixinAsync"): 16 | return type( 17 | f"MixinAsync{base_manager_cls.__name__}", 18 | (AsyncManager, base_manager_cls), 19 | {}, 20 | ) 21 | 22 | 23 | def patch_manager(model): 24 | """ 25 | Patches django models to add async capabilities 26 | :param model: A django model class 27 | :type model: models.Model 28 | :return: None 29 | :rtype: None 30 | """ 31 | async_manager_cls = mixin_async_manager_factory(model) 32 | if async_manager_cls: 33 | model.objects = async_manager_cls() 34 | model.objects.model = model 35 | -------------------------------------------------------------------------------- /django_async_orm/wrappers.py: -------------------------------------------------------------------------------- 1 | from channels.db import database_sync_to_async as sync_to_async 2 | from django.contrib.auth import login, logout 3 | from django.shortcuts import render 4 | 5 | 6 | def _sync_form_is_valid(form_instance): 7 | return form_instance.is_valid() 8 | 9 | 10 | arender = sync_to_async(render, thread_sensitive=True) 11 | alogin = sync_to_async(login, thread_sensitive=True) 12 | alogout = sync_to_async(logout, thread_sensitive=True) 13 | aform_is_valid = sync_to_async(_sync_form_is_valid, thread_sensitive=True) 14 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import os 6 | import sys 7 | 8 | if __name__ == "__main__": 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "asgiref" 3 | version = "3.6.0" 4 | description = "ASGI specs, helper code, and adapters" 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.7" 8 | 9 | [package.dependencies] 10 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 11 | 12 | [package.extras] 13 | tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] 14 | 15 | [[package]] 16 | name = "atomicwrites" 17 | version = "1.4.1" 18 | description = "Atomic file writes." 19 | category = "dev" 20 | optional = false 21 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 22 | 23 | [[package]] 24 | name = "attrs" 25 | version = "22.2.0" 26 | description = "Classes Without Boilerplate" 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=3.6" 30 | 31 | [package.extras] 32 | cov = ["attrs", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] 33 | dev = ["attrs"] 34 | docs = ["furo", "sphinx", "myst-parser", "zope.interface", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] 35 | tests = ["attrs", "zope.interface"] 36 | tests-no-zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] 37 | tests_no_zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] 38 | 39 | [[package]] 40 | name = "black" 41 | version = "22.12.0" 42 | description = "The uncompromising code formatter." 43 | category = "dev" 44 | optional = false 45 | python-versions = ">=3.7" 46 | 47 | [package.dependencies] 48 | click = ">=8.0.0" 49 | mypy-extensions = ">=0.4.3" 50 | pathspec = ">=0.9.0" 51 | platformdirs = ">=2" 52 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 53 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} 54 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 55 | 56 | [package.extras] 57 | colorama = ["colorama (>=0.4.3)"] 58 | d = ["aiohttp (>=3.7.4)"] 59 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 60 | uvloop = ["uvloop (>=0.15.2)"] 61 | 62 | [[package]] 63 | name = "certifi" 64 | version = "2022.12.7" 65 | description = "Python package for providing Mozilla's CA Bundle." 66 | category = "dev" 67 | optional = false 68 | python-versions = ">=3.6" 69 | 70 | [[package]] 71 | name = "charset-normalizer" 72 | version = "3.0.1" 73 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 74 | category = "dev" 75 | optional = false 76 | python-versions = "*" 77 | 78 | [[package]] 79 | name = "click" 80 | version = "8.1.3" 81 | description = "Composable command line interface toolkit" 82 | category = "dev" 83 | optional = false 84 | python-versions = ">=3.7" 85 | 86 | [package.dependencies] 87 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 88 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 89 | 90 | [[package]] 91 | name = "codecov" 92 | version = "2.1.12" 93 | description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" 94 | category = "dev" 95 | optional = false 96 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 97 | 98 | [package.dependencies] 99 | coverage = "*" 100 | requests = ">=2.7.9" 101 | 102 | [[package]] 103 | name = "colorama" 104 | version = "0.4.6" 105 | description = "Cross-platform colored terminal text." 106 | category = "dev" 107 | optional = false 108 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 109 | 110 | [[package]] 111 | name = "coverage" 112 | version = "7.1.0" 113 | description = "Code coverage measurement for Python" 114 | category = "dev" 115 | optional = false 116 | python-versions = ">=3.7" 117 | 118 | [package.extras] 119 | toml = ["tomli"] 120 | 121 | [[package]] 122 | name = "distlib" 123 | version = "0.3.6" 124 | description = "Distribution utilities" 125 | category = "dev" 126 | optional = false 127 | python-versions = "*" 128 | 129 | [[package]] 130 | name = "django" 131 | version = "3.2.16" 132 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 133 | category = "dev" 134 | optional = false 135 | python-versions = ">=3.6" 136 | 137 | [package.dependencies] 138 | asgiref = ">=3.3.2,<4" 139 | pytz = "*" 140 | sqlparse = ">=0.2.2" 141 | 142 | [package.extras] 143 | argon2 = ["argon2-cffi (>=19.1.0)"] 144 | bcrypt = ["bcrypt"] 145 | 146 | [[package]] 147 | name = "filelock" 148 | version = "3.9.0" 149 | description = "A platform independent file lock." 150 | category = "dev" 151 | optional = false 152 | python-versions = ">=3.7" 153 | 154 | [package.extras] 155 | docs = ["furo (>=2022.12.7)", "sphinx-autodoc-typehints (>=1.19.5)", "sphinx (>=5.3)"] 156 | testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)", "pytest (>=7.2)"] 157 | 158 | [[package]] 159 | name = "flake8" 160 | version = "5.0.4" 161 | description = "the modular source code checker: pep8 pyflakes and co" 162 | category = "dev" 163 | optional = false 164 | python-versions = ">=3.6.1" 165 | 166 | [package.dependencies] 167 | importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} 168 | mccabe = ">=0.7.0,<0.8.0" 169 | pycodestyle = ">=2.9.0,<2.10.0" 170 | pyflakes = ">=2.5.0,<2.6.0" 171 | 172 | [[package]] 173 | name = "idna" 174 | version = "3.4" 175 | description = "Internationalized Domain Names in Applications (IDNA)" 176 | category = "dev" 177 | optional = false 178 | python-versions = ">=3.5" 179 | 180 | [[package]] 181 | name = "importlib-metadata" 182 | version = "4.2.0" 183 | description = "Read metadata from Python packages" 184 | category = "dev" 185 | optional = false 186 | python-versions = ">=3.6" 187 | 188 | [package.dependencies] 189 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 190 | zipp = ">=0.5" 191 | 192 | [package.extras] 193 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 194 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 195 | 196 | [[package]] 197 | name = "isort" 198 | version = "5.11.4" 199 | description = "A Python utility / library to sort Python imports." 200 | category = "dev" 201 | optional = false 202 | python-versions = ">=3.7.0" 203 | 204 | [package.extras] 205 | colors = ["colorama (>=0.4.3,<0.5.0)"] 206 | requirements-deprecated-finder = ["pip-api", "pipreqs"] 207 | pipfile-deprecated-finder = ["pipreqs", "requirementslib"] 208 | plugins = ["setuptools"] 209 | 210 | [[package]] 211 | name = "mccabe" 212 | version = "0.7.0" 213 | description = "McCabe checker, plugin for flake8" 214 | category = "dev" 215 | optional = false 216 | python-versions = ">=3.6" 217 | 218 | [[package]] 219 | name = "more-itertools" 220 | version = "9.0.0" 221 | description = "More routines for operating on iterables, beyond itertools" 222 | category = "dev" 223 | optional = false 224 | python-versions = ">=3.7" 225 | 226 | [[package]] 227 | name = "mypy-extensions" 228 | version = "0.4.3" 229 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 230 | category = "dev" 231 | optional = false 232 | python-versions = "*" 233 | 234 | [[package]] 235 | name = "packaging" 236 | version = "23.0" 237 | description = "Core utilities for Python packages" 238 | category = "dev" 239 | optional = false 240 | python-versions = ">=3.7" 241 | 242 | [[package]] 243 | name = "pathspec" 244 | version = "0.11.0" 245 | description = "Utility library for gitignore style pattern matching of file paths." 246 | category = "dev" 247 | optional = false 248 | python-versions = ">=3.7" 249 | 250 | [[package]] 251 | name = "platformdirs" 252 | version = "2.6.2" 253 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 254 | category = "dev" 255 | optional = false 256 | python-versions = ">=3.7" 257 | 258 | [package.dependencies] 259 | typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} 260 | 261 | [package.extras] 262 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.19.5)", "sphinx (>=5.3)"] 263 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2)"] 264 | 265 | [[package]] 266 | name = "pluggy" 267 | version = "0.13.1" 268 | description = "plugin and hook calling mechanisms for python" 269 | category = "dev" 270 | optional = false 271 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 272 | 273 | [package.dependencies] 274 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 275 | 276 | [package.extras] 277 | dev = ["pre-commit", "tox"] 278 | 279 | [[package]] 280 | name = "py" 281 | version = "1.11.0" 282 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 283 | category = "dev" 284 | optional = false 285 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 286 | 287 | [[package]] 288 | name = "pycodestyle" 289 | version = "2.9.1" 290 | description = "Python style guide checker" 291 | category = "dev" 292 | optional = false 293 | python-versions = ">=3.6" 294 | 295 | [[package]] 296 | name = "pyflakes" 297 | version = "2.5.0" 298 | description = "passive checker of Python programs" 299 | category = "dev" 300 | optional = false 301 | python-versions = ">=3.6" 302 | 303 | [[package]] 304 | name = "pytest" 305 | version = "5.4.3" 306 | description = "pytest: simple powerful testing with Python" 307 | category = "dev" 308 | optional = false 309 | python-versions = ">=3.5" 310 | 311 | [package.dependencies] 312 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 313 | attrs = ">=17.4.0" 314 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 315 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 316 | more-itertools = ">=4.0.0" 317 | packaging = "*" 318 | pluggy = ">=0.12,<1.0" 319 | py = ">=1.5.0" 320 | wcwidth = "*" 321 | 322 | [package.extras] 323 | checkqa-mypy = ["mypy (==v0.761)"] 324 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 325 | 326 | [[package]] 327 | name = "pytz" 328 | version = "2022.7.1" 329 | description = "World timezone definitions, modern and historical" 330 | category = "dev" 331 | optional = false 332 | python-versions = "*" 333 | 334 | [[package]] 335 | name = "requests" 336 | version = "2.28.2" 337 | description = "Python HTTP for Humans." 338 | category = "dev" 339 | optional = false 340 | python-versions = ">=3.7, <4" 341 | 342 | [package.dependencies] 343 | certifi = ">=2017.4.17" 344 | charset-normalizer = ">=2,<4" 345 | idna = ">=2.5,<4" 346 | urllib3 = ">=1.21.1,<1.27" 347 | 348 | [package.extras] 349 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 350 | use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] 351 | 352 | [[package]] 353 | name = "six" 354 | version = "1.16.0" 355 | description = "Python 2 and 3 compatibility utilities" 356 | category = "dev" 357 | optional = false 358 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 359 | 360 | [[package]] 361 | name = "sqlparse" 362 | version = "0.4.3" 363 | description = "A non-validating SQL parser." 364 | category = "dev" 365 | optional = false 366 | python-versions = ">=3.5" 367 | 368 | [[package]] 369 | name = "tomli" 370 | version = "2.0.1" 371 | description = "A lil' TOML parser" 372 | category = "dev" 373 | optional = false 374 | python-versions = ">=3.7" 375 | 376 | [[package]] 377 | name = "tox" 378 | version = "3.28.0" 379 | description = "tox is a generic virtualenv management and test command line tool" 380 | category = "dev" 381 | optional = false 382 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 383 | 384 | [package.dependencies] 385 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 386 | filelock = ">=3.0.0" 387 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 388 | packaging = ">=14" 389 | pluggy = ">=0.12.0" 390 | py = ">=1.4.17" 391 | six = ">=1.14.0" 392 | tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} 393 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 394 | 395 | [package.extras] 396 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 397 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] 398 | 399 | [[package]] 400 | name = "tox-pyenv" 401 | version = "1.1.0" 402 | description = "tox plugin that makes tox use `pyenv which` to find python executables" 403 | category = "dev" 404 | optional = false 405 | python-versions = "*" 406 | 407 | [package.dependencies] 408 | tox = ">=2.0" 409 | 410 | [[package]] 411 | name = "typed-ast" 412 | version = "1.5.4" 413 | description = "a fork of Python 2 and 3 ast modules with type comment support" 414 | category = "dev" 415 | optional = false 416 | python-versions = ">=3.6" 417 | 418 | [[package]] 419 | name = "typing-extensions" 420 | version = "4.4.0" 421 | description = "Backported and Experimental Type Hints for Python 3.7+" 422 | category = "dev" 423 | optional = false 424 | python-versions = ">=3.7" 425 | 426 | [[package]] 427 | name = "urllib3" 428 | version = "1.26.14" 429 | description = "HTTP library with thread-safe connection pooling, file post, and more." 430 | category = "dev" 431 | optional = false 432 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 433 | 434 | [package.extras] 435 | brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] 436 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] 437 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 438 | 439 | [[package]] 440 | name = "virtualenv" 441 | version = "20.16.2" 442 | description = "Virtual Python Environment builder" 443 | category = "dev" 444 | optional = false 445 | python-versions = ">=3.6" 446 | 447 | [package.dependencies] 448 | distlib = ">=0.3.1,<1" 449 | filelock = ">=3.2,<4" 450 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 451 | platformdirs = ">=2,<3" 452 | 453 | [package.extras] 454 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] 455 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] 456 | 457 | [[package]] 458 | name = "wcwidth" 459 | version = "0.2.6" 460 | description = "Measures the displayed width of unicode strings in a terminal" 461 | category = "dev" 462 | optional = false 463 | python-versions = "*" 464 | 465 | [[package]] 466 | name = "zipp" 467 | version = "3.11.0" 468 | description = "Backport of pathlib-compatible object wrapper for zip files" 469 | category = "dev" 470 | optional = false 471 | python-versions = ">=3.7" 472 | 473 | [package.extras] 474 | docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"] 475 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "jaraco.functools", "more-itertools", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "pytest-flake8"] 476 | 477 | [metadata] 478 | lock-version = "1.1" 479 | python-versions = "^3.7" 480 | content-hash = "c1e9e1bb7337f2a6f85cc4e667992da8670a17d43194de54d1f9fda9cb81c5c5" 481 | 482 | [metadata.files] 483 | asgiref = [] 484 | atomicwrites = [] 485 | attrs = [] 486 | black = [] 487 | certifi = [] 488 | charset-normalizer = [] 489 | click = [] 490 | codecov = [] 491 | colorama = [] 492 | coverage = [] 493 | distlib = [] 494 | django = [] 495 | filelock = [] 496 | flake8 = [] 497 | idna = [] 498 | importlib-metadata = [] 499 | isort = [] 500 | mccabe = [] 501 | more-itertools = [] 502 | mypy-extensions = [] 503 | packaging = [] 504 | pathspec = [] 505 | platformdirs = [] 506 | pluggy = [] 507 | py = [] 508 | pycodestyle = [] 509 | pyflakes = [] 510 | pytest = [] 511 | pytz = [] 512 | requests = [] 513 | six = [] 514 | sqlparse = [] 515 | tomli = [] 516 | tox = [] 517 | tox-pyenv = [] 518 | typed-ast = [] 519 | typing-extensions = [] 520 | urllib3 = [] 521 | virtualenv = [] 522 | wcwidth = [] 523 | zipp = [] 524 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-async-orm" 3 | version = "0.1.12" 4 | description = "Bringing async capabilities to django ORM" 5 | authors = ["SkanderBM "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/rednaks/django-async-orm" 9 | repository = "https://github.com/rednaks/django-async-orm" 10 | keywords = ["django", "async", "orm"] 11 | classifiers = [ 12 | "Development Status :: 3 - Alpha", 13 | "Framework :: Django", 14 | "Operating System :: OS Independent", 15 | "Topic :: Software Development :: Documentation", 16 | "Topic :: Software Development :: Libraries :: Python Modules", 17 | "Topic :: Software Development :: Quality Assurance", 18 | 'Programming Language :: Python :: 3.8', 19 | 'Programming Language :: Python :: 3.9', 20 | 'Programming Language :: Python :: 3.10', 21 | ] 22 | include = [ 23 | "LICENSE", 24 | ] 25 | 26 | [tool.poetry.dependencies] 27 | python = "^3.8" 28 | channels = "^2.1.2" 29 | 30 | [tool.poetry.dev-dependencies] 31 | pytest = "^5.2" 32 | Django = "^3.2.3" 33 | black = "^22.12.0" 34 | isort = "^5.11.4" 35 | flake8 = "5.0.4" 36 | coverage = "^7.1.0" 37 | tox = "3.28.0" 38 | tox-pyenv = "^1.1.0" 39 | codecov = "^2.1.12" 40 | 41 | [build-system] 42 | requires = ["poetry-core>=1.0.0"] 43 | build-backend = "poetry.core.masonry.api" 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednaks/django-async-orm/453d74dd3e762ce23720ae4e2eee8c961ef186a6/tests/__init__.py -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | from django.db import models 6 | 7 | 8 | class TestModel(models.Model): 9 | name = models.CharField(max_length=50, null=True, blank=True) 10 | obj_type = models.CharField(max_length=50, null=True, blank=True) 11 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | black 2 | codecov 3 | coverage 4 | flake8 5 | isort 6 | pytest 7 | tox 8 | channels>=2.1.2 9 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | """Django settings.""" 2 | # -*- coding: utf-8 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | # import django 6 | 7 | DEBUG = True 8 | USE_TZ = True 9 | 10 | # SECURITY WARNING: keep the secret key used in production secret! 11 | SECRET_KEY = "-%n*2l!dqp6wxjnz4kgv5y=2m6en@l495gb9@&$#o89%8oy75g" 12 | 13 | DATABASES = { 14 | "default": { 15 | "ENGINE": "django.db.backends.sqlite3", 16 | #'NAME': ':memory:', 17 | "TEST": { 18 | "NAME": "testdb.sqlite3", 19 | }, 20 | "OPTIONS": {"timeout": 5}, 21 | } 22 | } 23 | 24 | ROOT_URLCONF = "tests.urls" 25 | 26 | INSTALLED_APPS = [ 27 | "django_async_orm.apps.AsyncOrmConfig", 28 | "tests", 29 | ] 30 | 31 | MIDDLEWARE = [] 32 | 33 | 34 | SITE_ID = 1 35 | 36 | LOGGING = { 37 | "version": 1, 38 | "disable_existing_loggers": False, 39 | "handlers": { 40 | "console": { 41 | "class": "logging.StreamHandler", 42 | }, 43 | }, 44 | "root": { 45 | "handlers": ["console"], 46 | "level": "WARNING", 47 | }, 48 | } 49 | 50 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 51 | -------------------------------------------------------------------------------- /tests/test_django_async_orm.py: -------------------------------------------------------------------------------- 1 | from unittest import IsolatedAsyncioTestCase 2 | 3 | from django.apps import apps 4 | from django.test import TestCase, TransactionTestCase, tag 5 | 6 | from .models import TestModel 7 | 8 | 9 | class AppLoadingTestCase(TestCase): 10 | @tag("ci") 11 | def test_dao_loaded(self): 12 | self.assertTrue(apps.is_installed("django_async_orm")) 13 | 14 | @tag("ci") 15 | def test_manager_is_async(self): 16 | manager_class_name = TestModel.objects.__class__.__name__ 17 | self.assertTrue( 18 | manager_class_name.startswith("MixinAsync"), 19 | f'Manager class name is {manager_class_name} but should start with "MixinAsync"', 20 | ) 21 | 22 | 23 | class ModelTestCase(TransactionTestCase, IsolatedAsyncioTestCase): 24 | async def asyncSetUp(self): 25 | await TestModel.objects.acreate(name="setup 1", obj_type="setup") 26 | await TestModel.objects.acreate(name="setup 2", obj_type="setup") 27 | 28 | async def asyncTearDown(self): 29 | await TestModel.objects.adelete() 30 | 31 | @tag("ci") 32 | async def test_async_get(self): 33 | result = await TestModel.objects.aget(name="setup 1") 34 | self.assertEqual(result.name, "setup 1") 35 | 36 | @tag("ci") 37 | async def test_async_create(self): 38 | result = await TestModel.objects.acreate(name="test") 39 | self.assertEqual(result.name, "test") 40 | 41 | @tag("ci") 42 | async def test_async_bulk_create(self): 43 | objs = await TestModel.objects.abulk_create( 44 | [ 45 | TestModel(name="bulk create 1"), 46 | TestModel(name="bulk create 2"), 47 | ] 48 | ) 49 | objs = await TestModel.objects.aall() 50 | objs = await objs.acount() 51 | self.assertEqual(objs, 4) 52 | 53 | @tag("dev") 54 | async def test_async_bulk_update(self): 55 | self.assertTrue(False, "Not Implemented") 56 | 57 | @tag("ci") 58 | async def test_async_get_or_create(self): 59 | async def test_async_get_or_create_on_obj_get(self): 60 | obj = await TestModel.objects.aget_or_create(name="setup 1") 61 | self.assertEqual(obj[1], False) 62 | 63 | async def test_async_get_or_create_on_obj_create(self): 64 | obj = await TestModel.objects.aget_or_create(name="setup 3") 65 | self.assertEqual(obj[0].name, "setup 3") 66 | self.assertEqual(obj[1], True) 67 | 68 | await test_async_get_or_create_on_obj_get(self) 69 | await test_async_get_or_create_on_obj_create(self) 70 | 71 | @tag("dev") 72 | async def test_async_update_or_create(self): 73 | self.assertTrue(False, "Not Implemented") 74 | 75 | @tag("ci") 76 | async def test_async_earliest(self): 77 | first = await (await TestModel.objects.aall()).afirst() 78 | earliest = await TestModel.objects.aearliest("id") 79 | self.assertTrue(earliest.id, first.id) 80 | 81 | @tag("ci") 82 | async def test_async_latest(self): 83 | created = await TestModel.objects.acreate(name="latest") 84 | latest = await TestModel.objects.alatest("id") 85 | self.assertEqual(latest.id, created.id) 86 | 87 | @tag("ci") 88 | async def test_async_first_in_all(self): 89 | all_result = await TestModel.objects.aall() 90 | first = await all_result.afirst() 91 | self.assertEqual(all_result[0].name, first.name) 92 | 93 | @tag("ci") 94 | async def test_async_last_in_all(self): 95 | all_result = await TestModel.objects.aall() 96 | last = await all_result.alast() 97 | self.assertEqual(all_result[1].name, last.name) 98 | 99 | @tag("dev") 100 | async def test_async_in_bulk(self): 101 | self.assertTrue(False, "Not Implemented") 102 | 103 | @tag("ci") 104 | async def test_async_delete(self): 105 | created = await TestModel.objects.acreate(name="to delete") 106 | all_created = await TestModel.objects.aall() 107 | count = await all_created.acount() 108 | self.assertEqual(count, 3) 109 | 110 | await all_created.adelete() 111 | all_after_delete = await TestModel.objects.aall() 112 | count = await all_after_delete.acount() 113 | self.assertEqual(count, 0) 114 | 115 | @tag("ci") 116 | async def test_async_update(self): 117 | created = await TestModel.objects.acreate(name="to update") 118 | qs = await TestModel.objects.afilter(name="to update") 119 | updated = await qs.aupdate(name="updated") 120 | self.assertEqual(updated, 1) 121 | 122 | @tag("ci") 123 | async def test_async_exists(self): 124 | qs = await TestModel.objects.afilter(name="setup 1") 125 | exists = await qs.aexists() 126 | self.assertTrue(exists) 127 | 128 | @tag("ci") 129 | async def test_async_explain(self): 130 | explained = await (await TestModel.objects.afilter(name="setup 1")).aexplain() 131 | print(explained) 132 | self.assertEqual(explained, "2 0 0 SCAN tests_testmodel") 133 | 134 | @tag("dev") 135 | async def test_async_raw(self): 136 | rs = await TestModel.objects.araw("SELECT * from tests_testmodel") 137 | print(list(rs)) 138 | 139 | @tag("ci") 140 | async def test_async_count(self): 141 | result = await TestModel.objects.aall() 142 | result = await result.acount() 143 | self.assertEqual(result, 2) 144 | 145 | @tag("ci") 146 | async def test_async_none(self): 147 | result = await TestModel.objects.anone() 148 | self.assertEqual(list(result), []) 149 | 150 | @tag("ci") 151 | async def test_async_aiter(self): 152 | all_qs = await TestModel.objects.aall() 153 | count = 0 154 | async for obj in all_qs: 155 | count += 1 156 | self.assertEqual(count, 2) 157 | 158 | @tag("dev") 159 | async def test_async_fetch_all(self): 160 | self.assertTrue(False, "Not Implemented") 161 | 162 | @tag("ci") 163 | async def test_async_all(self): 164 | result = await TestModel.objects.aall() 165 | result = await result.acount() 166 | self.assertEqual(result, 2) 167 | 168 | @tag("ci") 169 | async def test_async_filter(self): 170 | qs = await TestModel.objects.afilter(name="setup 2") 171 | element = await qs.afirst() 172 | self.assertEqual(element.name, "setup 2") 173 | 174 | @tag("ci") 175 | async def test_async_exclude(self): 176 | qs = await TestModel.objects.afilter(obj_type="setup") 177 | qs = await qs.aexclude(name="setup 1") 178 | el = await qs.afirst() 179 | self.assertEqual(el.name, "setup 2") 180 | 181 | @tag("dev") 182 | async def test_async_complex_filter(self): 183 | self.assertTrue(False, "Not Implemented") 184 | 185 | @tag("dev") 186 | async def test_async_union(self): 187 | self.assertTrue(False, "Not Implemented") 188 | 189 | @tag("dev") 190 | async def test_async_intersection(self): 191 | self.assertTrue(False, "Not Implemented") 192 | 193 | @tag("dev") 194 | async def test_async_difference(self): 195 | self.assertTrue(False, "Not Implemented") 196 | 197 | @tag("dev") 198 | async def test_async_select_for_update(self): 199 | self.assertTrue(False, "Not Implemented") 200 | 201 | @tag("dev") 202 | async def test_async_prefetch_related(self): 203 | self.assertTrue(False, "Not Implemented") 204 | 205 | @tag("dev") 206 | async def test_async_annotate(self): 207 | self.assertTrue(False, "Not Implemented") 208 | 209 | @tag("ci") 210 | async def test_async_order_by_ascending(self): 211 | qs = await TestModel.objects.aall() 212 | qs = await qs.aorder_by("name") 213 | qs = await qs.afirst() 214 | self.assertEqual(qs.name, "setup 1") 215 | 216 | @tag("ci") 217 | async def test_async_order_by_descending(self): 218 | qs = await TestModel.objects.aall() 219 | qs = await qs.aorder_by("-name") 220 | qs = await qs.afirst() 221 | self.assertEqual(qs.name, "setup 2") 222 | 223 | @tag("dev") 224 | async def test_async_distinct(self): 225 | self.assertTrue(False, "Not Implemented") 226 | 227 | @tag("dev") 228 | async def test_async_extra(self): 229 | self.assertTrue(False, "Not Implemented") 230 | 231 | @tag("dev") 232 | async def test_async_reverse(self): 233 | self.assertTrue(False, "Not Implemented") 234 | 235 | @tag("dev") 236 | async def test_async_defer(self): 237 | self.assertTrue(False, "Not Implemented") 238 | 239 | @tag("dev") 240 | async def test_async_only(self): 241 | self.assertTrue(False, "Not Implemented") 242 | 243 | @tag("dev") 244 | async def test_async_using(self): 245 | self.assertTrue(False, "Not Implemented") 246 | 247 | @tag("dev") 248 | async def test_async_resolve_expression(self): 249 | self.assertTrue(False, "Not Implemented") 250 | 251 | @tag("dev") 252 | async def test_async_async_ordered(self): 253 | self.assertTrue(False, "Not Implemented") 254 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | urlpatterns = [] 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = True 3 | envlist = 4 | {py3}-django-3 5 | {py3}-django-4 6 | 7 | [tox.package] 8 | basepython = python3 9 | [testenv] 10 | setenv = 11 | PYTHONPATH = {toxinidir}:{toxinidir}/django_async_orm 12 | commands = coverage run --source django_async_orm manage.py test -v2 {posargs} 13 | deps = 14 | django-3: Django>=3.2,<4.0 15 | django-4: Django>=4.0,<5.0 16 | -r{toxinidir}/tests/requirements.txt 17 | 18 | --------------------------------------------------------------------------------