Metadata-Version: 2.5
Name: django-featurevault
Version: 0.1.0
Summary: Feature Flags Framework for Django.
Project-URL: Homepage, https://github.com/prafulgulani/django-featurevault
Project-URL: Repository, https://github.com/prafulgulani/django-featurevault
Project-URL: Issues, https://github.com/prafulgulani/django-featurevault/issues
Author-email: Praful Gulani <prafulgulani555@gmail.com>, Andrew Miller <andrew@softwarecrafts.co.uk>
License: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
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
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: django>=4.2
Description-Content-Type: text/markdown

# Django FeatureVault

[![PyPI](https://img.shields.io/pypi/v/django-featurevault.svg)](https://pypi.org/project/django-featurevault/)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-featurevault.svg)
![PyPI - License](https://img.shields.io/pypi/l/django-featurevault.svg)

Feature Flag Framework for Django.

## Installation

```bash
python -m pip install django-featurevault

```

First, add `django_featurevault` to your `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    # ...
    "django_featurevault",
]

```

Next, add `FeatureContextMiddleware` to your `MIDDLEWARE` list:

```python
MIDDLEWARE = [
    # ...
    "django_featurevault.middleware.FeatureContextMiddleware",
]

```

Finally, configure your feature flags in `settings.py`:

```python
FEATURE_FLAGS = {
    "default": {
        "BACKEND": "django_featurevault.backends.settings.SettingsBackend",
        "OPTIONS": {
            "FLAGS": {
                "GLOBAL_BANNER": True,
                "STAFF_DASHBOARD": {
                    "enabled": True,
                    "conditions": {
                        "groups": [
                            {
                                "properties": [
                                    {"key": "is_staff", "operator": "exact", "value": True}
                                ]
                            }
                        ]
                    },
                },
                "NEW_CHECKOUT": {
                    "enabled": True,
                    "conditions": {
                        "groups": [
                            {"rollout_percentage": 50}
                        ]
                    },
                },
            }
        },
    }
}

```

## Backends

Few backends are included by default:

* `django_featurevault.backends.settings.SettingsBackend`: Reads feature flags directly from `settings.FEATURE_FLAGS`.
* `django_featurevault.backends.dummy.DummyBackend`: In-memory backend for testing.

### Custom Backends

You can create custom storage backends by subclassing `BaseFeatureBackend` and implementing `get_feature` and `get_all_features`:

```python
from typing import Any
from django_featurevault.backends.base import BaseFeatureBackend


class CustomRedisBackend(BaseFeatureBackend):
    def __init__(self, alias: str = "default", **options: Any) -> None:
        super().__init__(alias=alias, **options)
        # Initialize client connections using options passed from settings
        self.redis_url = options.get("URL", "redis://localhost:6379/0")

    def get_feature(self, feature_name: str, default: Any = False) -> dict[str, Any]:
        """
        Fetch feature configuration dictionary by name.
        Must return a dict containing at minimum:
        {"enabled": bool, "conditions": dict}
        """
        # Fetch from your storage engine...
        return {
            "enabled": True,
            "conditions": {},
        }

    def get_all_features(self) -> dict[str, dict[str, Any]]:
        """Fetch all feature definitions for bulk evaluation."""
        return {}

```

Point to your custom backend in `settings.py`:

```python
FEATURE_FLAGS = {
    "default": {
        "BACKEND": "my_app.backends.CustomRedisBackend",
        "OPTIONS": {
            "URL": "redis://127.0.0.1:6379/1",
        },
    }
}

```

## Usage

### Checking flags in views

Import `feature` and call `is_enabled`:

```python
from django.shortcuts import render
from django_featurevault import feature


def home_view(request):
    if feature.is_enabled("NEW_CHECKOUT"):
        return render(request, "new_checkout.html")
    return render(request, "old_checkout.html")

```

If a flag is not defined, `is_enabled` returns `False` by default. You can change this using the `default` parameter:

```python
feature.is_enabled("UNKNOWN_FLAG", default=True)

```

### Passing explicit context

You can pass a custom context dictionary directly into `is_enabled`:

```python
feature.is_enabled("BETA_FEATURE", context={"plan": "enterprise", "country": "IN"})

```

### Background tasks and testing context

To evaluate feature flags in Celery workers, cron jobs, or tests where no HTTP request exists, use the `context` manager:

```python
from django_featurevault import feature

with feature.context(user_id="user_101", is_staff=True):
    if feature.is_enabled("STAFF_DASHBOARD"):
        ...

```

## Conditions and Targeting

Flags defined as dictionaries support targeting rules via `conditions`.

```python
"FEATURE_NAME": {
    "enabled": True,
    "conditions": {
        "groups": [
            # Group 1: Enabled for internal staff
            {
                "properties": [
                    {"key": "is_staff", "operator": "exact", "value": True}
                ],
            }
            # OR Group 2: Enabled for 20% of beta users in India
            {
                "properties": [
                    {"key": "country", "operator": "exact", "value": "IN"},
                    {"key": "plan", "operator": "exact", "value": "beta"}
                ],
                "rollout_percentage": 20,
            }
        ]
    },
}

```

* **`groups`**: Evaluated with OR logic (if any group matches, the flag is enabled).
* **`properties` within a group**: Evaluated with AND logic (all properties in the group must match).
* **`rollout_percentage`**: A percentage between 0 and 100 that uses sticky hashing against the user or device ID.

### Supported operators

* `exact`: Matches exact equality (`==`).
* `is_not`: Matches inequality (`!=`).
* `in`: Checks values in a list (`value in [...]`).
* `icontains`: Case-insensitive substring match.

### Automatically resolved user fields

When evaluating against an authenticated Django user, the following context keys are resolved automatically:

* `user_id`, `id`, `pk`: The user's primary key (`user.pk`).
* `username`: The user's username (`getattr(user, user.USERNAME_FIELD)`).
* `django_group`: Group names the user belongs to (`user.groups.values_list("name", flat=True)`).
* Any standard or custom attribute on the user model (like `is_staff`, `is_superuser`, `email`).

## Cookie Configuration

`FeatureContextMiddleware` automatically sets an anonymous device cookie for sticky rollouts when users are not logged in.

You can customize the cookie name and options in `settings.py`:

```python
FEATURE_FLAGS = {
    "CLIENT_ID_COOKIE": "ff_client_id",
    "COOKIE_OPTIONS": {
        "max_age": 30 * 24 * 60 * 60,
        "httponly": True,
        "samesite": "Lax",
    },
}

```

## API Endpoint

To expose evaluated feature flags to frontend clients as JSON, include the URLs in your `urls.py`:

```python
from django.urls import include, path

urlpatterns = [
    # ...
    path("features/", include("django_featurevault.urls")),
]

```

This registers `GET /features/api/flags/`, and returns a JSON map of all evaluated flags:

```json
{
  "GLOBAL_BANNER": {"enabled": true},
  "STAFF_DASHBOARD": {"enabled": false},
  "NEW_CHECKOUT": {"enabled": true}
}

```

## Example Project

An example project is included in the `example/` directory.
