Metadata-Version: 2.4
Name: django-flex
Version: 26.1.3
Summary: A flexible query language for Django - enable frontends to dynamically construct database queries
Author: Django-Flex Contributors
Maintainer: Django-Flex Contributors
License: MIT
Project-URL: Homepage, https://github.com/your-org/django-flex
Project-URL: Documentation, https://github.com/your-org/django-flex#readme
Project-URL: Repository, https://github.com/your-org/django-flex.git
Project-URL: Issues, https://github.com/your-org/django-flex/issues
Project-URL: Changelog, https://github.com/your-org/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

<p align="center">
    <em>A flexible query language for Django — let your frontend dynamically construct database queries</em>
</p>

<p align="center">
    <a href="https://pypi.org/project/django-flex/">
        <img src="https://img.shields.io/pypi/v/django-flex.svg" alt="PyPI version">
    </a>
    <a href="https://pypi.org/project/django-flex/">
        <img src="https://img.shields.io/pypi/pyversions/django-flex.svg" alt="Python versions">
    </a>
    <a href="https://github.com/your-org/django-flex/blob/main/LICENSE">
        <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
    </a>
</p>

---

**Django-Flex** enables frontends to send flexible, dynamic queries to your Django backend — think of it as a simpler alternative to GraphQL that feels native to Django.

## Features

- 🎯 **Field Selection** — Request only the fields you need, including nested relations
- 🔍 **Dynamic Filtering** — Full Django ORM operator support with composable AND/OR/NOT
- 📄 **Smart Pagination** — Limit/offset with cursor-based continuation
- 🔒 **Built-in Security** — Row-level, field-level, and operation-level permissions
- ⚡ **Automatic Optimization** — N+1 prevention with smart `select_related`
- 🐍 **Django-Native** — Feels like a natural extension of Django

## Installation

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

Add to your Django settings:

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

# Optional: Configure permissions and defaults
DJANGO_FLEX = {
    'DEFAULT_LIMIT': 50,
    'MAX_LIMIT': 200,
    'PERMISSIONS': {
        # See Permission Configuration below
    },
}
```

## Quick Start

### 1. Class-Based View (Recommended)

```python
# views.py
from django_flex import FlexQueryView
from myapp.models import Booking

class BookingQueryView(FlexQueryView):
    model = Booking
    
    # Define permissions for this view
    flex_permissions = {
        'authenticated': {
            'rows': lambda user: Q(team__members=user),
            'fields': ['id', 'status', 'customer.name', 'customer.email'],
            'filters': ['status', 'status.in', 'customer.name.icontains'],
            'order_by': ['created_at', '-created_at'],
            'ops': ['get', 'query'],
        },
    }
```

```python
# urls.py
from django.urls import path
from myapp.views import BookingQueryView

urlpatterns = [
    path('api/bookings/', BookingQueryView.as_view()),
]
```

### 2. Make Queries from Frontend

```javascript
// List bookings with field selection and filtering
const response = await fetch('/api/bookings/', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({
        fields: 'id, status, customer.name, customer.email',
        filters: {
            'status.in': ['confirmed', 'completed'],
            'customer.name.icontains': 'khan'
        },
        order_by: '-created_at',
        limit: 20
    })
});

const data = await response.json();
// {
//     "success": true,
//     "code": "FLEX_OK_QUERY",
//     "pagination": {"offset": 0, "limit": 20, "has_more": true},
//     "results": {
//         "1": {"id": 1, "status": "confirmed", "customer": {"name": "Aisha Khan", "email": "aisha@example.com"}},
//         "2": {"id": 2, "status": "completed", "customer": {"name": "Omar Khan", "email": "omar@example.com"}}
//     }
// }
```

```javascript
// Get single object by ID
const booking = await fetch('/api/bookings/', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({
        id: 1,
        fields: 'id, status, customer.*, address.*'
    })
});
```

## Query Language Reference

### Field Selection

```javascript
// All fields on the model
{ fields: '*' }

// Specific fields
{ fields: 'id, name, email' }

// Nested relation fields (dot notation)
{ fields: 'id, customer.name, customer.email' }

// Relation wildcards
{ fields: 'id, status, customer.*, address.*' }
```

### Filtering

```javascript
// Simple equality
{ filters: { status: 'confirmed' } }

// With operators
{ filters: { 'price.gte': 100, 'price.lte': 500 } }

// Text search
{ filters: { 'name.icontains': 'khan' } }

// List membership
{ filters: { 'status.in': ['pending', 'confirmed', 'completed'] } }

// OR conditions
{ filters: { or: { status: 'pending', 'customer.vip': true } } }

// NOT conditions  
{ filters: { not: { status: 'cancelled' } } }

