Metadata-Version: 2.5
Name: weft-django
Version: 0.9.38
Summary: Django integration layer for Weft
Author-email: Van Lindberg <van@modelmonster.ai>
License: MIT
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: django<6,>=4.2
Requires-Dist: weft>=0.9.103
Provides-Extra: channels
Requires-Dist: channels<5,>=4.1; extra == 'channels'
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Provides-Extra: realtime
Requires-Dist: channels<5,>=4.1; extra == 'realtime'
Description-Content-Type: text/markdown

# weft-django

`weft-django` is the first-party Django integration for Weft.

The package is typed (`py.typed`) and depends on Weft through the public
`weft.client` API.

It provides:

- `@weft_task` for Django-owned synchronous background functions
- transaction-safe submission helpers such as `enqueue_on_commit()`
- native TaskSpec, stored spec, and pipeline submission helpers
- read-only Django URLs for task inspection
- SSE by default, with optional Channels/WebSocket transport
- Django management commands for task status and control

Install:

```bash
uv add weft-django
```

Or from the main package convenience extra:

```bash
uv add "weft[django]"
```

Install the optional Channels transport with:

```bash
uv add "weft-django[channels]"
```

Equivalent install surfaces:

```bash
uv add "weft-django[realtime]"
uv add "weft[django-channels]"
```

Basic usage:

```python
from weft_django import weft_task


@weft_task(name="billing.send_invoice", timeout=60)
def send_invoice(invoice_id: int) -> dict[str, int]:
    return {"invoice_id": invoice_id}


submission = send_invoice.enqueue(123)
result = submission.result(timeout=30)
assert result.status == "completed"
```

## Project Context

Django requests its runtime context from Weft. With no explicit context setting,
Weft discovers the nearest project starting at Django's `BASE_DIR`, using that
directory itself when discovery finds nothing:

```python
# settings.py
INSTALLED_APPS += ["weft_django"]
WEFT_DJANGO = {}
```

To pin a project explicitly, set `WEFT_DJANGO = {"CONTEXT": BASE_DIR}`. The explicit
Django setting wins over `WEFT_CONTEXT`. Otherwise `WEFT_CONTEXT` selects the root
before discovery from `BASE_DIR`. Without `BASE_DIR`, Weft uses its ordinary CWD
discovery. Explicit roots are used directly; they are not discovery anchors.

Broker settings such as `WEFT_BACKEND_TARGET` select the broker while preserving
this project-root policy. For example, a PostgreSQL target does not move Weft's
artifact directory to the web worker's CWD. Project broker configuration retains
its existing precedence. `BROKER_*` variables do not configure Weft.

Settings import and task discovery do not initialize Weft. Runtime operations
acquire a client with a resolved context and Config snapshot; a retained client
keeps that snapshot, while a later acquisition can observe new settings.

After upgrading, stray `BROKER_*` or default-valued broker settings no longer
redirect Django's artifacts to CWD. If an installation used that former
destination, pin `CONTEXT` to its existing root before upgrading. The integration
does not move existing tasks or artifacts. Restart long-lived Django processes
to pick up the new code and settings.

## Submission Handle

`enqueue(...)` and the native submission helpers return `WeftSubmission`.

The handle exposes:

- `tid`
- `name`
- `status()`
- `wait(timeout=None)`
- `result(timeout=None)`
- `stop()`
- `kill()`
- `events(follow=False)`

`status()` returns the current public status string or `None` if no snapshot is
available yet. `wait()` and `result()` both return the structured Weft
`TaskResult`.

Module-level `status(tid)` and `terminal_snapshot(tid)` use Weft's compact
known-TID terminal snapshot path. They are read-only and can report terminal
Monitor-store fallback after raw task-log rows retire. Use `snapshot(tid)` when
callers need diagnostic fields such as task metadata, runtime details, or
timestamps.

`enqueue_on_commit(...)` and the native `*_on_commit(...)` helpers return
`WeftDeferredSubmission`. The deferred handle has a stable `name` immediately
and gains `tid` plus task methods after the outer transaction commits. Calling
result-like methods before commit raises a local `RuntimeError`.

A successful Weft broker write binds the deferred TID even if manager readiness
then degrades. That readiness warning does not raise from the commit callback or
stop later callbacks. Broker-write failure and authoritative manager rejection
still raise. The hook is not a durable outbox or an atomic cross-database write.

