Metadata-Version: 2.5
Name: crudkit
Version: 0.6.0
Summary: A metadata-driven CRUD framework for Django: typed object IDs, soft delete, merge, change log, activity feed, saved views, AI fields and a generic REST API.
Project-URL: Homepage, https://github.com/fenerum/crudkit
Project-URL: Changelog, https://github.com/fenerum/crudkit/blob/main/backend/CHANGELOG.md
Author: Fenerum ApS
License-Expression: MIT
Keywords: api,crud,django,framework,rest
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.1
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.12
Requires-Dist: celery>=5.3
Requires-Dist: django-phonenumber-field[phonenumberslite]>=8.0
Requires-Dist: django>=5.1
Requires-Dist: pydantic-ai-slim>=2.51
Requires-Dist: pydantic>=2.11
Requires-Dist: pyjwt>=2.9
Provides-Extra: api
Requires-Dist: djangorestframework-simplejwt>=5.4; extra == 'api'
Requires-Dist: djangorestframework>=3.15; extra == 'api'
Requires-Dist: drf-extra-fields>=3.7; extra == 'api'
Provides-Extra: assistant
Requires-Dist: channels>=4.1; extra == 'assistant'
Requires-Dist: djangorestframework-simplejwt>=5.4; extra == 'assistant'
Requires-Dist: djangorestframework>=3.15; extra == 'assistant'
Requires-Dist: drf-extra-fields>=3.7; extra == 'assistant'
Provides-Extra: mcp
Requires-Dist: djangorestframework-simplejwt>=5.4; extra == 'mcp'
Requires-Dist: djangorestframework>=3.15; extra == 'mcp'
Requires-Dist: drf-extra-fields>=3.7; extra == 'mcp'
Provides-Extra: realtime
Requires-Dist: channels>=4.1; extra == 'realtime'
Requires-Dist: djangorestframework-simplejwt>=5.4; extra == 'realtime'
Requires-Dist: djangorestframework>=3.15; extra == 'realtime'
Requires-Dist: drf-extra-fields>=3.7; extra == 'realtime'
Description-Content-Type: text/markdown

# crudkit

A metadata-driven CRUD framework for Django. Define models; get typed object
IDs, soft delete, merge, change logging, an activity feed, saved views,
AI-populated fields and a generic REST API — without writing per-model
serializers, viewsets or routes.

## Core concepts

Every CrudKit model inherits `BaseCrudKitModel` and declares a 3-letter
`TYPE_ID`. Object IDs are rendered as `<TYPE_ID><pk>` (e.g. `CUS42`) across
the API and UI.

```python
from django.db import models
from crudkit.models import BaseCrudKitModel

class Customer(BaseCrudKitModel):
    TYPE_ID = "CUS"
    name = models.CharField(max_length=255)

    class CrudKitSettings(BaseCrudKitModel.CrudKitSettings):
        search_fields = ["name"]
```

Included with every model: `created_by`/`updated_by`/`created_at`/`updated_at`
audit columns, a `deleted` soft-delete flag (`soft_delete()`), merge support
(`delete_and_merge_with()`), a change log, an activity feed (`FeedItem`),
external-system sync (`ExternalObject`), saved views/layouts, and optional AI
fields (`AISummaryField`, `AICategoryField`, `AIBooleanField`, `AITagsField`,
`AIForeignKeyField`) populated asynchronously via Celery and pydantic-ai.

## Installation

```
pip install crudkit[api]          # REST API included
pip install crudkit[assistant]    # + AI assistant sidebar (Channels)
pip install crudkit[mcp]          # + remote MCP server with OAuth (e.g. a Claude connector)
```

```python
INSTALLED_APPS = [
    ...,
    "rest_framework",
    "crudkit",
    "crudkit_assistant",  # optional
]

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        "crudkit_api.permissions.CrudKitModelPermissions",
    ],
    "DEFAULT_PAGINATION_CLASS": "crudkit_api.pagination.CrudKitPagination",
    "DEFAULT_FILTER_BACKENDS": ["crudkit_api.filters.BasicFilter"],
    "PAGE_SIZE": 50,
}

# urls.py — one include registers a full CRUD API for every TYPE_ID model
urlpatterns = [path("api/v1/", include("crudkit_api.urls"))]
```

The assistant sidebar talks over a WebSocket at `ws/assistant/`, next to the
realtime `ws/changes/` socket; route both in your ASGI application (see
`examples/demo/demo/asgi.py`):

