Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies = [
"django-ninja>=0.22",
"pydantic>=2.0",
"inflect>=7.5",
"colorama>=0.4.6"
]

classifiers = [
Expand Down
12 changes: 12 additions & 0 deletions src/lazy_ninja/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,21 @@

from django.db.models import QuerySet, Model
from django.core.exceptions import FieldDoesNotExist
from django.db import models

from ninja import Schema

def parse_model_id(model: Type[models.Model], item_id: str) -> Any:
"""
Converts a path parameter string to the correct type for the model's PK.
Supports AutoField (int) and UUIDField (str).
"""
pk_field = model._meta.pk
if isinstance(pk_field, models.AutoField) and item_id.isdigit():
return int(item_id)
return item_id


def get_hook(controller: Optional[Any], hook_name: str) -> Optional[Callable]:
"""
Safely get a hook method from a controller.
Expand Down
42 changes: 25 additions & 17 deletions src/lazy_ninja/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
execute_hook_async,
)
from .pagination import BasePagination
from .helpers import execute_hook, handle_response, apply_filters, apply_filters_async
from .helpers import execute_hook, handle_response, apply_filters, apply_filters_async, parse_model_id
from .errors import handle_exception, handle_exception_async
from .file_upload import FileUploadConfig, detect_file_fields

Expand Down Expand Up @@ -107,10 +107,11 @@ async def list_items(
return await handle_exception_async(e)

@router.get("/{item_id}", response=detail_schema, tags=[model.__name__], operation_id=f"get_{model_name}")
async def get_item(request, item_id: int) -> Any:
async def get_item(request, item_id: str) -> Any:
"""Retrieve a single object by ID."""
try:
instance = await get_object_or_404_async(model, id=item_id)
item_id_value = parse_model_id(model, item_id)
instance = await get_object_or_404_async(model, id=item_id_value)
return await handle_response_async(instance, detail_schema, custom_response, request)
except Exception as e:
return await handle_exception_async(e)
Expand Down Expand Up @@ -238,10 +239,11 @@ async def create_item(request, payload: create_schema) -> Any: #type: ignore
if update_schema:
if use_multipart_update:
@router.patch("/{item_id}", response=detail_schema, tags=[model.__name__], operation_id=f"update_{model_name}")
async def update_item(request, item_id: int, payload: update_schema = Form(...)) -> Any: #type: ignore
async def update_item(request, item_id: str, payload: update_schema = Form(...)) -> Any: #type: ignore
"""Update an existing object."""
try:
instance = await get_object_or_404_async(model, id=item_id)
item_id_value = parse_model_id(model, item_id)
instance = await get_object_or_404_async(model, id=item_id_value)

if before_update:
payload = await execute_hook_async(before_update, request, instance, payload, update_schema) or payload
Expand Down Expand Up @@ -341,10 +343,11 @@ async def update_item(request, item_id: int, payload: update_schema = Form(...))
return await handle_exception_async(e)
else:
@router.patch("/{item_id}", response=detail_schema, tags=[model.__name__], operation_id=f"update_{model_name}")
async def update_item(request, item_id: int, payload: update_schema) -> Any: #type: ignore
async def update_item(request, item_id: str, payload: update_schema) -> Any: #type: ignore
"""Update an existing object."""
try:
instance = await get_object_or_404_async(model, id=item_id)
item_id_value = parse_model_id(model, item_id)
instance = await get_object_or_404_async(model, id=item_id_value)

if before_update:
payload = await execute_hook_async(before_update, request, instance, payload, update_schema) or payload
Expand All @@ -365,10 +368,11 @@ async def update_item(request, item_id: int, payload: update_schema) -> Any: #ty
return await handle_exception_async(e)

@router.delete("/{item_id}", response={200: Dict[str, str]}, tags=[model.__name__], operation_id=f"delete_{model_name}")
async def delete_item(request, item_id: int) -> Dict[str, str]:
async def delete_item(request, item_id: str) -> Dict[str, str]:
"""Delete an object."""
try:
instance = await get_object_or_404_async(model, id=item_id)
item_id_value = parse_model_id(model, item_id)
instance = await get_object_or_404_async(model, id=item_id_value)

if before_delete:
await execute_hook_async(before_delete, request, instance)
Expand Down Expand Up @@ -399,10 +403,11 @@ def list_items(request, q: Optional[str] = None, sort: Optional[str] = None,
return handle_exception(e)

@router.get("/{item_id}", response=detail_schema, tags=[model.__name__], operation_id=f"get_{model_name}")
def get_item(request, item_id: int) -> Any:
def get_item(request, item_id: str) -> Any:
"""Retrieve a single object by ID."""
try:
instance = get_object_or_404(model, id=item_id)
item_id_value = parse_model_id(model, item_id)
instance = get_object_or_404(model, id=item_id_value)
return handle_response(instance, detail_schema, custom_response, request)
except Exception as e:
return handle_exception(e)
Expand Down Expand Up @@ -508,10 +513,11 @@ def create_item(request, payload: create_schema) -> Any: #type: ignore
if update_schema:
if use_multipart_update:
@router.patch("/{item_id}", response=detail_schema, tags=[model.__name__], operation_id=f"update_{model_name}")
def update_item(request, item_id: int, payload: update_schema = Form(...)) -> Any: #type: ignore
def update_item(request, item_id: str, payload: update_schema = Form(...)) -> Any: #type: ignore
"""Update an existing object."""
try:
instance = get_object_or_404(model, id=item_id)
item_id_value = parse_model_id(model, item_id)
instance = get_object_or_404(model, id=item_id_value)

if before_update:
payload = execute_hook(before_update, request, instance, payload, update_schema) or payload
Expand Down Expand Up @@ -593,10 +599,11 @@ def update_item(request, item_id: int, payload: update_schema = Form(...)) -> An

else:
@router.patch("/{item_id}", response=detail_schema, tags=[model.__name__], operation_id=f"update_{model_name}")
def update_item(request, item_id: int, payload: update_schema) -> Any: #type: ignore
def update_item(request, item_id: str, payload: update_schema) -> Any: #type: ignore
"""Update an existing object."""
try:
instance = get_object_or_404(model, id=item_id)
item_id_value = parse_model_id(model, item_id)
instance = get_object_or_404(model, id=item_id_value)
if before_update:
payload = execute_hook(before_update, request, instance, payload, update_schema) or payload
data = convert_foreign_keys(model, payload.model_dump(exclude_unset=True))
Expand All @@ -612,10 +619,11 @@ def update_item(request, item_id: int, payload: update_schema) -> Any: #type: ig
return handle_exception(e)

@router.delete("/{item_id}", response={200: Dict[str, str]}, tags=[model.__name__], operation_id=f"delete_{model_name}")
def delete_item(request, item_id: int) -> Dict[str, str]:
def delete_item(request, item_id: str) -> Dict[str, str]:
"""Delete an object."""
try:
instance = get_object_or_404(model, id=item_id)
item_id_value = parse_model_id(model, item_id)
instance = get_object_or_404(model, id=item_id_value)
if before_delete:
execute_hook(before_delete, request, instance)
instance.delete()
Expand Down
169 changes: 0 additions & 169 deletions src/lazy_ninja/utils.py

This file was deleted.

18 changes: 15 additions & 3 deletions src/lazy_ninja/utils/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.shortcuts import get_object_or_404



def convert_foreign_keys(model: Type[models.Model], data: Dict[str, Any]) -> Dict[str, Any]:
"""
Converts integer values for ForeignKey fields in `data` to the corresponding model instances.
Expand All @@ -25,7 +26,7 @@ def convert_foreign_keys(model: Type[models.Model], data: Dict[str, Any]) -> Dic
for field in model._meta.fields:
if isinstance(field, models.ForeignKey) and field.name in data:
fk_value = data[field.name]
if isinstance(fk_value, int):
if isinstance(fk_value, (int, str)):
data[field.name] = field.related_model.objects.get(pk=fk_value)
return data

Expand Down Expand Up @@ -74,10 +75,18 @@ def serialize_model_instance(obj: models.Model) -> Dict[str, Any]:

if value is None:
data[field.name] = None
elif isinstance(field, models.UUIDField):
data[field.name] = str(value)
elif isinstance(field, (models.DateField, models.DateTimeField)):
data[field.name] = value.isoformat() if value else None
elif isinstance(field, (models.ImageField, models.FileField)):
data[field.name] = value.url if hasattr(value, 'url') else str(value)
elif isinstance(field, models.ForeignKey):
target_field = field.target_field
if isinstance(target_field, models.UUIDField):
data[field.name] = str(value) if value else None
else:
data[field.name] = value
elif hasattr(value, 'pk'):
data[field.name] = value.pk
else:
Expand Down Expand Up @@ -110,7 +119,9 @@ def get_pydantic_type(field: models.Field) -> Type:
Returns:
Python type for Pydantic
"""
if isinstance(field, models.AutoField):
if isinstance(field, models.UUIDField):
return str
elif isinstance(field, models.AutoField):
return int
elif isinstance(field, (models.CharField, models.TextField)):
return str
Expand All @@ -127,7 +138,8 @@ def get_pydantic_type(field: models.Field) -> Type:
elif isinstance(field, (models.ImageField, models.FileField)):
return str
elif isinstance(field, models.ForeignKey):
return int
target_field = field.target_field
return get_pydantic_type(target_field)
else:
return str

Expand Down
1 change: 0 additions & 1 deletion src/lazy_ninja/utils/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from .base import serialize_model_instance, serialize_model_instance_async


class BaseModelUtils:
"""Base class for model utilities."""

Expand Down