Metadata-Version: 2.4
Name: django-ninja-service-objects
Version: 0.1.3
Summary: Service objects for Django Ninja using Pydantic validation
Author-email: Hans Ramírez <monkey@bekind.software>
License: MIT
Project-URL: Homepage, https://github.com/bekindsoft/ninja-service-objects
Project-URL: Documentation, https://github.com/bekindsoft/ninja-service-objects#readme
Project-URL: Repository, https://github.com/bekindsoft/ninja-service-objects
Project-URL: Issues, https://github.com/bekindsoft/ninja-service-objects/issues
Keywords: django,ninja,service-objects,pydantic,business-logic
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: django>=4.2
Requires-Dist: django-ninja>=1.0
Requires-Dist: pydantic>=2.0

# Django Ninja Service Objects

An implementation of the [django-service-objects](https://django-service-objects.readthedocs.io/en/latest/pages/philosophy.html) philosophy for Django Ninja with Pydantic validation.

Encapsulate your business logic in reusable, testable service classes.

## Installation

```bash
pip install django-ninja-service-objects
```

Add to your Django settings:

```python
# settings.py
INSTALLED_APPS = [
    ...
    'ninja_service_objects',
    ...
]
```

## Usage

```python
from ninja import Schema
from ninja_service_objects import Service

class CreateUserInput(Schema):
    email: str
    name: str

class CreateUserService(Service[CreateUserInput, User]):
    schema = CreateUserInput

    def process(self) -> User:
        return User.objects.create(
            email=self.cleaned_data.email,
            name=self.cleaned_data.name,
        )

    def post_process(self) -> None:
        # Called after successful transaction commit
        send_welcome_email(self.cleaned_data.email)

# In your view
user = CreateUserService.execute({"email": "test@example.com", "name": "Test"})
```

### Decorator-Based Services

For smaller operations, use `service_object` to get the same validation and
transaction handling without defining a service class:

```python
from ninja import Schema
from ninja_service_objects import service_object

class CreateUserInput(Schema):
    email: str
    name: str

def send_welcome_email_after_commit(user: User) -> None:
    send_welcome_email(user.email)

@service_object(post_process=send_welcome_email_after_commit)
def create_user(data: CreateUserInput) -> User:
    return User.objects.create(
        email=data.email,
        name=data.name,
    )

user = create_user({"email": "test@example.com", "name": "Test"})
```

The decorator validates any function argument annotated with a Pydantic model
or Ninja schema. Use `db_transaction=False` to disable the transaction wrapper,
`using="other_db"` to select a database alias, or `post_process=callback` to run
a side effect after a successful commit. The callback receives the service
function result.

### Using Pydantic BaseModel with Custom Validators

You can also use Pydantic's BaseModel directly for more complex validation:

```python
from pydantic import BaseModel, EmailStr, field_validator, model_validator
from ninja_service_objects import Service

class RegisterUserInput(BaseModel):
    email: EmailStr
    password: str
    password_confirm: str

    @field_validator("password")
    @classmethod
    def password_min_length(cls, v: str) -> str:
        if len(v) < 8:
            raise ValueError("Password must be at least 8 characters")
        return v

    @model_validator(mode="after")
    def passwords_match(self) -> "RegisterUserInput":
        if self.password != self.password_confirm:
            raise ValueError("Passwords do not match")
        return self

class RegisterUserService(Service[RegisterUserInput, User]):
    schema = RegisterUserInput

    def process(self) -> User:
        return User.objects.create_user(
            email=self.cleaned_data.email,
            password=self.cleaned_data.password,
        )
```

### Using ModelField for Django Model Instances

Use `ModelField` and `MultipleModelField` to validate Django model instances as service inputs:

```python
from pydantic import BaseModel
from ninja_service_objects import Service, ModelField, MultipleModelField

class TransferOwnershipInput(BaseModel):
    from_user: ModelField[User]
    to_user: ModelField[User]
    posts: MultipleModelField[Post]

class TransferOwnershipService(Service[TransferOwnershipInput, None]):
    schema = TransferOwnershipInput

    def process(self) -> None:
        for post in self.cleaned_data.posts:
            post.author = self.cleaned_data.to_user
            post.save()
```

By default, `ModelField` rejects unsaved model instances (objects without a primary key). To allow unsaved instances:

```python
from typing import Annotated

class MyInput(BaseModel):
    user: Annotated[User, ModelField(allow_unsaved=True)]
    items: Annotated[list[Item], MultipleModelField(allow_unsaved=True)]
```

## Features

- Pydantic validation for inputs
- Automatic database transaction handling
- `post_process` hook for side effects (runs after commit)
- Decorator-based services for lightweight operations
- Type-safe with generics support
- `ModelField` and `MultipleModelField` for Django model instance validation

## Design Decisions

### Why Pydantic instead of Django Forms?

The original django-service-objects uses Django Forms for validation. Since Django Ninja already uses Pydantic for request/response schemas, this library uses Pydantic to:

- Avoid mixing two validation systems in the same project
- Reuse your existing Django Ninja schemas as service inputs
- Get better type hints and IDE support

### API Compatibility

We maintain familiar patterns from django-service-objects:

- `cleaned_data` - Access validated input data (same naming as Django forms/original library)
- `process()` - Override this with your business logic
- `post_process()` - Runs after successful transaction commit (for emails, notifications, etc.)
- `execute()` - Class method entry point that handles validation and transactions

### What's Different

| django-service-objects | ninja-service-objects |
|------------------------|----------------------|
| Django Forms validation | Pydantic validation |
| `service_clean()` method | Pydantic validators |
| Form fields | Pydantic BaseModel |
| `is_valid()` + `execute()` | Single `execute()` call |

## Configuration

### Transaction Control

```python
class MyService(Service[MyInput, MyOutput]):
    schema = MyInput
    db_transaction = False  # Disable automatic transaction wrapping
    using = "other_db"      # Use a different database alias
```

## License

MIT