// Complex composition
{
    filters: {
        'created_at.gte': '2024-01-01',
        or: [
            { status: 'confirmed' },
            { and: { status: 'pending', 'urgent': true } }
        ]
    }
}
```

**Supported Operators:**

| Category | Operators |
|----------|-----------|
| Comparison | `lt`, `lte`, `gt`, `gte`, `exact`, `iexact`, `in`, `isnull`, `range` |
| Text | `contains`, `icontains`, `startswith`, `istartswith`, `endswith`, `iendswith`, `regex`, `iregex` |
| Date/Time | `date`, `year`, `month`, `day`, `week_day`, `hour`, `minute`, `second` |

### Pagination

```javascript
{
    limit: 20,      // Number of results (default: 50, max: 200)
    offset: 0,      // Starting position
    order_by: '-created_at'  // Sort order (prefix with - for descending)
}
```

Response includes pagination info:

```javascript
{
    "pagination": {
        "offset": 0,
        "limit": 20,
        "has_more": true,
        "next": {
            "fields": "...",
            "filters": {...},
            "limit": 20,
            "offset": 20
        }
    }
}
```

## Permission Configuration

Django-Flex uses a **deny-by-default** security model. You must explicitly grant access.

```python
# settings.py
DJANGO_FLEX = {
    'PERMISSIONS': {
        'booking': {
            # Fields excluded from wildcard expansion (security)
            'exclude': ['internal_notes', 'stripe_payment_id'],
            
            # Role-based permissions
            'owner': {
                # Row-level: which rows can this role see?
                'rows': lambda user: Q(created_by=user),
                
                # Field-level: which fields can they access?
                'fields': ['*', 'customer.*', 'address.*'],
                
                # Filter-level: which fields can they filter on?
                'filters': [
                    'id', 'status', 'status.in',
                    'customer.name', 'customer.name.icontains',
                    'created_at.gte', 'created_at.lte',
                ],
                
                # Order-level: which fields can they sort by?
                'order_by': ['id', '-id', 'created_at', '-created_at', 'customer.name'],
                
                # Operation-level: which actions can they perform?
                'ops': ['get', 'query', 'create', 'update', 'delete'],
            },
            
            'staff': {
                'rows': lambda user: Q(team__members=user),
                'fields': ['id', 'status', 'customer.name', 'address.city'],
                'filters': ['status', 'status.in'],
                'order_by': ['created_at', '-created_at'],
                'ops': ['get', 'query'],
            },
            
            # Roles not listed have NO ACCESS
        },
    },
}
```

### Custom Role Resolution

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

```python
from django_flex import FlexPermission

class MyPermission(FlexPermission):
    def get_user_role(self, user):
        if user.is_superuser:
            return 'superuser'
        if user.groups.filter(name='Managers').exists():
            return 'manager'
        return 'staff'
```

## Usage Patterns

### 1. Class-Based View (Recommended)

```python
from django_flex import FlexQueryView

class BookingQueryView(FlexQueryView):
    model = Booking
    require_auth = True
    allowed_actions = ['get', 'query']
    flex_permissions = {...}
```

### 2. Function Decorator

```python
from django_flex import flex_query
from django.http import JsonResponse

@flex_query(
    model=Booking,
    allowed_fields=['id', 'status', 'customer.name'],
    allowed_filters=['status', 'status.in'],
)
def booking_list(request, result, query_spec):
    return JsonResponse(result.to_dict())
```

### 3. Programmatic Usage

```python
from django_flex import FlexQuery

def my_view(request):
    result = FlexQuery(Booking).execute({
        'fields': 'id, customer.name',
        'filters': {'status': 'confirmed'},
        'limit': 20,
    }, user=request.user)
    
    return JsonResponse(result.to_dict())
```

### 4. Middleware (Single Endpoint)

```python
# settings.py
MIDDLEWARE = [
    ...
    'django_flex.middleware.FlexQueryMiddleware',
]

DJANGO_FLEX = {
    'MIDDLEWARE_PATH': '/api/flex/',
    ...
}
```

Then query any configured model:

```javascript
fetch('/api/flex/', {
    method: 'POST',
    body: JSON.stringify({
        _model: 'booking',
        _action: 'query',
        fields: 'id, status',
        limit: 20
    })
});
```

## Configuration Reference

```python
DJANGO_FLEX = {
    # Pagination
    'DEFAULT_LIMIT': 50,        # Default page size
    'MAX_LIMIT': 200,           # Maximum page size (hard cap)
    
    # Security
    'MAX_RELATION_DEPTH': 2,    # Max depth for nested fields/filters
    'REQUIRE_AUTHENTICATION': True,  # Require auth by default
    'AUDIT_QUERIES': False,     # Log all queries (for debugging)
    
    # Middleware
    'MIDDLEWARE_PATH': '/api/flex/',  # Path for middleware endpoint
    
    # Model permissions
    'PERMISSIONS': {...},
    
    # Response codes (customizable)
    'RESPONSE_CODES': {
        'OK': 'FLEX_OK',
        'OK_LIST': 'FLEX_OK_QUERY',
        'LIMIT_CLAMPED': 'FLEX_LIMIT_CLAMPED',
        'NOT_FOUND': 'FLEX_NOT_FOUND',
        'MODEL_NOT_FOUND': 'FLEX_MODEL_NOT_FOUND',
        'PERMISSION_DENIED': 'FLEX_PERMISSION_DENIED',
        'INVALID_FIELD': 'FLEX_INVALID_FIELD',
        'INVALID_FILTER': 'FLEX_INVALID_FILTER',
    },
}
```

## Response Format

### Successful Single Object (get)

```json
{
    "success": true,
    "code": "FLEX_OK",
    "id": 1,
    "status": "confirmed",
    "customer": {
        "name": "Aisha Khan",
        "email": "aisha@example.com"
    }
}
```

### Successful Query (query)

```json
{
    "success": true,
    "code": "FLEX_OK_QUERY",
    "pagination": {
        "offset": 0,
        "limit": 20,
        "has_more": true,
        "next": {...}
    },
    "results": {
        "1": {...},
        "2": {...}
    }
}
```

### Error Response

```json
{
    "success": false,
    "code": "FLEX_PERMISSION_DENIED",
    "error": "Access denied: field 'secret_field' not accessible"
}
```

## Why Django-Flex?

| Feature | Django-Flex | GraphQL | REST |
|---------|-------------|---------|------|
| Learning curve | Low (Django-native) | High | Low |
| Field selection | ✅ | ✅ | ❌ (fixed endpoints) |
| Dynamic filtering | ✅ | ✅ | Limited |
| Built-in security | ✅ | Manual | Manual |
| Django integration | Native | Requires graphene | Native |
| Schema definition | Optional | Required | N/A |
| N+1 prevention | Automatic | Manual | Manual |

## Contributing

Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

## License

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