Metadata-Version: 2.4
Name: django-featureflow
Version: 0.1.0
Summary: Realtime distributed feature flag engine for Django
Author-email: Oladapo Kolawole <dapkolly@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/kollydap/featureflow
Keywords: django,feature flags,feature toggles,redis,realtime
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Classifier: Intended Audience :: Developers
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=5.2
Requires-Dist: djangorestframework>=3.15
Requires-Dist: redis>=5.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-django>=4.8; extra == "dev"
Requires-Dist: psycopg2-binary>=2.9; extra == "dev"
Dynamic: license-file

# FeatureFlow

A realtime, distributed feature flag engine for Django. Flags are evaluated in-process from a local in-memory cache, kept consistent across all workers via Redis pub/sub. Zero network hops on the hot path.

---

## Requirements

- Python 3.11+
- Django 5.2+
- PostgreSQL 14+ (uses `ArrayField`)
- Redis 6+

---

## Installation

```bash
pip install -r requirements.txt
```

Add to `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    ...
    "rest_framework",
    "rest_framework.authtoken",
    "featureflow.apps.FeatureFlowConfig",
]
```

Mount the API in your root URLconf:

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

urlpatterns = [
    path("api/", include("featureflow.urls")),
]
```

---

## Configuration

Add a `FEATUREFLOW` block to your Django settings:

```python
FEATUREFLOW = {
    "REDIS_URL": "redis://localhost:6379",   # Redis connection string
    "ENV": "production",                     # Default environment label
    "CACHE_TTL": 300,                        # Seconds before a full cache refresh
    "DEFAULT_VALUE": False,                  # Returned when a flag key is missing
}
```

---

## Database setup

```bash
# Create the PostgreSQL database
createdb featureflow

# Run migrations
python manage.py migrate
```

---

## Usage

### Python SDK

```python
from featureflow import feature_enabled, enable, disable

# Basic check (no user context)
if feature_enabled("dark-mode"):
    ...

# Per-user evaluation with conditions + rollout
if feature_enabled("new-checkout", user=request.user):
    ...

# Pass extra context attributes not on the user object
if feature_enabled("beta-search", user=request.user, context={"country": "NG"}):
    ...

# Programmatically flip flags
enable("dark-mode")    # sets enabled=True, rollout_percentage=100
disable("dark-mode")   # sets enabled=False
```

### Evaluation order

1. Fetch flag from local in-memory cache (never DB directly).
2. Flag missing or `enabled=False` → return `DEFAULT_VALUE`.
3. `rollout_percentage == 100` → return `True`.
4. `user.id` in `targeted_user_ids` → return `True`.
5. User attributes don't satisfy `conditions` → return `False`.
6. HMAC-based deterministic bucket: `hmac(flag_key, user_id) % 100 < rollout_percentage`.

---

## REST API

All endpoints require authentication (Token or Session).

| Method | URL | Description |
|--------|-----|-------------|
| `POST` | `/api/flags/` | Create a flag |
| `GET` | `/api/flags/` | List all flags (`?environment=production`) |
| `GET` | `/api/flags/<key>/` | Retrieve a flag |
| `PATCH` | `/api/flags/<key>/` | Partial update |
| `DELETE` | `/api/flags/<key>/` | Delete |
| `POST` | `/api/flags/<key>/enable/` | Enable + set 100% rollout |
| `POST` | `/api/flags/<key>/disable/` | Disable |
| `GET` | `/api/flags/<key>/audit/` | Audit log for this flag |

### Create a flag

```bash
curl -X POST http://localhost:8000/api/flags/ \
  -H "Authorization: Token <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "dark-mode",
    "name": "Dark Mode",
    "enabled": true,
    "rollout_percentage": 50,
    "conditions": {"subscription": "premium"},
    "targeted_user_ids": [1, 2, 3],
    "environment": "production"
  }'
```

### Obtain an auth token

```bash
python manage.py drf_create_token <username>
```

---

## Flag model fields

| Field | Type | Description |
|-------|------|-------------|
| `key` | slug | Unique identifier used in code |
| `name` | string | Human-readable label |
| `description` | string | Optional notes |
| `enabled` | bool | Master switch |
| `rollout_percentage` | 0–100 | % of users who see the feature |
| `conditions` | JSON | User attribute filters (all must match) |
| `targeted_user_ids` | int[] | Users who always get the feature |
| `environment` | enum | `development` / `staging` / `production` |

---

## How caching and pub/sub work

1. On startup (`AppConfig.ready`), all flags are loaded from PostgreSQL into a thread-safe in-memory dict.
2. A background daemon thread subscribes to the Redis channel `featureflow:flags`.
3. Every time a flag is saved or deleted, a `post_save` / `post_delete` signal publishes the updated payload to that channel.
4. All workers receive the message and update their local caches atomically via `threading.RLock`.
5. If Redis is unavailable, each worker continues serving the last known cached state with no interruption.

---

## Running tests

```bash
# Create the test database
createdb featureflow_test

# Run the full suite
pytest
```

Tests mock Redis pub/sub interactions and require a live PostgreSQL connection for API and model tests.

---

## Django Admin

Visit `/admin/` to manage flags and view immutable audit logs. The audit log admin is read-only.
