Skip to main content

Production-ready, provider-agnostic authentication library for Django REST Framework applications in the Novah Care ecosystem

Project description

Patronus

Production-ready, provider-agnostic authentication library for Django REST Framework applications in the Novah Care ecosystem.

Features

  • Provider Agnostic: Abstract authentication provider interface with GCIP (Firebase Auth) implementation
  • DRF Compatible: Native Django REST Framework authentication and permission classes
  • Multi-tenant Support: Automatic tenant context injection via middleware
  • Type Safe: Full type annotations with mypy strict mode support
  • Async Ready: Async variants for all provider operations

Requirements

  • Python 3.13+
  • Django 6.0+
  • Django REST Framework 3.14+

Installation

From PyPI (Recommended)

# Install with pip
pip install novah-patronus

# Or with Poetry
poetry add novah-patronus

From GitHub

# Add specific version with Poetry
poetry add git+ssh://git@github.com/Novah-Care/patronus.git@v0.1.0

# Or with pip
pip install git+ssh://git@github.com/Novah-Care/patronus.git@v0.1.0

Or add to your pyproject.toml manually:

[tool.poetry.dependencies]
novah-patronus = { git = "https://github.com/Novah-Care/patronus.git", tag = "v0.1.0" }

From Source (Development)

# Clone the repository
git clone https://github.com/Novah-Care/patronus.git
cd patronus

# Install in editable mode with development dependencies
pip install -e ".[dev]"

Quick Start

1. Configure Django Settings

# settings.py
import os

PATRONUS = {
    # Provider configuration
    "PROVIDER_CLASS": "patronus.providers.gcip.GCIPProvider",

    # Credentials from environment variable (production)
    # JSON strings are auto-parsed, no need for json.loads()
    "PROVIDER_CREDENTIALS": os.environ.get("GCIP_CREDENTIALS"),

    # Or from file path (development)
    # "PROVIDER_CREDENTIALS": "/path/to/service-account.json",

    # Or use application default credentials
    # "PROVIDER_CREDENTIALS": None,

    # Profile loader (implement your own or use mock for testing)
    "PROFILE_LOADER_CLASS": "patronus.profile_loader.MockProfileLoader",
}

# Add Patronus authentication to DRF
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "patronus.PatronusAuthentication",
    ],
}

# Add tenant middleware
MIDDLEWARE = [
    # ... other middleware ...
    "patronus.TenantMiddleware",
]

2. Implement a Profile Loader

# your_app/auth.py
from patronus import ProfileLoader, UserProfile, NoProfileError

class YourProfileLoader(ProfileLoader):
    def load_profile(
        self,
        uid: str,
        email: str | None = None,
        phone_number: str | None = None,
    ) -> UserProfile:
        try:
            user = User.objects.get(identity_provider_uid=uid)
            permissions = user.get_all_permissions()
            return UserProfile(
                company_id=user.company_id,
                permissions=frozenset(permissions),
                profile_type=user.profile_type,
            )
        except User.DoesNotExist:
            raise NoProfileError(f"No profile found for uid: {uid}")

    async def load_profile_async(
        self,
        uid: str,
        email: str | None = None,
        phone_number: str | None = None,
    ) -> UserProfile:
        # Async implementation
        return self.load_profile(uid, email, phone_number)

3. Use Permission Classes

# your_app/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from patronus import HasPermission, HasProfile, IsSameCompany

class PatientListView(APIView):
    permission_classes = [HasPermission("read:patients")]

    def get(self, request):
        # request.user is a NovahUser instance
        return Response({"patients": []})

class PatientDetailView(APIView):
    permission_classes = [HasProfile, IsSameCompany]

    def get(self, request, pk):
        patient = get_object_or_404(Patient, pk=pk)
        self.check_object_permissions(request, patient)
        return Response({"patient": patient.data})

4. Use Tenant Context

from patronus import get_current_company, company_context

def some_service_function():
    company_id = get_current_company()
    if company_id:
        # Filter by tenant
        return Model.objects.filter(company_id=company_id)

# Or use context manager for scoped access
with company_context(some_company_id):
    do_tenant_scoped_work()

API Reference

Authentication

  • PatronusAuthentication: DRF authentication class

User Models

  • NovahUser: Authenticated user with permissions
  • TokenPayload: Raw token data from JWT
  • UserProfile: User profile from database

Context Management

  • get_current_company(): Get current tenant UUID
  • set_current_company(uuid): Set current tenant
  • clear_current_company(): Clear tenant context
  • company_context(uuid): Context manager for scoped access