```python
application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": URLRouter(
        crudkit_api.routing.websocket_urlpatterns + crudkit_assistant.routing.websocket_urlpatterns
    ),
})
```

The sidebar sees what the user has on screen — the open record, or the list
with its saved view, search, filters, visible and selected rows — and can
search and read across record types. It only *proposes* changes (to any record
the user may change); each one waits for Confirm. Conversations are stored
per user (`AssistantConversation`, ASC) and resume after a reload. The SPA
only shows the assistant when an AI model is configured (`CRUDKIT_AI_MODEL` or
`CRUDKIT_AI_MODEL_FACTORY`).

Company knowledge for the AI lives in **AI context** documents (`AIContext`,
AIC), which users edit in the UI at `/AIC` (add a saved view to the menu to link it).
Active documents without `model_types` go into the assistant's instructions,
joined by documents scoped to a type while a record or list of that type is
open; AI fields get both in a "Company context" section
of their prompt. Code can render them with `crudkit.llm.ai_context(type_id)`.
The assistant proposes edits to these documents when the user teaches it
something durable about the company.

CrudKit requires the standard Django model permissions for every API and
assistant operation. Projects that need row-level rules can override
`CrudKitSettings.get_authorized_queryset(user, queryset, action)`; `action`
is one of `view`, `add`, `change`, or `delete`. Model actions require `change`
permission and can be narrowed further with
`CrudKitSettings.has_action_permission(user, instance, action_name)`.

## Settings

| Setting | Purpose |
|---|---|
| `CRUDKIT_AI_MODEL` | pydantic-ai model string (e.g. `"mistral:mistral-large-latest"`) enabling AI fields/assistant |
| `CRUDKIT_AI_MODEL_FACTORY` | dotted path to an async context manager yielding a pydantic-ai `Model` (advanced) |
| `CRUDKIT_USER_PROFILE_ADAPTER` | dotted path to a class supplying preferred language + avatar images for users |
| `CRUDKIT_EXTRA_GENERIC_RELATIONS` | project models surfaced as generic relations in object metadata |
| `CRUDKIT_DEFAULT_CURRENCY`, `CRUDKIT_CURRENCY_CHOICES` | currency configuration for `MoneyField` |
| `CRUDKIT_GRAVATAR_FALLBACK_EMAIL` | fallback avatar email (gravatar `mp` default otherwise) |
| `CRUDKIT_DASHBOARD_WIDGETS` | dotted path to a `dashboard_for_user(user)` widget provider |
| `CRUDKIT_ASSISTANT_NAME`, `CRUDKIT_ASSISTANT_SYSTEM_PROMPT`, `CRUDKIT_ASSISTANT_AVATAR_URL` | assistant branding |
| `CRUDKIT_FRONTEND_CONFIG` | dict injected into the bundled SPA at runtime (`app_name`, `logo_url`, ...) |
| `CRUDKIT_FRONTEND_LOGIN_REQUIRED` | redirect anonymous users of the SPA view to `LOGIN_URL` |
| `CRUDKIT_MCP_SERVER_NAME` | `serverInfo.name` reported by the MCP server (default `"crudkit"`) |
| `CRUDKIT_MCP_WRITE_ENABLED` | offer the `write` OAuth scope: the create/update/action/note/undo tools write directly (default `False`) |
| `CRUDKIT_MCP_WRITE_RATE` | write tool calls allowed per token, as `"<count>/<second\|minute\|hour\|day>"` (default `"60/min"`; `None` for no limit). Counted in the default Django cache: use a shared one (Redis, Memcached, database) with several processes; with `CRUDKIT_MCP_WRITE_ENABLED`, system check `crudkit_mcp.W001` warns otherwise |
| `CRUDKIT_AI_CONTEXT_MAX_CHARS` | cap on the AI context text sent with every assistant step and AI-field run (default 20000) |
| `CRUDKIT_MCP_MODELS` | list of TYPE_IDs to expose over MCP (default: every project model) |
| `CRUDKIT_MCP_EXTRA_TOOLS` | dotted paths to `crudkit_mcp.tools.Tool` instances; added to, or replacing, the generated tools |
| `CRUDKIT_MCP_BASE_URL` | public origin for OAuth metadata URLs when the request's host/scheme is wrong (e.g. behind a proxy) |

## MCP server

