Refactor test utilities and enhance ASGI settings

- Cleaned up and standardized assertions in ApiTestMixin for API response validation.
- Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE.
- Removed unused imports and improved formatting in settings.py.
- Refactored URL patterns in urls.py for better readability and organization.
- Enhanced view functions in views.py for consistency and clarity.
- Added .flake8 configuration for linting and style enforcement.
- Introduced type stubs for django-environ to improve type checking with Pylance.
This commit is contained in:
pacnpal
2025-08-20 19:51:59 -04:00
parent 69c07d1381
commit 66ed4347a9
230 changed files with 15094 additions and 11578 deletions

View File

@@ -8,15 +8,15 @@ from .performance_middleware import (
PerformanceMiddleware,
QueryCountMiddleware,
DatabaseConnectionMiddleware,
CachePerformanceMiddleware
CachePerformanceMiddleware,
)
# Make all middleware classes available at the package level
__all__ = [
'PageViewMiddleware',
'PgHistoryContextMiddleware',
'PerformanceMiddleware',
'QueryCountMiddleware',
'DatabaseConnectionMiddleware',
'CachePerformanceMiddleware'
"PageViewMiddleware",
"PgHistoryContextMiddleware",
"PerformanceMiddleware",
"QueryCountMiddleware",
"DatabaseConnectionMiddleware",
"CachePerformanceMiddleware",
]

View File

@@ -13,12 +13,19 @@ from core.analytics import PageView
class RequestContextProvider(pghistory.context):
"""Custom context provider for pghistory that extracts information from the request."""
def __call__(self, request: WSGIRequest) -> dict:
return {
'user': str(request.user) if request.user and not isinstance(request.user, AnonymousUser) else None,
'ip': request.META.get('REMOTE_ADDR'),
'user_agent': request.META.get('HTTP_USER_AGENT'),
'session_key': request.session.session_key if hasattr(request, 'session') else None
"user": (
str(request.user)
if request.user and not isinstance(request.user, AnonymousUser)
else None
),
"ip": request.META.get("REMOTE_ADDR"),
"user_agent": request.META.get("HTTP_USER_AGENT"),
"session_key": (
request.session.session_key if hasattr(request, "session") else None
),
}
@@ -30,6 +37,7 @@ class PgHistoryContextMiddleware:
"""
Middleware that ensures request object is available to pghistory context.
"""
def __init__(self, get_response):
self.get_response = get_response
@@ -40,14 +48,14 @@ class PgHistoryContextMiddleware:
class PageViewMiddleware(MiddlewareMixin):
"""Middleware to track page views for DetailView-based pages."""
def process_view(self, request, view_func, view_args, view_kwargs):
# Only track GET requests
if request.method != 'GET':
if request.method != "GET":
return None
# Get view class if it exists
view_class = getattr(view_func, 'view_class', None)
view_class = getattr(view_func, "view_class", None)
if not view_class or not issubclass(view_class, DetailView):
return None
@@ -66,8 +74,8 @@ class PageViewMiddleware(MiddlewareMixin):
PageView.objects.create(
content_type=ContentType.objects.get_for_model(obj.__class__),
object_id=obj.pk,
ip_address=request.META.get('REMOTE_ADDR', ''),
user_agent=request.META.get('HTTP_USER_AGENT', '')[:512]
ip_address=request.META.get("REMOTE_ADDR", ""),
user_agent=request.META.get("HTTP_USER_AGENT", "")[:512],
)
except Exception:
# Fail silently to not interrupt the request

View File

