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 permissionsTokenPayload: Raw token data from JWTUserProfile: User profile from database
Context Management
get_current_company(): Get current tenant UUIDset_current_company(uuid): Set current tenantclear_current_company(): Clear tenant contextcompany_context(uuid): Context manager for scoped access
Permission Classes
HasProfile: Require authenticated NovahUserHasPermission(permission): Require specific permissionHasAnyPermission(permissions): Require any of listed permissionsIsSameCompany: 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 configurationget_provider(): Get configured AuthProviderget_profile_loader(): Get configured ProfileLoaderreset_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:
- Update the version in
pyproject.toml - Create a GitHub release with a new tag (e.g.,
v0.2.0) - 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c278676e518bf05f694f5781df3d66a3c3646932a381e561f0660645a239b8c6
|
|
| MD5 |
82b65e09b7ae4617254bf24604f8e39b
|
|
| BLAKE2b-256 |
727112cf92c49f0623358b3f12e4e6de2fb3eab0871df25d57c2a036a801d15b
|
Provenance
The following attestation bundles were made for novah_patronus-0.2.0.tar.gz:
Publisher:
publish.yml on Novah-Care/patronus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
novah_patronus-0.2.0.tar.gz -
Subject digest:
c278676e518bf05f694f5781df3d66a3c3646932a381e561f0660645a239b8c6 - Sigstore transparency entry: 833798054
- Sigstore integration time:
-
Permalink:
Novah-Care/patronus@7039a5c5960770062eae84e73030bb030c6cf1a5 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Novah-Care
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7039a5c5960770062eae84e73030bb030c6cf1a5 -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
24d7a3068d4227267e755da2dd76f8c70e15fe9bb5d05b86dae50f77d5047bea
|
|
| MD5 |
761559b33274b270ec6088fdab954c64
|
|
| BLAKE2b-256 |
ae197eb6df577e7d4d0e6c2a2870b752f5363a998f60e7602d7793782210274b
|
Provenance
The following attestation bundles were made for novah_patronus-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on Novah-Care/patronus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
novah_patronus-0.2.0-py3-none-any.whl -
Subject digest:
24d7a3068d4227267e755da2dd76f8c70e15fe9bb5d05b86dae50f77d5047bea - Sigstore transparency entry: 833798055
- Sigstore integration time:
-
Permalink:
Novah-Care/patronus@7039a5c5960770062eae84e73030bb030c6cf1a5 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Novah-Care
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7039a5c5960770062eae84e73030bb030c6cf1a5 -
Trigger Event:
release
-
Statement type: