83 lines
2.9 KiB
Python
83 lines
2.9 KiB
Python
"""
|
|
extensions.py
|
|
=============
|
|
Shared Flask extension instances (SQLAlchemy db + AppLogger).
|
|
|
|
All Blueprints import from here to avoid circular imports.
|
|
|
|
Initialization order (enforced in app.py):
|
|
1. db = SQLAlchemy() -- created here at module level
|
|
2. app.py configures Flask app
|
|
3. db.init_app(app) -- called in app.py
|
|
4. set_db(db) -- unpacks model classes
|
|
5. init_logger(app, db) -- binds logger_handler here
|
|
6. Blueprints are registered
|
|
"""
|
|
|
|
from flask_sqlalchemy import SQLAlchemy
|
|
from logger_handler import AppLogger
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Database — single shared instance
|
|
# ---------------------------------------------------------------------------
|
|
db = SQLAlchemy()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Application-level logger
|
|
# ---------------------------------------------------------------------------
|
|
# `logger_handler` is a PROXY, not the AppLogger itself.
|
|
#
|
|
# Modules imported before create_app() calls init_logger() — utils/helpers.py,
|
|
# utils/template_helpers.py, utils/geocoding.py — bind whatever object this name
|
|
# holds at import time, and `from extensions import logger_handler` copies the
|
|
# reference. While that was a plain None, any later call such as
|
|
# `logger_handler.logger.warning(...)` raised
|
|
# AttributeError: 'NoneType' object has no attribute 'logger'
|
|
# (Sept 16 2026: a blocked project manager got a 500 instead of a redirect,
|
|
# because the access-denied path logs a warning.)
|
|
#
|
|
# The proxy forwards to the real AppLogger once init_logger() has built it, and
|
|
# to a plain stdlib logger before that, so logging can never break a request.
|
|
import logging as _logging
|
|
|
|
_logger_instance = None # the real AppLogger, set by init_logger()
|
|
|
|
|
|
class _FallbackLogger:
|
|
"""Stand-in used before init_logger() has run: logs, never raises."""
|
|
|
|
def __init__(self):
|
|
self.logger = _logging.getLogger('qr_attendance_app')
|
|
|
|
def __getattr__(self, name):
|
|
def _noop(*args, **kwargs):
|
|
self.logger.debug(f"{name}() called before init_logger()")
|
|
return _noop
|
|
|
|
|
|
_fallback_logger = _FallbackLogger()
|
|
|
|
|
|
class _LoggerHandlerProxy:
|
|
"""Forwards every attribute to the real AppLogger, or to _FallbackLogger."""
|
|
|
|
def __getattr__(self, name):
|
|
return getattr(_logger_instance or _fallback_logger, name)
|
|
|
|
def __repr__(self):
|
|
return f"<logger_handler proxy -> {_logger_instance or _fallback_logger!r}>"
|
|
|
|
|
|
logger_handler = _LoggerHandlerProxy()
|
|
|
|
|
|
def init_logger(app, database) -> AppLogger:
|
|
"""
|
|
Instantiate AppLogger and bind it behind the module-level ``logger_handler``
|
|
proxy, so every module that did ``from extensions import logger_handler`` —
|
|
whenever it was imported — ends up using this fully-initialized instance.
|
|
"""
|
|
global _logger_instance
|
|
_logger_instance = AppLogger(app, database)
|
|
return _logger_instance
|