Metadata-Version: 2.3
Name: django-async-backend
Version: 6.0.7
Summary: Django extension providing async capabilities for database and other components
License: MIT
Keywords: django,asyncio,database
Author: Arfey
Author-email: arfey@gmail.com
Requires-Python: >=3.12
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Utilities
Requires-Dist: django (>=6)
Requires-Dist: psycopg[binary] (>=3,<4)
Project-URL: Documentation, https://github.com/Arfey/django-async-backend/blob/main/README.md
Project-URL: Homepage, https://github.com/Arfey/django-async-backend
Project-URL: Repository, https://github.com/Arfey/django-async-backend
Description-Content-Type: text/markdown

# Django Async Backend

## 🚀 Installation & Django Integration

### 1. Install the package

```bash
pip install django-async-backend
```

The current packages depend heavily on the Django version, because a large part of them is autogenerated. The goal is to stay in sync with Django's major and minor versions, for example 6.0.x, tracking whatever Django version is in use.

### 2. Django settings

In your settings.py, set the database engine to use the async backend

```python
DATABASES = {
    "default": {
        "ENGINE": "django_async_backend.db.backends.postgresql",
        ...
    },
}
```

Make sure your Django app (and any other required apps) are listed in INSTALLED_APPS

```python
INSTALLED_APPS = [
    ...
    "django_async_backend",
    ...
]
```

---

## Connection Pooling

> ⚠️ Connection pooling is not supported when running under a WSGI server (including the Django development server), because WSGI creates a new event loop for each request. This prevents reliable management of connection pool state. To disable the warning, set `ASYNC_BACKEND_DISABLE_POOL_WARNING=True`

---

## Middleware

When running under ASGI, add `close_async_connections` to `MIDDLEWARE` so connections are returned to the pool at the end of each request. Django's `request_finished` signal only closes sync connections.

```python
MIDDLEWARE = [
    "django_async_backend.middleware.close_async_connections",
    ...
]
```

---

## Concurrency model

> ⚠️ **Async does not mean parallel here.** Database queries are *not* run in
> parallel by default.

Within a single async context (such as one request or task), all async ORM and
cursor calls share **one connection per database alias**. Because they share a
connection, queries are serialized — running them under `asyncio.gather()` does
**not** execute them on the database concurrently; they take turns on the shared
connection.

