Skip to content
Merged

Dev #31

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
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@

# Lazy Ninja

**Lazy Ninja** is a Django library that simplifies the creation of CRUD API endpoints using Django Ninja. It dynamically scans your Django models and generates Pydantic schemas for listing, retrieving, creating, and updating records. The library also allows you to customize behavior through hook functions (controllers) and schema configurations.
**Lazy Ninja** is a Django library that simplifies the generation of API endpoints using Django Ninja. It dynamically scans your Django models and generates Pydantic schemas for listing, retrieving, creating, and updating records. The library also allows you to customize behavior through hook functions (controllers) and schema configurations.

By leveraging Django Ninja, Lazy Ninja provides automatic, interactive API documentation via OpenAPI, making it easy to visualize and interact with your endpoints.

> **Note:** This pre-release (alpha) version supports only JSON data. `multipart/form-data` (file uploads) is not yet available. For `ImageField` or `FileField`, use the full URL as a string.

---

## Installation
Expand Down Expand Up @@ -47,7 +45,7 @@ Add `api.urls` to your `urls.py` to expose the endpoints.

## Features

- **Automatic CRUD Endpoints**: Instantly generate API routes for your Django models.
- **Automatic Endpoints**: Instantly generate API routes for your Django models.
- **Dynamic Schema Generation**: Automatically create Pydantic schemas for your models.
- **Custom Controllers**: Customize route behavior with hooks like `before_create` and `after_update`.
- **Built-in Filtering, Sorting, and Pagination**: Simplify data handling with query parameters.
Expand All @@ -60,7 +58,7 @@ Add `api.urls` to your `urls.py` to expose the endpoints.
- [x] Basic CRUD operations
- [x] Asynchronous support
- [x] Filtering, sorting, and pagination
- [ ] File upload support
- [X] File upload support
- [ ] Authentication and RBAC
- [ ] Advanced model relationships

Expand Down
85 changes: 77 additions & 8 deletions docs/docs/en/index.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
# Lazy Ninja: Generate CRUD API Endpoints for Django
# Lazy Ninja: Generate API Endpoints for Django

