django-admin-grpc
Django Admin backed by gRPC services — no ORM required.
django-admin-grpc lets you expose remote gRPC microservices inside Django Admin with full list, create, update, delete and search support. You define a resource schema, wire a gRPC adapter, and register a single admin class — the package handles forms, pagination, filtering, and error mapping for you.
Installation
pip install django-admin-grpc
Add the app to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"django_admin_grpc",
]
Quick Start
# resources.py
from django_admin_grpc.resources import (
BaseGrpcResource,
BooleanFieldConfig,
CharFieldConfig,
FloatFieldConfig,
)
class Product(BaseGrpcResource):
class Meta:
app_label = "catalog"
model_name = "product"
verbose_name = "Product"
pk_field = "id"
fields = [
CharFieldConfig(name="id"),
CharFieldConfig(name="name", max_length=200),
FloatFieldConfig(name="price"),
BooleanFieldConfig(name="active", initial=True),
]
That is all. Django Admin now shows list, add, change, and delete screens powered by your gRPC service.
Package Architecture
| Component | Purpose |
|---|---|
BaseGrpcResource |
Declarative schema for a remote entity. Defines fields, primary key, and metadata. |
BaseFieldConfig |
Base class for field metadata. Use concrete subclasses like CharFieldConfig, IntegerFieldConfig, etc. |
BaseGrpcServiceAdapter |
Abstract transport layer. Implement list, get, and optionally create, update, delete. |
GrpcResourceAdmin |
ModelAdmin subclass that renders lists and forms using the adapter instead of the ORM. |
BaseGrpcMapper |
Optional request/response mapper between Django forms and protobuf messages. |
PagedResult |
Dataclass returned by adapter.list() carrying items, total count, and optional cursor. |
AdapterRegistry |
Central registry mapping service names to adapter instances. |
How to Define a New Resource
A resource is a Python class that tells Django Admin what columns exist, what types they are, and which one is the primary key.
from django_admin_grpc.resources import (
BaseGrpcResource,
BooleanFieldConfig,
CharFieldConfig,
FKFieldConfig,
IntegerFieldConfig,
)
class NetworkRule(BaseGrpcResource):
class Meta:
app_label = "network"
model_name = "networkrule"
verbose_name = "Network Rule"
verbose_name_plural = "Network Rules"
pk_field = "rule_id"
fields = [
CharFieldConfig(name="rule_id", label="Rule ID"),
CharFieldConfig(name="name", max_length=120),
IntegerFieldConfig(name="priority"),
BooleanFieldConfig(name="active", initial=True),
FKFieldConfig(
name="partner_id",
label="Partner",
model="contacts.Contact", # resolves via Django ORM
display_field="name",
required=False,
),
]
Supported field types:
| Type | Form widget | Notes |
|---|---|---|
char |
TextInput |
Use max_length to limit input. |
text |
Textarea |
Multi-line text. |
integer |
NumberInput |
Whole numbers. |
float |
NumberInput |
Decimal numbers. |
boolean |
CheckboxInput |
Defaults to False unless initial=True. |
choices |
Select |
Provide choices=[("a", "A"), ...]. |
fk |
Select |
Model FKs load options automatically; service/custom FKs use choices or choices_loader. |
date / datetime |
DateInput / DateTimeInput |
Stored as string; validate in the adapter. |
Common field options (all subclasses):
name— field identifier (required).label— human label (default: auto-title-casedname).required— whether the form field is required (default:True).help_text— shown below the form field.initial— default value.source— name of the attribute in the gRPC response if it differs fromname.
Type-specific options:
CharFieldConfig:max_length— max characters allowed.ChoicesFieldConfig:choices— list of(value, label)tuples.FKFieldConfig:model—"app_label.ModelName"for Django FK resolution.to_field— model field to use as the FK value.display_field— field to show in detail views; omitted means show the raw FK value.service— service name in the adapter registry for gRPC FK resolution.get_method— adapter method to call for gRPC FK resolution (default:get).choices— static(value, label)select options for service/custom FK fields.choices_loader— callable returning(value, label)options for service/custom FK fields.
How to Wire gRPC Stubs
An adapter bridges Django Admin and your gRPC service. Subclass BaseGrpcServiceAdapter and implement at least list and get. Implement create, update, and delete only if you need write access.
from django_admin_grpc.adapters import BaseGrpcServiceAdapter
from django_admin_grpc.paginator import PagedResult
import grpc
class NetworkRulesAdapter(BaseGrpcServiceAdapter):
service_name = "network_rules"
def __init__(self):
self._channel = None
@property
def channel(self):
if self._channel is None:
raw = grpc.insecure_channel("network-service:50051")
self._channel = self._wrap_channel(raw)
return self._channel
def list(self, resource_class, page=1, page_size=25, filters=None):
stub = NetworkRulesStub(self.channel)
request = ListRulesRequest(page=page, page_size=page_size)
response = stub.ListRules(request)
items = [resource_class.from_response(r) for r in response.rules]
return PagedResult(items=items, total=response.total)
def get(self, resource_class, pk):
stub = NetworkRulesStub(self.channel)
response = stub.GetRule(GetRuleRequest(rule_id=pk))
return resource_class.from_response(response)
def create(self, resource_class, data):
stub = NetworkRulesStub(self.channel)
request = CreateRuleRequest(**data)
response = stub.CreateRule(request)
return resource_class.from_response(response)
def update(self, resource_class, pk, data):
stub = NetworkRulesStub(self.channel)
request = UpdateRuleRequest(rule_id=pk, **data)
response = stub.UpdateRule(request)
return resource_class.from_response(response)
def delete(self, resource_class, pk):
stub = NetworkRulesStub(self.channel)
stub.DeleteRule(DeleteRuleRequest(rule_id=pk))
return True
Registering via the adapter registry (optional):
from django_admin_grpc.registry import adapter_registry
adapter = NetworkRulesAdapter()
adapter_registry.register("network_rules", adapter)
When registered by name, the admin class can reference it with service_name = "network_rules" instead of adapter_class.
How to Register in Django Admin
Register the resource's fake model with a GrpcResourceAdmin subclass:
from django.contrib import admin
from django_admin_grpc.admin import GrpcResourceAdmin
from .resources import NetworkRule
from .adapters import NetworkRulesAdapter
@admin.register(NetworkRule.admin_model())
class NetworkRuleAdmin(GrpcResourceAdmin):
resource_class = NetworkRule
adapter_class = NetworkRulesAdapter # or service_name = "network_rules"
list_display = ["rule_id", "name", "active"]
list_filter = ["active"]
search_fields = ["name"]
grpc_enable_create = True
grpc_enable_update = True
grpc_enable_delete = True
grpc_form_fields = ["name", "priority", "active", "partner_id"]
Resource.admin_model() builds a lightweight compatible class with _meta, objects, DoesNotExist, and MultipleObjectsReturned so that Django's ModelAdmin machinery works without a real ORM model.
How to Customize Forms
By default, forms are built automatically from grpc_form_fields and the resource's field config list. You can override widgets or build the form class yourself.
Custom widgets per field:
class ProductAdmin(GrpcResourceAdmin):
resource_class = Product
adapter_class = CatalogAdapter
grpc_form_fields = ["name", "description", "price"]
def _build_form_class(self):
return self.resource_class.build_form_class(widgets={
"description": forms.Textarea(attrs={"rows": 8}),
"price": forms.NumberInput(attrs={"step": "0.01"}),
})
Using build_form_class() on the resource:
from django import forms
form_class = Product.build_form_class(widgets={
"name": forms.TextInput(attrs={"class": "vTextField"}),
})
Filtering which fields appear:
Set grpc_form_fields to a subset of the resource's fields. Only those fields will be rendered in add/change views.
Customizing create/update payloads:
Override get_grpc_create_data and get_grpc_update_data on the admin class to transform cleaned_data before it reaches the adapter:
class ProductAdmin(GrpcResourceAdmin):
def get_grpc_create_data(self, cleaned_data):
data = dict(cleaned_data)
data["created_by"] = self.request.user.username
return data
How to Customize Permissions
Permissions are controlled by three flags on the admin class:
class ProductAdmin(GrpcResourceAdmin):
grpc_enable_create = True # show "Add" button
grpc_enable_update = True # allow editing in change view
grpc_enable_delete = True # show delete button and bulk delete action
These flags are ANDed with adapter capability: if the adapter does not implement create(), the add view is disabled automatically even when grpc_enable_create = True.
Hook methods for dynamic permission checks:
class ProductAdmin(GrpcResourceAdmin):
def has_add_permission(self, request):
return request.user.is_superuser and super().has_add_permission(request)
def has_delete_permission(self, request, obj=None):
return request.user.groups.filter(name="Admins").exists()
How to Customize List Pages
GrpcResourceAdmin supports the standard ModelAdmin list options:
class ProductAdmin(GrpcResourceAdmin):
list_display = ["id", "name", "price", "active"]
list_filter = ["active", "category_id"]
search_fields = ["name", "description"]
list_per_page = 25
Filter configuration:
For list filters to work, declare grpc_filter_config as a list or dict:
class ProductAdmin(GrpcResourceAdmin):
# Simple list — fields are auto-detected by type
grpc_filter_config = ["active", "category_id"]
# Dict form — override type or choices per field
grpc_filter_config = {
"active": {"type": "boolean"},
"status": {"type": "choices", "choices": [("draft", "Draft"), ("live", "Live")]},
"name": {"type": "text", "label": "Product Name"},
}
Supported filter types: boolean, choices, text. The admin renders a sidebar filter panel just like standard Django Admin.
Pagination:
By default the adapter receives page (1-indexed) and page_size. Return a PagedResult from adapter.list():
PagedResult(items=instances, total=total_count, page=page, page_size=page_size)
For cursor-based pagination, set grpc_cursor_pagination = True on the admin. The adapter will receive page_size and filters["cursor"]. Return the next cursor in PagedResult.next_cursor.
How to Customize Actions
Bulk delete is built in. When grpc_enable_delete = True and the adapter supports delete, a "Delete selected records" action appears in the dropdown.
Adding custom actions:
from django.contrib import messages
class ProductAdmin(GrpcResourceAdmin):
actions = ["activate_selected"]
@admin.action(description="Activate selected products")
def activate_selected(self, request, queryset):
adapter = self.get_adapter()
for obj in queryset:
adapter.update(self.resource_class, obj.pk, {"active": True})
messages.success(request, "Selected products activated.")
Because queryset is a GrpcFakeQuerySet, iterate over it to access the wrapped resource instances.
Error Handling
gRPC errors are caught at the adapter boundary and mapped to typed exceptions. The admin displays them as Django messages.
| gRPC Status Code | Mapped Exception | Admin Behavior |
|---|---|---|
NOT_FOUND |
GrpcNotFoundError |
Redirects to "object does not exist" page. |
PERMISSION_DENIED / UNAUTHENTICATED |
GrpcPermissionDeniedError |
Shown as red error message; user stays on the page. |
INVALID_ARGUMENT |
GrpcInvalidArgumentError |
Shown as red error message (validation failed). |
UNAVAILABLE |
GrpcUnavailableError |
Shown as red error message (service down). |
DEADLINE_EXCEEDED |
GrpcDeadlineExceededError |
Shown as red error message (timeout). |
| Other | GrpcAdminError |
Shown as generic error message. |
Mapping errors in your adapter:
from django_admin_grpc.exceptions import map_grpc_error
class MyAdapter(BaseGrpcServiceAdapter):
def get(self, resource_class, pk):
try:
return self.stub.Get(...)
except grpc.RpcError as exc:
raise self._map_rpc_error(exc)
You can also catch the typed exceptions in custom admin methods if you need special handling.
Customizing Appearance
Custom Widgets
You can override widgets per field when building the form class:
from django import forms
from django_admin_grpc.admin import GrpcResourceAdmin
class ProductAdmin(GrpcResourceAdmin):
resource_class = Product
adapter_class = CatalogAdapter
grpc_form_fields = ["name", "description", "price", "active"]
def _build_form_class(self):
return self.resource_class.build_form_class(
widgets={
"description": forms.Textarea(attrs={"rows": 8}),
"price": forms.NumberInput(attrs={"step": "0.01"}),
}
)
Or set defaults globally in settings.py:
GRPC_ADMIN = {
"DEFAULT_WIDGETS": {
"char": forms.TextInput,
"text": forms.Textarea,
"boolean": forms.CheckboxInput,
},
}
Custom Admin Base Class
GrpcResourceAdmin inherits from Django's ModelAdmin. If you use a custom admin theme (e.g. django-unfold, django-jazzmin, etc.), subclass with the theme's ModelAdmin after GrpcResourceAdmin:
from django.contrib import admin
from django_admin_grpc.admin import GrpcResourceAdmin
from unfold.admin import ModelAdmin as UnfoldModelAdmin
class MyGrpcAdmin(GrpcResourceAdmin, UnfoldModelAdmin):
pass
@admin.register(Product.admin_model())
class ProductAdmin(MyGrpcAdmin):
resource_class = Product
adapter_class = CatalogAdapter
Alternatively, use the factory helper:
MyGrpcAdmin = GrpcResourceAdmin.with_base(UnfoldModelAdmin)
Custom Templates
You can override templates per-resource via the resource Meta class:
class Product(BaseGrpcResource):
class Meta:
app_label = "catalog"
change_form_template = "myapp/product_change_form.html"
delete_confirm_template = "myapp/product_delete_confirm.html"
Or globally via settings.py:
GRPC_ADMIN = {
"DEFAULT_CHANGE_FORM_TEMPLATE": "myapp/change_form.html",
"DEFAULT_DELETE_CONFIRM_TEMPLATE": "myapp/delete_confirm.html",
"DEFAULT_CURSOR_PAGINATION_TEMPLATE": "myapp/cursor_pagination.html",
}
Configuration
Set these in your Django settings.py:
| Setting | Default | Description |
|---|---|---|
GRPC_ADMIN_TRACE_CONTEXT_PROVIDER |
None |
Callable or dotted path that returns a dict of trace headers injected into every gRPC call. |
GRPC_ADMIN_DEFAULT_PAGE_SIZE |
25 |
Default items per page for list views. |
GRPC_ADMIN_MAX_PAGE_SIZE |
100 |
Maximum items per page. |
GRPC_ADMIN_CURSOR_PAGINATION |
False |
Enable cursor-based pagination globally. |
GRPC_ADMIN_LOG_LEVEL |
"INFO" |
Log level for the package logger. |
DEFAULT_WIDGETS |
None |
Dict mapping field type to widget class or dotted path. |
DEFAULT_ADMIN_CLASS |
django.contrib.admin.ModelAdmin |
Dotted path to the base ModelAdmin subclass. |
DEFAULT_CHANGE_FORM_TEMPLATE |
django_admin_grpc/change_form.html |
Template for add/change views. |
DEFAULT_DELETE_CONFIRM_TEMPLATE |
django_admin_grpc/delete_confirm.html |
Template for delete confirmation. |
DEFAULT_CURSOR_PAGINATION_TEMPLATE |
django_admin_grpc/cursor_pagination.html |
Template for cursor pagination controls. |
Example Project
An example Django project is included in the example/ directory. It demonstrates a catalog microservice with in-memory adapters so you can run the admin locally without a real gRPC server.
cd example
python manage.py migrate
python manage.py runserver
Browse to /admin/ to see Products and Categories backed by gRPC-style adapters.
Contributing
- Fork the repository.
- Create a feature branch:
git checkout -b feature/my-change. - Install dev dependencies:
pip install -e '.[dev]'. - Run tests:
pytest. - Open a pull request.
Please include tests for new functionality and keep line coverage at 80% or higher.
License
MIT License. See LICENSE for details.
Metadata
Release files for django-admin-grpc 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_admin_grpc-0.2.2.tar.gz | 38.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_admin_grpc-0.2.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 82.7 kB
Release files / django_admin_grpc-0.2.2.tar.gz
| Download URL | django_admin_grpc-0.2.2.tar.gz |
|---|---|
| Size | 38.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fac9dd54aede29a2efaed255515e4d642adad9b26401b2761ed139bfbc162f84
|
|
BLAKE2b-256 checksum How to use checksums |
0b0acadd733a7e69b6f488f8e18a93bf4e8fc5f17ff5891ae0ee9f3496e177e5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.
Transparency logRelease files / django_admin_grpc-0.2.2-py3-none-any.whl
| Download URL | django_admin_grpc-0.2.2-py3-none-any.whl |
|---|---|
| Size | 44.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7183abce951fc2c98171f66b6b8e03941dda3ce486bf62fff888158f6ff229a5
|
|
BLAKE2b-256 checksum How to use checksums |
26061c6ed4721b743ad9310db72bbc9b3471342f70070f2d54e11386653a569f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.
Transparency log