This is a deliberate design choice that mirrors Django's DEP 0009. To run
queries truly in parallel, you must opt in explicitly and give each one its own
connection with `async_connections._independent_connection()`. See the
[DEP 0009](#dep-0009) section below for details and an example.

---

## Connection Handler

The connection handler manages database connections for your async backend.

```python
from django_async_backend.db import async_connections

connection = async_connections['default']

async with await connection.cursor() as cursor:
    await cursor.execute("SELECT ...")
    rows = await cursor.fetchall()

await connection.close()
```

- Connections are reused and managed automatically.
- Use await connection.close() to manually close a connection if needed.

## Cursor

Async cursors provide the following methods:

- `execute`
- `executemany`
- `fetchone`
- `fetchmany`
- `fetchall`

```python
async with await connection.cursor() as cursor:
    await cursor.execute("SELECT 1")
    row = await cursor.fetchone()
```

## Async Transactions with `async_atomic`

### Basic Usage

Use `async_atomic` to run async database operations atomically.
All changes inside the block are committed together; if an error occurs, all changes are rolled back.

```python
from django_async_backend.db.transaction import async_atomic

async with async_atomic():
    await create_instance(1)
    # If no error, changes are committed
    # If error, changes are rolled back
```

### Rollback on Error

If an exception is raised inside the block, all changes are rolled back:

```python
async with async_atomic():
    await create_instance(1)
    raise Exception("fail")  # Nothing is committed
```

### Nested Transactions (Savepoints)

You can nest async_atomic blocks.
Each inner block creates a savepoint.
If an error occurs in the inner block, only its changes are rolled back; outer changes remain.

```python
async with async_atomic():
    await create_instance(1)
    try:
        async with async_atomic():
            await create_instance(2)
            raise Exception("fail inner")  # Only instance 2 is rolled back
    except Exception:
        pass
# Only instance 1 is in the database
```

### Using `on_commit` with async transactions

You can register a callback to run after a successful transaction commit using `connection.on_commit`.

```python
connection = async_connections[DEFAULT_DB_ALIAS]

async with async_atomic():
    await connection.on_commit(callback)
```

## Writing Async Tests

### AsyncioTestCase

Use for async tests that do **not** require database transactions.

```python
from django_async_backend.test import AsyncioTestCase


class MyAsyncTests(AsyncioTestCase):
    async def asyncSetUp(self):
        # Setup code

    async def asyncTearDown(self):
        # Cleanup code

    async def test_something(self):
        # Your async test logic
        await do_async_stuff()
```

### AsyncioTransactionTestCase

Use for async tests that need database transaction support (rollbacks, atomic blocks).

```python
from django_async_backend.test import AsyncioTransactionTestCase


class MyTransactionTests(AsyncioTransactionTestCase):
    async def asyncSetUp(self):
        # Setup database

    async def asyncTearDown(self):
        # Cleanup database

    async def test_something(self):
        async with async_atomic():
            # DB operations here
            await do_db_stuff()
```


# ORM support:
### Manager:


```python
from django.db import models, DEFAULT_DB_ALIAS
from django_async_backend.db import async_connections
from django_async_backend.db.models.manager import AsyncManager

class Book(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=100)

    async_object = AsyncManager()

    class Meta:
        db_table = "books"


async def main():
    async for i in Book.async_object.all():
        print(i.id)

    await async_connections[DEFAULT_DB_ALIAS].close()
```



| methods                             | supported | comments |
| ----------------------------------- | --------- | -------- |
| `Model.objects.aget`                | ✅        |          |
| `Model.objects.acreate`             | ❌        |          |
| `Model.objects.acount`              | ✅        |          |
| `Model.objects.none`                | ✅        |          |
| `Model.objects.abulk_create`        | ❌        |          |
| `Model.objects.abulk_update`        | ❌        |          |
| `Model.objects.aget_or_create`      | ❌        |          |
| `Model.objects.aupdate_or_create`   | ❌        |          |
| `Model.objects.aearliest`           | ✅        |          |
| `Model.objects.alatest`             | ✅        |          |
| `Model.objects.afirst`              | ✅        |          |
| `Model.objects.alast`               | ✅        |          |
| `Model.objects.ain_bulk`            | ✅        |          |
| `Model.objects.adelete`             | ❌        |          |
| `Model.objects.aupdate`             | ✅        |          |
| `Model.objects.aexists`             | ✅        |          |
| `Model.objects.acontains`           | ❌        |          |
| `Model.objects.aexplain`            | ✅        |          |
| `Model.objects.araw`                | ❌        |          |
| `Model.objects.all`                 | ✅        |          |
| `Model.objects.filter`              | ✅        |          |
| `Model.objects.exclude`             | ✅        |          |
| `Model.objects.complex_filter`      | ✅        |          |
| `Model.objects.union`               | ✅        |          |
| `Model.objects.intersection`        | ✅        |          |
| `Model.objects.difference`          | ✅        |          |
| `Model.objects.select_related`      | ❌        |          |
| `Model.objects.select_for_update`   | ❌        |          |
| `Model.objects.prefetch_related`    | ❌        |          |
| `Model.objects.annotate`            | ✅        |          |
| `Model.objects.order_by`            | ✅        |          |
| `Model.objects.distinct`            | ✅        |          |
| `Model.objects.extra`               | ✅        |          |
| `Model.objects.reverse`             | ✅        |          |
| `Model.objects.defer`               | ❌        |          |
| `Model.objects.only`                | ❌        |          |
| `Model.objects.using`               | ✅        |          |
| `Model.objects.resolve_expression`  | ❌        |          |
| `Model.objects.ordered`             | ❌        |          |
| `Model.objects.values`              | ✅        |          |
| `Model.objects.values_list`         | ✅        |          |
| `Model.objects.dates`               | ❌        |          |
| `Model.objects.datetimes`           | ❌        |          |
| `Model.objects.alias    `           | ❌        |          |
| `__aiter__`                         | ✅        |          |
| `__repr__`                          | ❌        |          |
| `__len__`                           | ❌        |          |
| `__and__`                           | ❌        |          |
| `__or__`                            | ❌        |          |
| `__xor__`                           | ❌        |          |
| `__getitem__`                       | ✅        |          |
| `Model.objects.aiterator`           | ❌        |          |

### RawQuerySet

Not supported ❌

### Model:

| methods         | supported | comments |
| --------------- | --------- | -------- |
| `Model.asave`   | ❌        |          |
| `Model.aupdate` | ❌        |          |
| `Model.adelete` | ❌        |          |
| `...`           | ❌        |          |

### User Model / Manager

| methods                     | supported | comments |
| --------------------------- | --------- | -------- |
| `User.is_authenticated`     | ❌        |          |
| `User.is_super_user`        | ❌        |          |
| `User.objects.acreate_user` | ❌        |          |
| `...`                       | ❌        |          |


## Code generation

Part of the ORM layer is **generated, not hand-written**. The async versions of
Django's query classes are produced from Django's own source by the `codemon`
tool (under `codemon/`), which rewrites the sync code into async using libcst.

These files are committed to git (so the package installs without running
codegen) and track a specific Django version, so they look hand-written but are
not. Each starts with a `# This file was generated automatically. Do not modify
it manually.` header.

To change them, edit the codemon config under `codemon/config/*.yaml` — **not**
the generated files — then regenerate:

```bash
lets test_generate
```

This restores the Django-derived files to pristine, runs `python -m codemon`,
and reformats the result — running `codemon` on its own skips the
restore/reformat and produces large formatting-only diffs. Regeneration
downloads Django's source for the pinned version over the network, so it needs
internet access. Any manual edits to the generated modules will be lost on the
next regeneration.

## ⚙️ Development Setup

**Install pre-commit hooks:**
```bash
pip install pre-commit
pre-commit install
```

**Install dependencies:**
```bash
poetry install --with dev
```

## 🧪 Running Tests

This project uses a comprehensive test suite powered by `unittest`.

**To run tests:**
```bash
docker-compose up postgres -d
DJANGO_SETTINGS_MODULE=settings poetry run python -m unittest discover -s tests
```

**Integration tests run locally.**

The `django_async_backend.db.backends.postgresql` backend is fully compatible with Django's default `django.db.backends.postgresql` backend, as it leverages the default implementation under the hood. To confirm this compatibility, run Django's test suite using the custom backend.

```python
DATABASES = {
    "default": {
        "ENGINE": "django_async_backend.db.backends.postgresql",
        ...
    },
    "other": {
        "ENGINE": "django_async_backend.db.backends.postgresql",
        ...
    },
}
```

To execute them:

```bash
cd tests_django
docker-compose run --build --rm test_django_integration
```

## DEP 0009

https://github.com/django/deps/blob/main/accepted/0009-async.rst

> Whenever a new_connections() block is entered, Django sets a new context with new database connections.

To show show an example how it might looks like with a current implementation we have the `independent_connection` context manager.

```python
import asyncio
from django_async_backend.db import async_connections

async def run_query():
    async with async_connections._independent_connection():
        conn = await async_connections['default']
        async with conn.cursor() as cursor:
            await cursor.execute("SELECT ...")
            return await cursor.fetchall()

results = await asyncio.gather(run_query(), run_query(), run_query())
```

It's just a concept that is not ready for production usage.

