Model Service Overview
ModelService is for CRUD-style endpoints over Django models with minimal boilerplate.
It auto-generates RPC methods from ModelServiceConfig and integrates filtering, searching, ordering and pagination.
Generated Endpoints
Depending on allowed_endpoints:
List(unary list)StreamList(server stream)DetailCreateUpdatePatchDelete
Minimal Example
from pydantic import BaseModel, ConfigDict
from grpc_extra import (
AllowedEndpoints,
ModelService,
ModelServiceConfig,
grpc_service,
)
class ProductOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
@grpc_service(app_label="products", package="products")
class ProductService(ModelService):
config = ModelServiceConfig(
model=Product,
allowed_endpoints=[
AllowedEndpoints.LIST,
AllowedEndpoints.DETAIL,
AllowedEndpoints.CREATE,
],
list_schema=ProductOut,
detail_schema=ProductOut,
create_schema=ProductOut,
)
Extended Example (with list customizations)
from grpc_extra import (
AllowedEndpoints as Endpoints,
LimitOffsetPagination,
ModelService,
ModelServiceConfig,
Ordering,
Searching,
grpc_service,
)
@grpc_service(name="ProductService", app_label="products", package="products")
class ProductService(ModelService):
config = ModelServiceConfig(
model=Product,
allowed_endpoints=[Endpoints.LIST, Endpoints.DETAIL],
list_schema=ProductListSchema,
detail_schema=ProductDetailSchema,
list_pagination_class=LimitOffsetPagination,
list_ordering_class=Ordering,
list_ordering_fields=["sku", "name"],
list_searching_class=Searching,
list_search_fields=["sku", "name", "description"],
)
Runtime order for list endpoint:
- searching
- ordering
- pagination
QuerySet Customization
For performance and relation preloading:
queryset: base queryset for list and general operationsdetail_queryset: dedicated queryset for detail endpoint
config = ModelServiceConfig(
model=Product,
queryset=Product.objects.select_related("category", "brand"),
detail_queryset=Product.objects.select_related(
"category",
"brand",
"supplier",
"warehouse",
),
...
)
Why this matters:
- list/detail often need different relation graphs
- avoids N+1 queries
- keeps endpoint-specific performance predictable
Generated Choice Endpoints
ModelService can also generate read-only RPCs for Django IntegerChoices and TextChoices.
This is useful when the service should expose enum-like reference data for clients, DWH services, forms, or other integrations without hand-writing one method per choice set.
from django.db import models
from grpc_extra import (
AllowedEndpoints as Endpoints,
ChoiceEndpointConfig,
ModelService,
ModelServiceConfig,
grpc_service,
)
class ProductStatus(models.IntegerChoices):
ACTIVE = 1, "Active"
ARCHIVED = 2, "Archived"
@grpc_service(
name="ProductService",
app_label="products",
package="products",
permissions=[IsAuthActive],
)
class ProductService(ModelService):
config = ModelServiceConfig(
model=Product,
allowed_endpoints=[Endpoints.LIST, Endpoints.DETAIL],
list_schema=ProductListSchema,
detail_schema=ProductDetailSchema,
choice_endpoints=[
ChoiceEndpointConfig(
name="Statuses",
source=ProductStatus,
description="List available product statuses.",
),
],
)
The builder generates:
- python handler:
statuses - RPC method:
Statuses - request schema:
google.protobuf.Empty - response schema:
list[IntChoiceSchema]
For TextChoices, the generated response uses TextChoiceSchema.
Available config fields:
name: public RPC method namesource: a Django choices class (or another object exposing.choices)description: optional RPC descriptionpermissions: optional method-level permissionsresponse_schema: optional custom schema override
Permission Semantics
- if
permissionsis omitted onChoiceEndpointConfig, the generated RPC inherits service-level permissions - if
permissions=[...]is provided, it overrides service-level permissions for that generated RPC
Example: make one choice endpoint public while keeping the rest of the service protected:
choice_endpoints=[
ChoiceEndpointConfig(
name="Statuses",
source=ProductStatus,
permissions=[AllowAny],
),
]
Data Helper
You can replace storage logic with custom helper:
from grpc_extra import ModelDataHelper
class CustomDataHelper(ModelDataHelper):
def list_objects(self, request):
return Product.objects.filter(is_deleted=False)
def get_object(self, request):
return Product.objects.get(pk=request.id, is_deleted=False)
def create_object(self, request):
return Product.objects.create(**request.model_dump())
def update_object(self, request):
...
def patch_object(self, request):
...
def delete_object(self, request):
...
Attach it:
@grpc_service(app_label="products", package="products")
class ProductService(ModelService):
data_helper_class = CustomDataHelper
config = ModelServiceConfig(...)
Error Semantics
- Missing object in
Detail->NOT_FOUND - Validation problems ->
INVALID_ARGUMENT - Permission denial ->
PERMISSION_DENIED
Permissions in ModelService
Permissions are declared the same way as regular services.
- service-level permissions via
@grpc_service(..., permissions=[...]) - method-level permissions via generated method metadata or custom methods
- method-level permissions override service-level permissions when declared explicitly
For Detail/Get, object-level checks are applied on both method-level and service-level permissions only when the method does not override service-level permissions.
Tips Before Production
- Start with
LIST + DETAILand stable schemas first. - Always define explicit
list_search_fieldsandlist_ordering_fields. - Use
queryset/detail_querysetfrom day one to avoid performance regressions. - Keep schema field types proto-compatible (
Decimalmaps to protostring).