Advanced Customization¶
FastAPI Guard uses a protocol-based architecture that makes it highly extensible. This guide explains how the protocol system works and how to create custom implementations.
Protocol-Based Architecture¶
FastAPI Guard uses Python's Protocol system to define interfaces that components must implement. This allows you to:
- Replace built-in handlers with custom implementations
- Extend functionality without modifying the core library
- Better separate concerns in your codebase
Why Protocols?¶
The protocol system solves several important problems:
- Avoiding dependency on third-party services: You can replace the built-in IPInfo geo-location with your own service
- Preventing circular imports: The protocols help break dependency cycles in the codebase
- Enabling extension points: Clear interfaces for adding custom functionality
Available Protocols¶
GeoIPHandler Protocol¶
The GeoIPHandler
protocol defines the interface for any geographical IP handler:
@runtime_checkable
class GeoIPHandler(Protocol):
"""Protocol for geographical IP handler."""
@property
def is_initialized(self) -> bool: ...
async def initialize(self) -> None: ...
async def initialize_redis(self, redis_handler: RedisHandlerProtocol) -> None: ...
def get_country(self, ip: str) -> str | None: ...
Method Details¶
is_initialized
: Should return whether the handler is ready to useinitialize()
: Should set up the handler (load databases, connect to APIs, etc.)initialize_redis()
: Should store the redis handler for optional cachingget_country()
: Should return the ISO 3166-1 alpha-2 country code for the IP
RedisHandlerProtocol¶
IMPORTANT: Users do NOT need to implement this protocol. It exists purely for internal use to break dependency cycles and define what the Redis handler must support for the custom GeoIP handlers.
@runtime_checkable
class RedisHandlerProtocol(Protocol):
"""Protocol for Redis handlers."""
async def get_key(self, namespace: str, key: str) -> Any: ...
async def set_key(
self, namespace: str, key: str, value: Any, ttl: int | None = None
) -> bool | None: ...
def get_connection(self) -> AsyncContextManager[Redis]: ...
async def initialize(self) -> None: ...
How Protocols Are Used¶
The FastAPI Guard initialization flow works like this:
- You create a
SecurityConfig
with your customgeo_ip_handler
- You add the
SecurityMiddleware
with this config - When the middleware initializes, it:
- Checks if you provided a
geo_ip_handler
that implements the protocol - If Redis is enabled, it passes its internal
RedisManager
to your handler - Your handler can use this Redis connection for caching or whatever you need
This makes your custom geo IP handler fully integrated with the middleware's Redis infrastructure.
Implementation Examples¶
Example: Custom Geo IP Service¶
Here's a complete example of a custom GeoIPHandler implementation that uses a different service:
from guard.protocols.geo_ip_protocol import GeoIPHandler
from guard.protocols.redis_protocol import RedisHandlerProtocol
class CustomGeoIPHandler:
"""Custom handler using Custom GeoIP database"""
def __init__(self, license_key: str, db_path: str = "CustomGeoIP.mmdb"):
self._initialized = False
self.license_key = license_key
self.db_path = db_path
self.reader = None
self.redis = None # Will store the FastAPI Guard's Redis handler
@property
def is_initialized(self) -> bool:
return self.reader is not None
async def initialize(self) -> None:
"""Initialize by downloading or loading the Custom GeoIP database"""
import os
import somelibrary
# Check if we have a cached copy in Redis
if self.redis:
cached_db = await self.redis.get_key("custom", "database")
if cached_db:
with open(self.db_path, "wb") as f:
f.write(cached_db if isinstance(cached_db, bytes)
else cached_db.encode("latin-1"))
self.reader = somelibrary.Reader(self.db_path)
self._initialized = True
return
# Download if needed (simplified - in a real app, use your API)
if not os.path.exists(self.db_path):
# Custom code to download database using license_key
pass
# Open the database
if os.path.exists(self.db_path):
self.reader = somelibrary.Reader(self.db_path)
self._initialized = True
async def initialize_redis(self, redis_handler: RedisHandlerProtocol) -> None:
"""Store Redis handler and initialize"""
self.redis = redis_handler # Store the Redis handler provided by FastAPI Guard
await self.initialize()
def get_country(self, ip: str) -> str | None:
"""Get country code from IP using Custom GeoIP database"""
if not self.reader:
raise RuntimeError("Database not initialized")
try:
response = self.reader.country(ip)
return response.country.iso_code
except Exception:
return None
Usage in Application¶
from fastapi import FastAPI
from guard.middleware import SecurityMiddleware
from guard.models import SecurityConfig
from your_custom_module import CustomGeoIPHandler
app = FastAPI()
# Use custom handler instead of default IPInfoManager
config = SecurityConfig(
geo_ip_handler=CustomGeoIPHandler(args),
blocked_countries=["CN", "RU"],
# Other configuration...
)
app.add_middleware(SecurityMiddleware, config=config)
Technical Details: How Redis Integration Works¶
When you create a custom GeoIPHandler:
- FastAPI Guard's middleware will call your handler's
initialize_redis()
method - It passes its internal
RedisManager
to your handler - Your handler can store this manager and use it for caching
You don't need to: - Create your own Redis connection - Implement RedisHandlerProtocol - Manage Redis connection pools
The built-in RedisManager handles all of this for you.
When a GeoIPHandler implementation receives a Redis handler in initialize_redis()
, it can use it to:
- Cache lookup results to improve performance
- Store database files across application restarts
- Share state across multiple application instances
The Redis handler provides these key methods:
# Store a value with optional TTL
await redis_handler.set_key("namespace", "key", "value", ttl=3600)
# Retrieve a value
value = await redis_handler.get_key("namespace", "key")
# Use the connection directly (advanced)
async with redis_handler.get_connection() as conn:
# Direct Redis operations
pass
Remember: You don't implement the RedisHandlerProtocol yourself - FastAPI Guard provides its built-in RedisManager which meets this protocol and is automatically passed to your custom GeoIPHandler.