Metadata-Version: 2.4
Name: django-flex
Version: 26.1.2
Summary: A flexible query language for Django - enable frontends to dynamically construct database queries
Author: Nehemiah Jacob
Maintainer: Nehemiah Jacob
License: MIT
Project-URL: Homepage, https://github.com/n3h3m/django-flex
Project-URL: Documentation, https://github.com/n3h3m/django-flex#readme
Project-URL: Repository, https://github.com/n3h3m/django-flex.git
Project-URL: Issues, https://github.com/n3h3m/django-flex/issues
Project-URL: Changelog, https://github.com/n3h3m/django-flex/blob/main/CHANGELOG.md
Keywords: django,query,api,flexible,dynamic,graphql-alternative,rest,orm
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 3.2
Classifier: Framework :: Django :: 4.0
Classifier: Framework :: Django :: 4.1
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
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.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: django>=3.2
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-django>=4.5; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: black>=23.0; extra == "dev"
Requires-Dist: isort>=5.12; extra == "dev"
Requires-Dist: flake8>=6.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: django-stubs>=4.0; extra == "dev"
Dynamic: license-file

# Django-Flex

[![PyPI version](https://badge.fury.io/py/django-flex.svg)](https://pypi.org/project/django-flex/)
[![Python versions](https://img.shields.io/pypi/pyversions/django-flex.svg)](https://pypi.org/project/django-flex/)
[![Django versions](https://img.shields.io/badge/django-3.2%20%7C%204.x%20%7C%205.x%20%7C%206.x-blue.svg)](https://www.djangoproject.com/)

**A flexible query language for Django** — Enable frontends to dynamically construct database queries with built-in security.

## Features

- 🔍 **Dynamic Field Selection** — Request only the fields you need
- 🔬 **Rich Filtering** — Full Django ORM operator support
- 📄 **Built-in Pagination** — Limit/offset with smart cursor support
- 🔐 **Layered Security** — Row, field, filter, and operation-level access control
- ⚡ **N+1 Prevention** — Automatic `select_related` optimization
- 🎯 **Django Native** — Uses Django's built-in auth (groups, permissions)

## Installation

```bash
pip install django-flex
```

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

## Quick Start

Using Django's official documentation models: Blog, Author, Entry.

### 1. Create a View

```python
# views.py
from datetime import date
from django.db.models import Q
from django_flex import FlexQueryView
from .models import Entry

class EntryQueryView(FlexQueryView):
    model = Entry

    flex_permissions = {
        'staff': {
            'rows': lambda user: Q(),  # All entries
            'fields': ['*', 'blog.*'],
            'filters': ['id', 'rating.gte', 'blog.id', 'headline.icontains'],
            'order_by': ['-pub_date', 'rating'],
            'ops': ['get', 'query'],
        },
        'authenticated': {
            'rows': lambda user: Q(pub_date__lte=date.today()),
            'fields': ['id', 'headline', 'pub_date', 'blog.name'],
            'filters': ['id', 'headline.icontains'],
            'order_by': ['-pub_date'],
            'ops': ['get', 'query'],
        },
    }
```

### 2. Add URL Route

```python
# urls.py
from .views import EntryQueryView

urlpatterns = [
    path('api/entries/', EntryQueryView.as_view()),
]
```

### 3. Query from Frontend

```javascript
// Query entries
const response = await fetch('/api/entries/', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({
        fields: 'id, headline, pub_date, blog.name',
        filters: { 'rating.gte': 4 },
        order_by: '-pub_date',
        limit: 20
    })
});

// Response
{
    "success": true,
    "code": "FLEX_OK_QUERY",
    "pagination": {"offset": 0, "limit": 20, "has_more": false},
    "results": {
        "1": {"id": 1, "headline": "Django 5.0 Released", "pub_date": "2024-01-15", "blog": {"name": "Django News"}},
        "2": {"id": 2, "headline": "Getting Started Guide", "pub_date": "2024-01-14", "blog": {"name": "Tutorials"}}
    }
}
```

## Query Language

### Field Selection

```javascript
fields: 'id, headline'              // Specific fields
fields: '*'                          // All model fields
fields: '*, blog.*'                  // All fields + related
fields: 'id, blog.name, blog.tagline'  // Nested fields
```

### Filtering

```javascript
// Simple equality
filters: { rating: 5 }

// Operators
filters: { 'rating.gte': 4 }           // Greater than or equal
filters: { 'rating.in': [4, 5] }       // In list
filters: { 'headline.icontains': 'django' }  // Case-insensitive search
filters: { 'pub_date.gte': '2024-01-01' }    // Date comparison

// Composition
filters: { or: { rating: 5, 'number_of_comments.gte': 100 } }
filters: { not: { 'rating.lt': 3 } }
```

### Pagination

```javascript
limit: 20       // Max results
offset: 40      // Skip first 40

// Response includes next cursor
pagination: {
    offset: 40,
    limit: 20,
    has_more: true,
    next: { fields: '...', limit: 20, offset: 60 }
}
```

## Security Configuration

Django-Flex uses Django's built-in auth for role resolution:

1. `superuser` → bypasses all checks
2. `staff` → `user.is_staff`
3. `<group_name>` → first Django group
4. `authenticated` → logged in, no group

```python
# settings.py
DJANGO_FLEX = {
    'DEFAULT_LIMIT': 50,
    'MAX_LIMIT': 200,
    'MAX_RELATION_DEPTH': 2,

    'PERMISSIONS': {
        'entry': {
            'exclude': ['internal_notes'],

            'staff': {
                'rows': lambda user: Q(),
                'fields': ['*', 'blog.*'],
                'filters': ['id', 'rating.gte', 'pub_date.gte', 'blog.id'],
                'order_by': ['-pub_date', '-rating'],
                'ops': ['get', 'query', 'create', 'update', 'delete'],
            },
            'authenticated': {
                'rows': lambda user: Q(pub_date__lte=date.today()),
                'fields': ['id', 'headline', 'pub_date', 'rating'],
                'filters': ['id'],
                'order_by': ['-pub_date'],
                'ops': ['get', 'query'],
            },
        },
    },
}
```

## Usage Patterns

### Class-Based View

```python
from django_flex import FlexQueryView

class EntryQueryView(FlexQueryView):
    model = Entry
    flex_permissions = {...}
```

### Decorator

```python
from django_flex import flex_query

@flex_query(
    model=Entry,
    allowed_fields=['id', 'headline', 'blog.name'],
    allowed_filters=['id', 'headline.icontains'],
    allowed_actions=['get', 'query'],
)
def entry_query(request, result, query_spec):
    return JsonResponse(result.to_dict())
```

### Programmatic

```python
from django_flex import FlexQuery

result = FlexQuery(Entry).execute({
    'fields': 'id, headline, blog.name',
    'filters': {'rating.gte': 4},
}, user=request.user)
```

## Supported Operators

| Category | Operators |
|----------|-----------|
| Comparison | `lt`, `lte`, `gt`, `gte`, `exact`, `in`, `isnull`, `range` |
| Text | `contains`, `icontains`, `startswith`, `endswith`, `regex` |
| Date/Time | `date`, `year`, `month`, `day`, `hour`, `minute`, `second` |
| Composition | `and`, `or`, `not` |

## Response Codes

| Code | Description |
|------|-------------|
| `FLEX_OK` | Single object retrieved |
| `FLEX_OK_QUERY` | Query results returned |
| `FLEX_LIMIT_CLAMPED` | Results returned, limit was reduced |
| `FLEX_NOT_FOUND` | Object not found |
| `FLEX_PERMISSION_DENIED` | Access denied |
| `FLEX_INVALID_FILTER` | Invalid filter syntax |

## Documentation

- [Installation Guide](https://github.com/n3h3m/django-flex/blob/main/docs/installation.md)
- [Quick Start](https://github.com/n3h3m/django-flex/blob/main/docs/quickstart.md)
- [Permissions Guide](https://github.com/n3h3m/django-flex/blob/main/docs/permissions.md)
- [API Reference](https://github.com/n3h3m/django-flex/blob/main/docs/api_reference.md)

## License

MIT License - see [LICENSE](https://github.com/n3h3m/django-flex/blob/main/LICENSE) for details.