@@ -8,131 +8,169 @@ from django.db import connection
from django.utils.deprecation import MiddlewareMixin
from django.conf import settings
performance_logger = logging.getLogger('performance')
performance_logger = logging.getLogger("performance")
logger = logging.getLogger(__name__)
class PerformanceMiddleware(MiddlewareMixin):
"""Middleware to collect performance metrics for each request"""
def process_request(self, request):
"""Initialize performance tracking for the request"""
request._performance_start_time = time.time()
request._performance_initial_queries = len(connection.queries) if hasattr(connection, 'queries') else 0
request._performance_initial_queries = (
len(connection.queries) if hasattr(connection, "queries") else 0
)
return None
def process_response(self, request, response):
"""Log performance metrics after response is ready"""
# Skip performance tracking for certain paths
skip_paths = ['/health/', '/admin/jsi18n/', '/static/', '/media/', '/__debug__/']
skip_paths = [
"/health/",
"/admin/jsi18n/",
"/static/",
"/media/",
"/__debug__/",
]
if any(request.path.startswith(path) for path in skip_paths):
return response
# Calculate metrics
end_time = time.time()
start_time = getattr(request, '_performance_start_time', end_time)
start_time = getattr(request, "_performance_start_time", end_time)
duration = end_time - start_time
initial_queries = getattr(request, '_performance_initial_queries', 0)
total_queries = len(connection.queries) - initial_queries if hasattr(connection, 'queries') else 0
initial_queries = getattr(request, "_performance_initial_queries", 0)
total_queries = (
len(connection.queries) - initial_queries
if hasattr(connection, "queries")
else 0
)
# Get content length
content_length = 0
if hasattr(response, 'content'):
if hasattr(response, "content"):
content_length = len(response.content)
elif hasattr(response, 'streaming_content'):
elif hasattr(response, "streaming_content"):
# For streaming responses, we can't easily measure content length
content_length = -1
# Build performance data
performance_data = {
'path': request.path,
'method': request.method,
'status_code': response.status_code,
'duration_ms': round(duration * 1000, 2),
'duration_seconds': round(duration, 3),
'query_count': total_queries,
'content_length_bytes': content_length,
'user_id': getattr(request.user, 'id', None) if hasattr(request, 'user') and request.user.is_authenticated else None,
'user_agent': request.META.get('HTTP_USER_AGENT', '')[:100], # Truncate user agent
'remote_addr': self._get_client_ip(request),
"path": request.path,
"method": request.method,
"status_code": response.status_code,
"duration_ms": round(duration * 1000, 2),
"duration_seconds": round(duration, 3),
"query_count": total_queries,
"content_length_bytes": content_length,
"user_id": (
getattr(request.user, "id", None)
if hasattr(request, "user") and request.user.is_authenticated
else None
),
"user_agent": request.META.get("HTTP_USER_AGENT", "")[
:100
], # Truncate user agent
"remote_addr": self._get_client_ip(request),
}
# Add query details in debug mode
if settings.DEBUG and hasattr(connection, 'queries') and total_queries > 0:
if settings.DEBUG and hasattr(connection, "queries") and total_queries > 0:
recent_queries = connection.queries[-total_queries:]
performance_data['queries'] = [
performance_data["queries"] = [
{
'sql': query['sql'][:200] + '...' if len(query['sql']) > 200 else query['sql'],
'time': float(query['time'])
"sql": (
query["sql"][:200] + "..."
if len(query["sql"]) > 200
else query["sql"]
),
"time": float(query["time"]),
}
for query in recent_queries[-10:] # Last 10 queries only
]
# Identify slow queries
slow_queries = [q for q in recent_queries if float(q['time']) > 0.1]
slow_queries = [q for q in recent_queries if float(q["time"]) > 0.1]
if slow_queries:
performance_data['slow_query_count'] = len(slow_queries)
performance_data['slowest_query_time'] = max(float(q['time']) for q in slow_queries)
performance_data["slow_query_count"] = len(slow_queries)
performance_data["slowest_query_time"] = max(
float(q["time"]) for q in slow_queries
)
# Determine log level based on performance
log_level = self._get_log_level(duration, total_queries, response.status_code)
# Log the performance data
performance_logger.log(
log_level,
f"Request performance: {request.method} {request.path} - "
f"{duration:.3f}s, {total_queries} queries, {response.status_code}",
extra=performance_data
extra=performance_data,
)
# Add performance headers for debugging (only in debug mode)
if settings.DEBUG:
response['X-Response-Time'] = f"{duration * 1000:.2f}ms"
response['X-Query-Count'] = str(total_queries)
if total_queries > 0 and hasattr(connection, 'queries'):
total_query_time = sum(float(q['time']) for q in connection.queries[-total_queries:])
response['X-Query-Time'] = f"{total_query_time * 1000:.2f}ms"
response["X-Response-Time"] = f"{duration * 1000:.2f}ms"
response["X-Query-Count"] = str(total_queries)
if total_queries > 0 and hasattr(connection, "queries"):
total_query_time = sum(
float(q["time"]) for q in connection.queries[-total_queries:]
)
response["X-Query-Time"] = f"{total_query_time * 1000:.2f}ms"
return response
def process_exception(self, request, exception):
"""Log performance data even when an exception occurs"""
end_time = time.time()
start_time = getattr(request, '_performance_start_time', end_time)
start_time = getattr(request, "_performance_start_time", end_time)
duration = end_time - start_time
initial_queries = getattr(request, '_performance_initial_queries', 0)
total_queries = len(connection.queries) - initial_queries if hasattr(connection, 'queries') else 0
performance_data = {
'path': request.path,
'method': request.method,
'status_code': 500, # Exception occurred
'duration_ms': round(duration * 1000, 2),
'query_count': total_queries,
'exception': str(exception),
'exception_type': type(exception).__name__,
'user_id': getattr(request.user, 'id', None) if hasattr(request, 'user') and request.user.is_authenticated else None,
}
performance_logger.error(
f"Request exception: {request.method} {request.path} - "
f"{duration:.3f}s, {total_queries} queries, {type(exception).__name__}: {exception}",
extra=performance_data
initial_queries = getattr(request, "_performance_initial_queries", 0)
total_queries = (
len(connection.queries) - initial_queries
if hasattr(connection, "queries")
else 0
)
performance_data = {
"path": request.path,
"method": request.method,
"status_code": 500, # Exception occurred
"duration_ms": round(duration * 1000, 2),
"query_count": total_queries,
"exception": str(exception),
"exception_type": type(exception).__name__,
"user_id": (
getattr(request.user, "id", None)
if hasattr(request, "user") and request.user.is_authenticated
else None
),
}
performance_logger.error(
f"Request exception: {
request.method} {
request.path} - "
f"{
duration:.3f}s, {total_queries} queries, {
type(exception).__name__}: {exception}",
extra=performance_data,
)
return None # Don't handle the exception, just log it
def _get_client_ip(self, request):
"""Extract client IP address from request"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
ip = x_forwarded_for.split(",")[0].strip()
else:
ip = request.META.get('REMOTE_ADDR', '')
ip = request.META.get("REMOTE_ADDR", "")
return ip
def _get_log_level(self, duration, query_count, status_code):
"""Determine appropriate log level based on performance metrics"""
# Error responses
@@ -140,7 +178,7 @@ class PerformanceMiddleware(MiddlewareMixin):
return logging.ERROR
elif status_code >= 400:
return logging.WARNING
# Performance-based log levels
if duration > 5.0: # Very slow requests
return logging.ERROR
@@ -154,50 +192,55 @@ class PerformanceMiddleware(MiddlewareMixin):
class QueryCountMiddleware(MiddlewareMixin):
"""Middleware to track and limit query counts per request"""
def __init__(self, get_response):
self.get_response = get_response
self.query_limit = getattr(settings, 'MAX_QUERIES_PER_REQUEST', 50)
self.query_limit = getattr(settings, "MAX_QUERIES_PER_REQUEST", 50)
super().__init__(get_response)
def process_request(self, request):
"""Initialize query tracking"""
request._query_count_start = len(connection.queries) if hasattr(connection, 'queries') else 0
request._query_count_start = (
len(connection.queries) if hasattr(connection, "queries") else 0
)
return None
def process_response(self, request, response):
"""Check query count and warn if excessive"""
if not hasattr(connection, 'queries'):
if not hasattr(connection, "queries"):
return response
start_count = getattr(request, '_query_count_start', 0)
start_count = getattr(request, "_query_count_start", 0)
current_count = len(connection.queries)
request_query_count = current_count - start_count
if request_query_count > self.query_limit:
logger.warning(
f"Excessive query count: {request.path} executed {request_query_count} queries "
f"(limit: {self.query_limit})",
f"Excessive query count: {
request.path} executed {request_query_count} queries "
f"(limit: {
self.query_limit})",
extra={
'path': request.path,
'method': request.method,
'query_count': request_query_count,
'query_limit': self.query_limit,
'excessive_queries': True
}
"path": request.path,
"method": request.method,
"query_count": request_query_count,
"query_limit": self.query_limit,
"excessive_queries": True,
},
)
return response
class DatabaseConnectionMiddleware(MiddlewareMixin):
"""Middleware to monitor database connection health"""
def process_request(self, request):
"""Check database connection at start of request"""
try:
# Simple connection test
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
@@ -205,64 +248,70 @@ class DatabaseConnectionMiddleware(MiddlewareMixin):
logger.error(
f"Database connection failed at request start: {e}",
extra={
'path': request.path,
'method': request.method,
'database_error': str(e)
}
"path": request.path,
"method": request.method,
"database_error": str(e),
},
)
# Don't block the request, let Django handle the database error
return None
def process_response(self, request, response):
"""Close database connections properly"""
try:
from django.db import connection
connection.close()
except Exception as e:
logger.warning(f"Error closing database connection: {e}")
return response
class CachePerformanceMiddleware(MiddlewareMixin):
"""Middleware to monitor cache performance"""
def process_request(self, request):
"""Initialize cache performance tracking"""
request._cache_hits = 0
request._cache_misses = 0
request._cache_start_time = time.time()
return None
def process_response(self, request, response):
"""Log cache performance metrics"""
cache_duration = time.time() - getattr(request, '_cache_start_time', time.time())
cache_hits = getattr(request, '_cache_hits', 0)
cache_misses = getattr(request, '_cache_misses', 0)
cache_duration = time.time() - getattr(
request, "_cache_start_time", time.time()
)
cache_hits = getattr(request, "_cache_hits", 0)
cache_misses = getattr(request, "_cache_misses", 0)
if cache_hits + cache_misses > 0:
hit_rate = (cache_hits / (cache_hits + cache_misses)) * 100
cache_data = {
'path': request.path,
'cache_hits': cache_hits,
'cache_misses': cache_misses,
'cache_hit_rate': round(hit_rate, 2),
'cache_operations': cache_hits + cache_misses,
'cache_duration': round(cache_duration * 1000, 2) # milliseconds
"path": request.path,
"cache_hits": cache_hits,
"cache_misses": cache_misses,
"cache_hit_rate": round(hit_rate, 2),
"cache_operations": cache_hits + cache_misses,
# milliseconds
"cache_duration": round(cache_duration * 1000, 2),
}
# Log cache performance
if hit_rate < 50 and cache_hits + cache_misses > 5:
logger.warning(
f"Low cache hit rate for {request.path}: {hit_rate:.1f}%",
extra=cache_data
extra=cache_data,
)
else:
logger.debug(
f"Cache performance for {request.path}: {hit_rate:.1f}% hit rate",
extra=cache_data
f"Cache performance for {
request.path}: {
hit_rate:.1f}% hit rate",
extra=cache_data,
)
return response