Metadata-Version: 2.5
Name: crudkit
Version: 0.4.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>=1.39
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'
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]    # + per-object AI assistant (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"))]
```

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 and the create/update/action/note tools (default `False`) |
| `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 and permissions |
| `search` | free-text search across types, returning `{id, label}` |
| `list_records` | one `type`, 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 |

With `CRUDKIT_MCP_WRITE_ENABLED` and a token granted the `write` scope, four
more: `create_record`, `update_record`, `run_action` and `add_note`.

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 never exposed;
narrow the rest with `CRUDKIT_MCP_MODELS = ["CMP", "PER", ...]` or opt a
single model out with `CrudKitSettings.mcp_exclude = True`.

## 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
