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?
173 lines
6.2 KiB
Python
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") |