123 lines
4.4 KiB
Python
123 lines
4.4 KiB
Python
import logging
|
|
import os
|
|
from flask import Flask
|
|
from app.config import config
|
|
from app.extensions import db, login_manager, migrate, csrf
|
|
from app.utils.formatters import format_currency, format_percent, format_large_number
|
|
|
|
|
|
def _setup_logging(app):
|
|
"""
|
|
Ensure app.* module loggers emit at INFO level.
|
|
|
|
Under Gunicorn the root logger already has handlers (pointing to Gunicorn's
|
|
error log / stderr) but its level is WARNING, so INFO records are dropped
|
|
before they reach any handler. We fix that by:
|
|
1. Reusing Gunicorn's handlers on the 'app' namespace logger so records
|
|
go to the same destination as Gunicorn's own logs.
|
|
2. Falling back to a plain stderr StreamHandler in dev / direct-run mode.
|
|
"""
|
|
import sys
|
|
|
|
app_log = logging.getLogger('app')
|
|
app_log.setLevel(logging.INFO)
|
|
|
|
if app_log.handlers:
|
|
return # already configured (e.g. running tests)
|
|
|
|
gunicorn_handlers = logging.getLogger('gunicorn.error').handlers
|
|
if gunicorn_handlers:
|
|
# Running under Gunicorn — attach its handlers so our logs land in the
|
|
# same error log file that Gunicorn writes to.
|
|
for h in gunicorn_handlers:
|
|
app_log.addHandler(h)
|
|
else:
|
|
# Dev / direct python run — stderr is fine.
|
|
handler = logging.StreamHandler(sys.stderr)
|
|
handler.setFormatter(logging.Formatter(
|
|
'%(asctime)s %(levelname)s %(name)s: %(message)s'
|
|
))
|
|
app_log.addHandler(handler)
|
|
|
|
app_log.propagate = False # avoid double-printing via root
|
|
|
|
|
|
def create_app(config_name=None):
|
|
if config_name is None:
|
|
config_name = os.environ.get('FLASK_ENV', 'development')
|
|
config_name = 'production' if config_name == 'production' else 'development'
|
|
|
|
app = Flask(__name__)
|
|
app.config.from_object(config[config_name])
|
|
|
|
# Wire app.* loggers into Gunicorn's error log (or stderr in dev).
|
|
# basicConfig is a no-op under Gunicorn because Gunicorn already
|
|
# installed root handlers before our app is imported — so we must
|
|
# attach handlers explicitly.
|
|
_setup_logging(app)
|
|
|
|
db.init_app(app)
|
|
login_manager.init_app(app)
|
|
migrate.init_app(app, db)
|
|
csrf.init_app(app)
|
|
|
|
from app.routes.auth import auth_bp
|
|
from app.routes.dashboard import dashboard_bp
|
|
from app.routes.accounts import accounts_bp
|
|
from app.routes.categories import categories_bp
|
|
from app.routes.transactions import transactions_bp
|
|
from app.routes.budgets import budgets_bp
|
|
from app.routes.goals import goals_bp
|
|
from app.routes.investments import investments_bp
|
|
from app.routes.ai import ai_bp
|
|
from app.routes.reports import reports_bp
|
|
from app.routes.settings import settings_bp
|
|
from app.routes.teller import teller_bp
|
|
|
|
app.register_blueprint(auth_bp)
|
|
app.register_blueprint(dashboard_bp)
|
|
app.register_blueprint(accounts_bp)
|
|
app.register_blueprint(categories_bp)
|
|
app.register_blueprint(transactions_bp)
|
|
app.register_blueprint(budgets_bp)
|
|
app.register_blueprint(goals_bp)
|
|
app.register_blueprint(investments_bp)
|
|
app.register_blueprint(ai_bp)
|
|
app.register_blueprint(reports_bp)
|
|
app.register_blueprint(settings_bp)
|
|
app.register_blueprint(teller_bp)
|
|
|
|
with app.app_context():
|
|
from app.models import (
|
|
User, Account, Category, Receipt, RecurringRule,
|
|
Transaction, Budget, Goal, GoalContribution,
|
|
Investment, InvestmentTransaction, NetWorthSnapshot,
|
|
AiInsight, FxRate
|
|
)
|
|
from app.models.teller_enrollment import TellerEnrollment, TellerAccount
|
|
|
|
app.jinja_env.globals['format_currency'] = format_currency
|
|
app.jinja_env.globals['format_percent'] = format_percent
|
|
app.jinja_env.globals['format_large_number'] = format_large_number
|
|
|
|
@app.template_filter('currency')
|
|
def currency_filter(value, symbol=None):
|
|
return format_currency(value, symbol)
|
|
|
|
@app.template_filter('percent')
|
|
def percent_filter(value):
|
|
return format_percent(value)
|
|
|
|
@app.template_filter('shares')
|
|
def shares_filter(value):
|
|
"""Format share quantity — strips trailing zeros, keeps up to 8 decimal places."""
|
|
try:
|
|
f = float(value)
|
|
except (TypeError, ValueError):
|
|
return '0'
|
|
# Format to 8 decimal places then strip trailing zeros
|
|
s = '{:.8f}'.format(f).rstrip('0').rstrip('.')
|
|
return s if s else '0'
|
|
|
|
return app
|