`crudkit_mcp` exposes the models as a remote [MCP](https://modelcontextprotocol.io)
server (streamable HTTP) with its own OAuth 2.1 authorization server
(dynamic client registration, PKCE, rotating refresh tokens), so it can be
added as e.g. a Claude connector by URL.

```python
INSTALLED_APPS = [..., "crudkit_mcp"]
LOGIN_URL = "/login/"  # the OAuth consent page needs a session login

urlpatterns = [
    path("api/v1/", include("crudkit_mcp.urls")),       # /api/v1/mcp, /api/v1/oauth/...
    path("", include("crudkit_mcp.well_known_urls")),   # /.well-known/oauth-* (site root)
    ...,
]
```

The tool set is fixed — records are addressed by TYPE_ID and CK-ID, so the
same tools serve any project:

| Tool | |
|---|---|
| `describe_types` | the record types; for one `type`, its filters, writable fields, actions, permissions and saved views |
| `search` | free-text search across types, returning `{id, label}` |
| `list_records` | one `type` or saved `view` (its filters, ordering and columns), a `filters` object (keys from `describe_types`), `query`, `order_by`, `limit`, `offset` |
| `get_record` | one record by ID, with its feed, change log and available actions |

Five more tools change data: `create_record`, `update_record`, `run_action`,
`add_note` and `undo`. On the consent page the user picks what the client may
do:

- **Read only** — the `read` scope; no write tools.
- **Propose changes** — the `propose` scope, offered when `crudkit_assistant`
  is installed. The write tools validate their input as usual but change
  nothing: each files a proposal and returns
  `{"status": "pending_approval", "proposal": "ASP12"}`. The user confirms or
  skips it in the Inbox's Proposals tab.
- **Write directly** — the `write` scope, only with
  `CRUDKIT_MCP_WRITE_ENABLED`. Each write returns the `change_set` it made;
  `undo` reverts one. Actions and fields that require approval (see
  `docs/concepts.md`) are still only proposed.

Writes and proposals are recorded as made over MCP by the token's OAuth
client, and limited by `CRUDKIT_MCP_WRITE_RATE`.

Every call is filtered through the token user's model, row and action
permissions, so a type the user can't view isn't listed and can't be read.
CrudKit's own models and Django's (including `User`) are not exposed, except
background agents (`AGT`), which clients can propose; narrow the rest with `CRUDKIT_MCP_MODELS = ["CMP", "PER", ...]` or opt a
single model out with `CrudKitSettings.mcp_exclude = True`.

## Background agents

With `crudkit_assistant` installed, users can save **agents** (TYPE_ID `AGT`):
instructions the assistant carries out on its own, on a record that was just
created or changed, on the records of a saved view on a schedule, or when
someone clicks Run now. Each go is an `AgentRun` (`AGR`) with the agent's
summary and the proposals it made. See `docs/concepts.md` for the details.

Runs are Celery tasks (`crudkit_assistant.tasks.run_agent`). For scheduled
agents, add the scheduler task to Celery beat:

```python
CELERY_BEAT_SCHEDULE = {
    "crudkit-scheduled-agents": {
        "task": "crudkit_assistant.tasks.run_scheduled_agents",
        "schedule": 300,  # every 5 minutes; each agent runs when its interval has passed
    },
}
```

Agents start from logged changes (the REST API, MCP, the assistant, actions),
so plain ORM saves in project code do not trigger them. The enabled agents per
type are cached in the Django cache for up to a minute.

## Bundled frontend

The wheel ships the built CrudKit web SPA. To serve it, add the app, the
context processor, a config dict, and a catch-all url include (last!):

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

TEMPLATES = [{
    ...,
    "OPTIONS": {"context_processors": [
        ...,
        "crudkit_frontend.context_processors.crudkit_config",
    ]},
}]

CRUDKIT_FRONTEND_CONFIG = {"app_name": "My App"}  # injected into the SPA at runtime
CRUDKIT_FRONTEND_LOGIN_REQUIRED = False  # True → redirect anonymous users to LOGIN_URL

# urls.py — must be the LAST pattern; everything unmatched serves the SPA
urlpatterns = [..., path("", include("crudkit_frontend.urls"))]
```

Static assets are served by `django.contrib.staticfiles` (or WhiteNoise et
al.) from `crudkit_frontend/static/`, under whatever `STATIC_URL` the project
uses — nothing assumes the default `static/` prefix. Contributors hacking on
the SPA itself
run the Vite dev server from [../frontend](../frontend/) against any CrudKit
backend; `npm run build` there regenerates the bundled assets.

## Running the tests

```
cd backend
uv sync --all-extras
uv run manage.py test crudkit crudkit_api crudkit_assistant crudkit_frontend crudkit_mcp tests
```

## License

MIT