Deferred helpers validate and snapshot before registering Django's
`transaction.on_commit()` callback. Missing spec references, invalid overrides,
and unserializable payloads fail before the app transaction commits. Mutating
args, kwargs, or payload objects after helper call time does not change the work
submitted at commit.

The helpers also capture the core client before registering the callback.
Changing Django settings, environment, CWD, or HOME before commit does not
redirect prepared work. Core preparation binds an explicit relative or
home-relative TaskSpec context to its absolute path. An explicitly different
TaskSpec root still has its broker selected at submission using captured Config;
broker project files are not snapshotted.

## Composition Export

`task.as_taskspec_for_call(*args, _overrides=None, **kwargs)` returns the
validated, normalized TaskSpec definition for that call, with the
call envelope embedded in `spec.args`, for manual composition into ordinary Weft
task or pipeline specs. It does not submit anything, builds no Weft context,
reads no Weft configuration, opens no broker, and writes nothing (the configured
`REQUEST_ID_PROVIDER` still runs, as it does for every call).
`_overrides` accepts exactly Weft's public submit overrides (`name`,
`description`, `tags`, `env`, `working_dir`, `stream_output`, `timeout`,
`memory_mb`, `cpu_percent`, `runner`, `runner_options`, `metadata`) with core
semantics: `None` values are ignored, unknown names (including `wait`) raise
`TypeError`, and invalid values raise the TaskSpec validation error. The export
is `weft.client.normalize_taskspec_payload(...)` applied to the generated
template; the package applies no overrides of its own.

An explicit Django `CONTEXT` is copied into `spec.weft_context` as declared,
including relative or home-relative text. With no explicit setting, the field
remains unset: the export does not capture `BASE_DIR`, environment, or a
discovered project. Such exports inherit their destination from the receiving
Weft context when submitted or composed. This makes exports portable; set
`CONTEXT` explicitly when the declaration must name a particular project.

## Native Helpers

Use these helpers when Django code wants to launch native Weft work instead of a
decorated Django function:

```python
from pathlib import Path

from weft_django import (
    submit_pipeline_reference,
    submit_spec_reference,
    submit_taskspec,
)


task = submit_taskspec(taskspec, payload={"job": 1})
task = submit_spec_reference(Path(".weft/tasks/report.json"), payload={"job": 1})
task = submit_pipeline_reference("nightly-report", payload={"job": 1})
```

Keyword rules:

- native helpers use `payload=...`
- `work_payload=...` and `input=...` are intentionally not supported
- deferred helpers reject `wait=True`

## Testing

`weft-django` does not ship an eager or inline execution mode.

Use the direct Python callable for narrow unit tests:

```python
assert send_invoice(123)["invoice_id"] == 123
```

Use broker-backed tests for enqueue behavior, transaction hooks, process
boundaries, streaming, native TaskSpecs, bundles, agents, and pipelines.

## Realtime And URLs

Include the read-only URLs explicitly:

```python
from django.urls import include, path

urlpatterns = [
    path("weft/", include("weft_django.urls")),
]
```

You must configure an authz callable:

```python
WEFT_DJANGO = {
    "AUTHZ": "myapp.weft_authz:authorize",
}
```

Supported realtime settings:

```python
WEFT_DJANGO = {
    "REALTIME": {
        "TRANSPORT": "sse",  # "none" | "sse" | "channels"
    },
}
```

Notes:

- `GET /weft/tasks/<tid>/` returns the current task snapshot
- `GET /weft/tasks/<tid>/events/` is the SSE endpoint when `TRANSPORT="sse"`
- `TRANSPORT="none"` disables the SSE endpoint
- `TRANSPORT="channels"` switches browser realtime delivery to the optional
  WebSocket consumer in `weft_django.channels`
- the Channels consumer starts a cancellable background stream after socket
  accept rather than blocking the connect lifecycle
- the HTTP and realtime surfaces are diagnostics only; they do not create a
  second task-truth store

## Celery Migration Guide

| Celery habit | `weft-django` |
| --- | --- |
| `@shared_task` | `@weft_task` |
| `task.delay(...)` | `task.enqueue(...)` |
| `transaction.on_commit(lambda: task.delay(...))` | `task.enqueue_on_commit(...)` |
| `AsyncResult` | `WeftSubmission` |

`delay` and `shared_task` are intentionally not shipped. They are close enough
to invite mechanical porting and far enough from Weft semantics to create
delayed failures.