[![PyPI version](https://badge.fury.io/py/lazy-ninja.svg)](https://badge.fury.io/py/lazy-ninja)
[![Downloads](https://static.pepy.tech/personalized-badge/lazy-ninja?period=total&units=international_system&left_color=grey&right_color=blue&left_text=Downloads)](https://pepy.tech/project/lazy-ninja)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

>**Note:** This version supports only JSON data. `multipart/form-data` (file uploads) is not yet available. For `ImageField` or `FileField`, use the full URL as a string


**Lazy Ninja** is a Django library that automates the generation of CRUD API endpoints with Django Ninja. It dynamically scans your Django models and creates Pydantic schemas for listing, detailing, creating, and updating records—all while allowing you to customize behavior via hook functions (controllers) and schema configurations.
**Lazy Ninja** is a Django library that automates the generation of API endpoints with Django Ninja. It dynamically scans your Django models and creates Pydantic schemas for listing, detailing, creating, and updating records—all while allowing you to customize behavior via hook functions (controllers) and schema configurations.

By leveraging Django Ninja, Lazy Ninja benefits from automatic, interactive API documentation generated through OpenAPI (Swagger UI and ReDoc), giving developers an intuitive interface to quickly visualize and interact with API endpoints.

>**Async by Default**: Lazy Ninja is designed to be asynchronous by default, leveraging Django's async capabilities for better performance. However, if needed, you can configure it to use synchronous operations by setting the `is_async` parameter to `False`.

**Key Features:**

- **Instant CRUD Endpoints:** Automatically generate API endpoints from Django models.
- **Instant API Endpoints:** Automatically generate API endpoints from Django models.
- **Dynamic Schema Generation:** Automatically create Pydantic models.
- **Customizable Hooks:** Add pre-processing, post-processing, or custom logic to routes by creating controllers for specific models.
- **Smart Filtering/Sorting:** Built-in support for filters like `field=value` or `field>value`.
Expand Down Expand Up @@ -96,7 +93,7 @@ python manage.py runserver

## Instant API Endpoints

Your CRUD API is now live at `http://localhost:8000/api` with these endpoints:
Your API is now live at `http://localhost:8000/api` with these endpoints:

| Method | URL | Action |
| ------ | -------------------| ---------------|
Expand Down Expand Up @@ -281,5 +278,77 @@ api = DynamicAPI(api, pagination_type="page-number")

Alternatively, set NINJA_PAGINATION_CLASS in settings.py to override the default globally.

### License
---
## File Upload Support

Lazy Ninja now supports handling file uploads for models with `FileField` and `ImageField` using `multipart/form-data`. This feature allows you to define which fields should use `multipart/form-data` and provides flexibility to handle mixed models where some routes use JSON while others use `multipart/form-data`.

### How to Use File Upload Parameters

When initializing `DynamicAPI`, you can configure the following parameters:

- **`file_fields`**: Specify which fields in a model should use `multipart/form-data`.
- **`use_multipart`**: Explicitly define whether `create` and `update` operations for specific models should use `multipart/form-data`.
- **`auto_multipart`**: Automatically detect file fields in models and enable `multipart/form-data` for them (default: `True`).
- **`auto_detect_files`**: Automatically detect `FileField` and `ImageField` in models (default: `True`).
- **`auto_multipart`**: Automatically enable `multipart/form-data` for models with detected file fields (default: `True`).

### Example Usage
```python
from ninja import NinjaAPI
from lazy_ninja.builder import DynamicAPI

api = NinjaAPI()

auto_api = DynamicAPI(
api,
is_async=True,
file_fields={"Gallery": ["images"], "Product": ["pimages"]}, # Specify file fields
use_multipart={
"Product": {
"create": True, # Use multipart/form-data for creation
"update": True # Use multipart/form-data for updates
}
},
auto_detect_files=True, # Automatically detect file fields in models
auto_multipart=True # Automatically enable multipart/form-data for detected file fields
)

auto_api.register_all_models()
```

In this example:
- The `Gallery` model will use `multipart/form-data` for the `images` field.
- The `Product` model will use `multipart/form-data` for the `pimages` field during `create` and `update` operations.
- Models without file fields will continue to use JSON by default.

---

## Custom Middleware for PUT/PATCH with Multipart

To handle `PUT` and `PATCH` requests with `multipart/form-data`, Lazy Ninja includes a custom middleware: `ProcessPutPatchMiddleware`. This middleware ensures that `PUT` and `PATCH` requests are processed correctly by temporarily converting them to `POST` for form data handling.

### Why This Middleware is Needed
Django has a known limitation where `PUT` and `PATCH` requests with `multipart/form-data` are not processed correctly. While Django Ninja recently introduced updates to address this, certain scenarios still require a custom solution. This middleware resolves those issues and ensures compatibility with both synchronous and asynchronous request handlers.

### How to Use the Middleware
Add the middleware to your Django project's `MIDDLEWARE` setting:
```python
MIDDLEWARE = [
...
'lazy_ninja.middleware.ProcessPutPatchMiddleware',
...
]
```

### How It Works
- Converts `PUT` and `PATCH` requests with `multipart/form-data` to `POST` temporarily for proper processing.
- Restores the original HTTP method after processing the request.

---

## Contributing & Feedback
Lazy Ninja is still evolving — contributions, suggestions, and feedback are more than welcome! Feel free to open issues, discuss ideas, or submit PRs.

## License
This project is licensed under the terms of the MIT license.
40 changes: 20 additions & 20 deletions src/lazy_ninja/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,7 @@
from .helpers import get_hook
from .registry import ModelRegistry, controller_for
from .routes import register_model_routes_internal
from .middleware import ErrorHandlingMiddleware

__all__ = [
'ErrorHandlingMiddleware',
'LazyNinjaError',
'NotFoundError',
'ValidationError',
'DatabaseOperationError',
'SynchronousOperationError',
]
from .file_upload import FileUploadConfig

def register_model_routes(
api: NinjaAPI,
Expand All @@ -29,31 +20,37 @@ def register_model_routes(
create_schema: Optional[Type[Schema]] = None,
update_schema: Optional[Type[Schema]] = None,
pagination_strategy: Optional[str] = None,
file_upload_config: Optional[FileUploadConfig] = None,
use_multipart_create: bool = False,
use_multipart_update: bool = False,
is_async: bool = True
) -> None:
"""
Main function to register CRUD routes for a Django model using Django Ninja.

Parameters:
- api: Instance of NinjaAPI.
- model: The Django model class.
- base_url: Base URL for the resource endpoints.
- list_schema: Pydantic schema for listing objects.
- detail_schema: Pydantic schema for retrieving object details.
- create_schema: (Optional) Pydantic schema for creating an object.
- update_schema: (Optional) Pydantic schema for updating an object.
Args:
api: NinjaAPI instance.
model: Django model class.
base_url: Base URL for the routes.
list_schema: Schema for list responses.
detail_schema: Schema for detail responses.
create_schema: (Optional) Schema for create requests.
update_schema: (Optional) Schema for update requests.
pagination_strategy: (Optional) Strategy for pagination.
file_upload_config: (Optional) Configuration for file uploads.
use_multipart_create: Whether to use multipart/form-data for create endpoint.
use_multipart_update: Whether to use multipart/form-data for update endpoint.
is_async: Whether to use async routes (default: True).

This function retrieves the registered controller for the model (if any)
and passes its hooks to the internal route registration function.
"""
# Retrieve the custom controller for the model; use BaseModelController if none is registered.
ModelRegistry.discover_controllers()

controller = ModelRegistry.get_controller(model.__name__)
if not controller:
controller = BaseModelController

# Call the internal function that sets up the router and registers all endpoints.
register_model_routes_internal(
api=api,
model=model,
Expand All @@ -71,5 +68,8 @@ def register_model_routes(
after_delete=get_hook(controller, 'after_delete'),
custom_response=get_hook(controller, 'custom_response'),
pagination_strategy=pagination_strategy,
file_upload_config=file_upload_config,
use_multipart_create=use_multipart_create,
use_multipart_update=use_multipart_update,
is_async=is_async
)
50 changes: 48 additions & 2 deletions src/lazy_ninja/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .utils import generate_schema
from .helpers import to_kebab_case
from .pagination import get_pagination_strategy
from .file_upload import FileUploadConfig, detect_file_fields

p = inflect.engine()

Expand Down Expand Up @@ -89,14 +90,17 @@ def __init__(
schema_config: Optional[Dict[str, Dict[str, List[str]]]] = None,
custom_schemas: Optional[Dict[str, Dict[str, Type[Schema]]]] = None,
pagination_type: Optional[str] = None,
file_fields: Optional[Dict[str, List[str]]] = None,
auto_detect_files: bool = True,
auto_multipart: bool = True,
use_multipart: Optional[Dict[str, Dict[str, bool]]] = None,
is_async: bool = True
):
"""
Initializes the DynamicAPI instance.

Args:
api: The NinjaAPI instance.
is_async: Whether to use async routes (default: True).
exclude: Configuration for model/app exclusions.
schema_config: Dictionary mapping model names to schema configurations
(e.g., exclude fields and optional fields).
Expand All @@ -106,7 +110,14 @@ def __init__(
If a schema is not provided for a specific operation, the default generated schema will be used.
pagination_type: Type of pagination to use ('limit-offset' or 'page-number').
If None, uses NINJA_PAGINATION_CLASS from settings.

file_fields: Dictionary mapping model names to lists of file field names
(e.g., {"Product": ["image", "document"]}).
auto_detect_files: Whether to automatically detect file fields in models.
auto_multipart: Whether to automatically use multipart for models with file fields.
use_multipart: Dictionary specifying which models should use multipart/form-data
(e.g., {"Product": {"create": True, "update": True}}).
is_async: Whether to use async routes (default: True).

Pagination Configuration:
The pagination can be configured in three ways (in order of precedence):
1. pagination_type parameter in DynamicAPI
Expand All @@ -121,6 +132,15 @@ def __init__(
self.custom_schemas = custom_schemas or {}
self.is_async = is_async
self.pagination_strategy = get_pagination_strategy(pagination_type=pagination_type)

self.file_fields = file_fields or {}
self.use_multipart = use_multipart or {}
self.auto_detect_files = auto_detect_files
self.auto_multipart = auto_multipart
self.file_upload_config = FileUploadConfig(
file_fields=self.file_fields
)

self._already_registered = False

@staticmethod
Expand Down Expand Up @@ -165,6 +185,29 @@ def _register_all_models_sync(self) -> None:
create_schema = generate_schema(model, exclude=exclude_fields, optional_fields=optional_fields)
update_schema = generate_schema(model, exclude=exclude_fields, optional_fields=optional_fields, update=True)

detected_single_file_fields = []
detected_multiple_file_fields = []

if self.auto_detect_files:
detected_single_file_fields, detected_multiple_file_fields = detect_file_fields(model)

model_file_fields = list(set(self.file_fields.get(model_name, []) + detected_single_file_fields))

if model_file_fields:
self.file_upload_config.file_fields[model_name] = model_file_fields

if detected_multiple_file_fields:
existing = self.file_upload_config.multiple_file_fields.get(model_name, [])
self.file_upload_config.multiple_file_fields[model_name] = list(set(existing + detected_multiple_file_fields))

use_multipart_create = self.use_multipart.get(model_name, {}).get("create", False)
use_multipart_update = self.use_multipart.get(model_name, {}).get("update", False)

has_any_file_fields = bool(model_file_fields) or bool(detected_multiple_file_fields)
if self.auto_multipart and has_any_file_fields:
use_multipart_create = True
use_multipart_update = True

register_model_routes(
api=self.api,
model=model,
Expand All @@ -174,6 +217,9 @@ def _register_all_models_sync(self) -> None:
create_schema=create_schema,
update_schema=update_schema,
pagination_strategy=self.pagination_strategy,
file_upload_config=self.file_upload_config if model_file_fields else None,
use_multipart_create=use_multipart_create,
use_multipart_update=use_multipart_update,
is_async=getattr(self, 'is_async', True)
)

Expand Down
Loading