Permission Classes

  • HasProfile: Require authenticated NovahUser
  • HasPermission(permission): Require specific permission
  • HasAnyPermission(permissions): Require any of listed permissions
  • IsSameCompany: Require same company as resource

Exceptions

  • InvalidTokenError: Token malformed (401)
  • ExpiredTokenError: Token expired (401)
  • RevokedTokenError: Token revoked (401)
  • NoProfileError: No user profile (403)
  • TenantMismatchError: Wrong tenant (403)
  • ProviderError: Provider unavailable (503)

Settings Functions

  • get_settings(): Get Patronus configuration
  • get_provider(): Get configured AuthProvider
  • get_profile_loader(): Get configured ProfileLoader
  • reset_instances(): Reset cached instances (for testing)

Development

Environment Setup

# Clone the repository
git clone https://github.com/Novah-Care/patronus.git
cd patronus

# Create virtual environment
python3.13 -m venv .venv

# Activate virtual environment
# On macOS/Linux:
source .venv/bin/activate

# Upgrade pip
pip install --upgrade pip

# Install package with dev dependencies
pip install -e ".[dev]"

Running Tests

# Run all tests
pytest

# Run tests with verbose output
pytest -v

# Run tests with coverage report
pytest --cov=patronus --cov-report=html

# Run specific test file
pytest tests/test_user.py

# Run tests matching a pattern
pytest -k "test_novah_user"

Code Quality

# Run type checking
mypy src/patronus

# Run linting
ruff check src/patronus tests

# Run linting with auto-fix
ruff check --fix src/patronus tests

# Format code
ruff format src/patronus tests

# Check formatting without changes
ruff format --check src/patronus tests

All-in-One Check (before committing)

# Run all checks
ruff check src/patronus tests && \
ruff format --check src/patronus tests && \
mypy src/patronus && \
pytest --cov=patronus

Project Structure

patronus/
├── src/patronus/
│   ├── __init__.py           # Public API exports
│   ├── authentication.py     # DRF authentication class
│   ├── permissions.py        # DRF permission classes
│   ├── middleware.py         # Tenant middleware
│   ├── context.py            # Tenant context (contextvars)
│   ├── user.py               # NovahUser, TokenPayload, UserProfile
│   ├── exceptions.py         # Exception hierarchy
│   ├── settings.py           # Configuration management
│   ├── profile_loader.py     # ProfileLoader interface
│   ├── decorators.py         # (Phase 2)
│   ├── cache.py              # (Phase 2)
│   └── providers/
│       ├── __init__.py
│       ├── base.py           # AuthProvider ABC
│       └── gcip.py           # GCIP/Firebase implementation
└── tests/
    ├── conftest.py           # Shared fixtures
    └── providers/

Publishing to PyPI

The package is published to PyPI using GitHub Actions with Trusted Publishing (no API tokens needed).

To publish a new version:

  1. Update the version in pyproject.toml
  2. Create a GitHub release with a new tag (e.g., v0.2.0)
  3. The workflow automatically builds and publishes to PyPI

For detailed setup and troubleshooting, see docs/PUBLISHING.md.

License

MIT License - see LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

novah_patronus-0.2.0.tar.gz (94.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

novah_patronus-0.2.0-py3-none-any.whl (21.9 kB view details)

Uploaded Python 3

File details

Details for the file novah_patronus-0.2.0.tar.gz.

File metadata

  • Download URL: novah_patronus-0.2.0.tar.gz
  • Upload date:
  • Size: 94.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for novah_patronus-0.2.0.tar.gz
Algorithm Hash digest
SHA256 c278676e518bf05f694f5781df3d66a3c3646932a381e561f0660645a239b8c6
MD5 82b65e09b7ae4617254bf24604f8e39b
BLAKE2b-256 727112cf92c49f0623358b3f12e4e6de2fb3eab0871df25d57c2a036a801d15b

See more details on using hashes here.

Provenance

The following attestation bundles were made for novah_patronus-0.2.0.tar.gz:

Publisher: publish.yml on Novah-Care/patronus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file novah_patronus-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: novah_patronus-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 21.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for novah_patronus-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 24d7a3068d4227267e755da2dd76f8c70e15fe9bb5d05b86dae50f77d5047bea
MD5 761559b33274b270ec6088fdab954c64
BLAKE2b-256 ae197eb6df577e7d4d0e6c2a2870b752f5363a998f60e7602d7793782210274b

See more details on using hashes here.

Provenance

The following attestation bundles were made for novah_patronus-0.2.0-py3-none-any.whl:

Publisher: publish.yml on Novah-Care/patronus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page