Files
3cloud-backend/app/__init__.py
T
coryHawkvelt 070429ef46 Implimented a ping\pong monitoring system work worker liveness. Will handle the situation where a websocket server crashes.
The only thing this doesnt handle is when a client goes into offline due to a transient link failure but it comes back online. We want to avoid updating the DB every time a pong is RX'd.. Perhaps we do a seocndary corss checks with offline hosts?
2025-07-26 02:52:56 +09:30

173 lines
6.2 KiB
Python

"""
Entry point for the xCloudify Flask application.
* Loads environment variables from a `.env` file.
* Configures Flask, SQLAlchemy, Celery, and Cloudflare settings.
* Masks sensitive values in logs for security.
"""
print("-----In init----------")
import uuid
import os
import pymysql
from dotenv import load_dotenv
from flask import Flask, g, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from celery import Celery, Task
from logger import logger
from app.utils.standard_responses import api_response
# --------------------------------------------------------------------------- #
# 0. Environment setup #
# --------------------------------------------------------------------------- #
load_dotenv()
logger.info("Environment variables loaded from .env")
# --------------------------------------------------------------------------- #
# 1. Flask application & database #
# --------------------------------------------------------------------------- #
app: Flask = Flask(__name__)
app.config.update(
SQLALCHEMY_DATABASE_URI="mysql://root:password@172.17.0.1:3306/theapi",
WEBSOCKET_SERVER_URL="http://172.17.0.1:6001/api/create_task",
# Celery settings (override via ENV)
CELERY_BROKER_URL=os.getenv("CELERY_BROKER_URL", "redis://172.17.0.1:6379/0"), # Redis Database 0 for celery broker tasks
CELERY_RESULT_BACKEND=os.getenv("CELERY_RESULT_BACKEND", "redis://172.17.0.1:6379/1"), # Redis Database 1 for celery results
)
# --------------------------------------------------------------------------- #
# 2. Cloudflare configuration #
# --------------------------------------------------------------------------- #
def _mask(value: str, visible: int = 4) -> str:
"""
Return a partially masked version of a sensitive string.
Args:
value (str): The string to mask.
visible (int): Number of visible characters to keep at the start.
Returns:
str: Masked string suitable for logging.
"""
if not value:
return ""
return f"{value[:visible]}{'*' * (len(value) - visible)}"
app.config["CLOUDFLARE_API_TOKEN"] = os.getenv("CLOUDFLARE_API_TOKEN")
app.config["CLOUDFLARE_ACCOUNT_ID"] = os.getenv("CLOUDFLARE_ACCOUNT_ID")
app.config["CLOUDFLARE_ZONE_ID"] = os.getenv("CLOUDFLARE_ZONE_ID")
app.config["PING_HEARTBEAT_TIMEOUT_SECONDS"] = os.getenv("PING_HEARTBEAT_TIMEOUT_SECONDS",5) # How long before a Websocket PING\PONG is classed as a failure and triggers a worker offline event
app.config["REDIS_URL"] = os.getenv("REDIS_URL","redis://172.17.0.1:6379/2") # Redis Database 2 for operational tasks like websocket server and PING logging
logger.info(
"Cloudflare configuration set "
f"(token={_mask(app.config['CLOUDFLARE_API_TOKEN'])}, "
f"account_id={_mask(app.config['CLOUDFLARE_ACCOUNT_ID'])})"
f"zone_id={_mask(app.config['CLOUDFLARE_ZONE_ID'])})"
)
pymysql.install_as_MySQLdb()
db: SQLAlchemy = SQLAlchemy(app)
migrate: Migrate = Migrate(app, db)
app.secret_key = "your_secret_key_here"
# --------------------------------------------------------------------------- #
# 2. Celery initialisation #
# --------------------------------------------------------------------------- #
def _make_celery(flask_app: Flask) -> Celery:
"""
Create a Celery instance that shares the Flask application context.
Every task will inherit `current_app` and can use the database session
without manual `app.app_context()` juggling.
"""
celery = Celery(
flask_app.import_name,
broker=flask_app.config["CELERY_BROKER_URL"],
backend=flask_app.config["CELERY_RESULT_BACKEND"],
)
celery.conf.update(flask_app.config)
class ContextTask(Task):
"""Celery Task base-class that wraps task execution in app_context."""
abstract = True
def __call__(self, *args, **kwargs):
with flask_app.app_context():
return super().__call__(*args, **kwargs)
celery.Task = ContextTask
return celery
celery_app: Celery = _make_celery(app)
celery_app.autodiscover_tasks(["app.tasks"], force=True)
app.extensions["celery"] = celery_app
# --------------------------------------------------------------------------- #
# 3. Blueprints and standard middleware #
# --------------------------------------------------------------------------- #
from app.controller import api_bp, ui_bp, celery_bp # noqa: E402 circular OK
app.register_blueprint(api_bp)
app.register_blueprint(ui_bp)
app.register_blueprint(celery_bp)
# ---------- Error-handlers, request IDs, context processors --------------- #
@app.errorhandler(500)
def handle_internal_server_error(error):
logger.error(
"Unhandled exception for %s %s (request_id=%s)",
request.method,
request.path,
getattr(g, "request_id", "n/a"),
exc_info=True,
)
return api_response(
success=False,
status=500,
message="Internal server error",
error_type="INTERNAL_SERVER_ERROR",
error_details={"path": request.path},
)
@app.errorhandler(404)
def not_found(e):
return api_response(
success=False, status=404, message=str(e), error_type="RESOURCE_NOT_FOUND"
)
@app.before_request
def assign_request_id():
g.request_id = str(uuid.uuid4())
@app.after_request
def log_request_id(response):
response.headers["X-Request-ID"] = g.request_id
return response
@app.context_processor
def inject_user_roles():
try:
user_details = {"name": "user.name", "roles": ["user_roles"]}
if "current_user" in g:
user_details = {
"id": str(g.current_user.id),
"friendly_id": g.current_user.friendly_id,
"email": g.current_user.email,
"name": g.current_user.name,
"roles": g.current_user.roles_text,
}
return dict(user_details=user_details)
except Exception as exc:
logger.error("inject_user_roles - %s", exc)
return {}
logger.debug("__init__ complete")