Metadata-Version: 2.5
Name: django-nx
Version: 0.7.0
Summary: Lightweight Django field utilities and extensions
License-File: LICENSE
Requires-Python: >=3.9
Requires-Dist: django<6.1,>=3.2
Requires-Dist: djangorestframework<3.18,>=3.12.4
Requires-Dist: pyhumps
Requires-Dist: shortuuid<1.1,>=1.0.13
Description-Content-Type: text/markdown

# django-nx

Lightweight Django field utilities and extensions that reduce boilerplate and enforce sensible defaults.

[![PyPI version](https://badge.fury.io/py/django-nx.svg)](https://pypi.org/project/django-nx/)
[![Python](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-3.2%7C4.x%7C5.x-green)](https://www.djangoproject.com/)

## Installation

```bash
pip install django-nx
```

Requires **Python >= 3.9**, **Django >= 3.2**, and **Django REST Framework >= 3.12.4**.

---

## Quick Start

```python
import nx

class Product(nx.Model):
    name = nx.CharField('Name') # Default max_length=128
    price = nx.MoneyField('Price')
    status = nx.IntChoiceField('Status', choices=ProductStatus)
    tags = nx.ArrayField('Tags')
    metadata = nx.ObjectField('Metadata')
```

---

## Table of Contents

- [Model Fields](#model-fields)
- [Choices](#choices)
- [Base Model](#base-model)
- [QuerySet](#queryset)
- [DRF Serializers](#drf-serializers)
- [DRF Views](#drf-views)
- [DRF Router](#drf-router)
- [Utilities](#utilities)
- [AI Coding Agents](#ai-coding-agents)

---

## Model Fields

All fields automatically use `verbose_name` as `help_text` when `help_text` is not explicitly provided.

### Character Fields

| Field | Default | Description |
|-------|---------|-------------|
| `CharField` | `default=""`, `blank=True`, `max_length=128` | String field with empty string default |
| `TextField` | `default=""`, `blank=True` | Long text field with empty string default |
| `TextChoiceField` | `max_length=64`, defaults to first choice | CharField backed by choices enum |

```python
name = nx.CharField('Name', max_length=255)
description = nx.TextField('Description')
priority = nx.TextChoiceField('Priority', choices=PriorityLevel)
```

### Numeric Fields

| Field | Default | Description |
|-------|---------|-------------|
| `IntegerField` | — | Standard integer with auto help_text |
| `MoneyField` | `max_digits=18`, `decimal_places=2`, `default=Decimal('0')` | Decimal field for monetary values |
| `IntChoiceField` | `default=first_choice` | SmallIntegerField backed by choices enum |

```python
quantity = nx.IntegerField('Quantity')
price = nx.MoneyField('Price')           # DECIMAL(18,2)
discount = nx.MoneyField('Discount', max_digits=5, decimal_places=4)
status = nx.IntChoiceField('Status', choices=OrderStatus)
```

### Boolean & Temporal Fields

| Field | Default | Description |
|-------|---------|-------------|
| `BooleanField` | `default=False`, `blank=True` | Boolean flag |
| `DateField` | `null=True`, `blank=True` | Date picker |
| `DateTimeField` | `null=True`, `blank=True` | DateTime picker |

```python
is_active = nx.BooleanField('Is Active')
published_at = nx.DateTimeField('Published At')
birth_date = nx.DateField('Birth Date')
```

### Relationship Fields

| Field | Default | Description |
|-------|---------|-------------|
| `ForeignKey` | `null=True`, `blank=True`, `on_delete=CASCADE` | Standard FK with nullable defaults |
| `OneToOne` | `null=True`, `blank=True`, `on_delete=CASCADE` | One-to-one with nullable defaults |
| `ManyToMany` | `blank=True` | Many-to-many relation |
| `ShadowForeignKey` | `db_constraint=False` | FK without database constraint |
| `ShadowOneToOne` | `db_constraint=False` | One-to-one without DB constraint |
| `ShadowManyToMany` | `db_constraint=False` | Many-to-many without DB constraint |

```python
user = nx.ForeignKey('auth.User', 'User')
profile = nx.OneToOne('accounts.Profile', 'Profile')
tags = nx.ManyToMany('products.Tag', 'Tags')

# Soft / logical foreign key (no DB-level constraint)
legacy_id = nx.ShadowForeignKey('legacy.Model', 'Legacy Ref')
```

### JSON & UUID Fields

| Field | Default | Description |
|-------|---------|-------------|
| `ObjectField` | `default=dict`, `blank=True` | JSONField defaulting to `{}` |
| `ArrayField` | `default=list`, `blank=True` | JSONField defaulting to `[]` |
| `ShortUUIDField` | `max_length=22`, auto-generated | URL-safe concise UUID |

```python
config = nx.ObjectField('Config')
items = nx.ArrayField('Items')
code = nx.ShortUUIDField('Code')
```

---

## Base Model

`nx.Model` is an abstract base model that provides:

- **`created_at`** – auto_now_add timestamp
- **`updated_at`** – auto_now timestamp
- **`is_deleted`** – soft-delete flag (default `False`)
- **Auto-generated `db_table`** – `{app_label}_{snake_case_model_name}` via `humps.decamelize`

```python
import nx

class Product(nx.Model):
    name = nx.CharField('Name', max_length=255)

    class Meta:
        app_label = 'shop'
        # db_table = 'shop_product'  # auto-generated if omitted
```

> Explicitly set `Meta.db_table` to skip auto-naming.

---

## QuerySet

`nx.QuerySet` adds soft-delete aware query methods. It reads `deleted_field` from `Model.Meta` (defaults to `is_deleted`).

```python
from nx.models.querysets import QuerySet

class ProductQuerySet(QuerySet):
    pass

class Product(nx.Model):
    ...
    class Meta:
        deleted_field = 'is_deleted'

Product.objects.valid()     # is_deleted=False / 0
Product.objects.invalid()   # is_deleted=True / 1
```

---

## DRF Serializers

| Class | Description |
|-------|-------------|
| `nx.drf.MoneyField` | `DecimalField(max_digits=18, decimal_places=2)` |
| `nx.drf.QuantityField` | `IntegerField(min_value=0)` |
| `nx.drf.MethodField` | Alias for `SerializerMethodField` |
| `nx.drf.AutoInstanceLookupMixin` | Mixin that auto-looks up instance by `id` on save |

```python
from rest_framework import serializers
import nx

class ProductSerializer(nx.drf.AutoInstanceLookupMixin, serializers.ModelSerializer):
    price = nx.drf.MoneyField()
    stock = nx.drf.QuantityField()
    category_name = nx.drf.MethodField()

    class Meta:
        model = Product
        fields = ['id', 'price', 'stock', 'category_name']

    def get_category_name(self, obj):
        return obj.category.name if obj.category else None
```

---

## DRF Views

### ListMetadataMixin

Inject a top-level `meta` object (or any custom root key) into `list` responses.

```python
from rest_framework import viewsets
import nx

class ProductViewSet(nx.drf.ListMetadataMixin, viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    list_metadata_root = "meta"  # optional; omit to merge at top-level

    def get_list_metadata(self, request, queryset, response):
        return {
            "total": queryset.count(),
            "timestamp": timezone.now().isoformat(),
        }
```

**Response shape with `list_metadata_root = "meta"`:**

```json
{
  "count": 100,
  "results": [...],
  "meta": {
    "total": 100,
    "timestamp": "2024-01-15T09:30:00Z"
  }
}
```

**Response shape without `list_metadata_root`:**

```json
{
  "count": 100,
  "results": [...],
  "total": 100,
  "timestamp": "2024-01-15T09:30:00Z"
}
```

---

## DRF Router

`nx.Router` wraps DRF's `DefaultRouter`. Routes have no trailing slash, and
`register()` derives the default basename from the singular form of `prefix`.
An explicitly supplied basename is left unchanged.

```python
import nx

router = nx.Router()
router.register("products", ProductViewSet)

# Route: /products (not /products/)
# Basename: product
```

---

## AI Coding Agents

django-nx ships a version-matched public API contract at `nx/ai_context.md` and
publishes an [llms.txt](https://github.com/JoshYuJump/django-nx/blob/main/llms.txt)
discovery file. AI coding agents should read
the installed contract before changing Django models or DRF code, so their
guidance matches the version used by the application.

If a project uses both django-nx and an AI coding agent, add the following
instructions to that project's `README.md`:

````md
## django-nx development rules

This project uses django-nx. Before changing Django models, serializers,
viewsets, or routes, locate and read the API contract bundled with the installed
django-nx version:

```bash
python -c 'from importlib.metadata import distribution; print(distribution("django-nx").locate_file("nx/ai_context.md"))'
```

- Use `import nx` and the public APIs documented in `nx/ai_context.md`.
- Prefer django-nx fields, model helpers, serializers, views, and router when
  the contract provides the required behavior.
- Do not inspect django-nx implementation source unless the public contract is
  insufficient or you are diagnosing a django-nx defect.
- Do not depend on undocumented names or internal implementation details.
````

When the installed Python environment is unavailable, use the
[online AI API context](https://raw.githubusercontent.com/JoshYuJump/django-nx/main/nx/ai_context.md)
as a fallback. The installed contract takes precedence because it matches the
project's installed django-nx version.

Ready-to-copy rules for AGENTS.md, Claude Code, and Cursor are available in the
[AI project setup guide](https://github.com/JoshYuJump/django-nx/blob/main/docs/ai-project-setup.md).

The preferred import is `import nx`. The older `from nx import nx` form remains
supported for backwards compatibility.

---

## Utilities

### get_stat_datetime_range

Returns `today`, `week`, `month`, and `year` datetime ranges respecting `USE_TZ`.

```python
from nx.utils import get_stat_datetime_range

ranges = get_stat_datetime_range()
# ranges.today  -> (2024-01-15 00:00:00, 2024-01-15 23:59:59.999999)
# ranges.week   -> (Mon 00:00:00, Sun 23:59:59.999999)
# ranges.month  -> (1st 00:00:00, last_day 23:59:59.999999)
# ranges.year   -> (Jan 1 00:00:00, Dec 31 23:59:59.999999)
```

---

## Development

```bash
# Install dependencies
uv sync

# Run tests
pytest

# Lint
ruff check .

# Build
uv build
```

---

## License

MIT License — see [LICENSE](LICENSE) for details.
