Skip to content

Commit

Permalink
- Move API to separate Python script, run as part of PM2
Browse files Browse the repository at this point in the history
  • Loading branch information
nwithan8 committed Jan 21, 2025
1 parent 788ff27 commit e41d844
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 43 deletions.
68 changes: 68 additions & 0 deletions api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import argparse

from flask import (
Flask,
)

import modules.logs as logging
from modules.errors import determine_exit_code
from consts import (
DEFAULT_LOG_DIR,
DEFAULT_DATABASE_PATH,
CONSOLE_LOG_LEVEL,
FILE_LOG_LEVEL,
FLASK_ADDRESS,
FLASK_PORT,
FLASK_DATABASE_PATH,
)
from api.routes.index import index
from api.routes.webhooks.tautulli.index import webhooks_tautulli

APP_NAME = "API"

# Parse CLI arguments
parser = argparse.ArgumentParser(description="Tauticord API - API for Tauticord")
"""
Bot will use config, in order:
1. Explicit config file path provided as CLI argument, if included, or
2. Default config file path, if exists, or
3. Environmental variables
"""
parser.add_argument("-l", "--log", help="Log file directory", default=DEFAULT_LOG_DIR)
parser.add_argument("-d", "--database", help="Path to database file", default=DEFAULT_DATABASE_PATH)
args = parser.parse_args()


def run_with_potential_exit_on_error(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logging.fatal(f"Fatal error occurred. Shutting down: {e}")
exit_code = determine_exit_code(exception=e)
logging.fatal(f"Exiting with code {exit_code}")
exit(exit_code)

return wrapper


@run_with_potential_exit_on_error
def set_up_logging():
logging.init(app_name=APP_NAME,
console_log_level=CONSOLE_LOG_LEVEL,
log_to_file=True,
log_file_dir=args.log,
file_log_level=FILE_LOG_LEVEL)


# Register Flask blueprints
application = Flask(APP_NAME)
application.config[FLASK_DATABASE_PATH] = args.database

application.register_blueprint(index)
application.register_blueprint(webhooks_tautulli)

if __name__ == "__main__":
set_up_logging()

application.run(host=FLASK_ADDRESS, port=FLASK_PORT, debug=False, use_reloader=False)
Empty file added api/__init__.py
Empty file.
Empty file added api/controllers/__init__.py
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
jsonify,
request as flask_request,
)
from sqlalchemy.testing.plugin.plugin_base import logging
import modules.logs as logging

import modules.database.repository as db
from modules.discord.bot import Bot
from modules.webhooks import RecentlyAddedWebhook


Expand All @@ -16,7 +15,7 @@ def __init__(self):
pass

@staticmethod
def process_tautulli_recently_added_webhook(request: flask_request, bot: Bot, database_path: str) -> [Union[str, None], int]:
def process_tautulli_recently_added_webhook(request: flask_request, database_path: str) -> [Union[str, None], int]:
"""
Process a configured recently-added webhook from Tautulli.
Return an empty response and a 200 status code back to Tautulli as confirmation.
Expand Down
27 changes: 27 additions & 0 deletions api/routes/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from flask import (
Blueprint,
request,
Response as FlaskResponse,
)

from consts import (
FLASK_POST,
FLASK_GET,
)

index = Blueprint("index", __name__, url_prefix="")


@index.route("/ping", methods=[FLASK_GET])
def ping():
return 'Pong!', 200


@index.route("/hello", methods=[FLASK_GET])
def hello_world():
return 'Hello, World!', 200


@index.route("/health", methods=[FLASK_GET])
def health_check():
return 'OK', 200
22 changes: 22 additions & 0 deletions api/routes/webhooks/tautulli/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from flask import (
Blueprint,
request as flask_request,
Response as FlaskResponse,
current_app,
)

from api.controllers.webhook_processor import WebhookProcessor
from consts import (
FLASK_POST,
FLASK_GET,
FLASK_DATABASE_PATH,
)

webhooks_tautulli = Blueprint("tautulli", __name__, url_prefix="/webhooks/tautulli")


@webhooks_tautulli.route("/recently_added", methods=[FLASK_POST])
def tautulli_webhook():
database_path = current_app.config[FLASK_DATABASE_PATH]
return WebhookProcessor.process_tautulli_recently_added_webhook(request=flask_request,
database_path=database_path)
3 changes: 3 additions & 0 deletions consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
GITHUB_REPO_MASTER_BRANCH = "master"
FLASK_ADDRESS = "0.0.0.0"
FLASK_PORT = 8283
FLASK_POST = "POST"
FLASK_GET = "GET"
FLASK_DATABASE_PATH = "FLASK_DATABASE_PATH"
11 changes: 10 additions & 1 deletion ecosystem.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
"exec_mode": "fork",
"instances": 1
},
{
"name": "api",
"interpreter": "/app/venv/bin/python",
"script": "api.py",
"autorestart": true,
"exec_mode": "fork",
"instances": 1,
"stop_exit_codes": [302]
},
{
"name": "tauticord",
"interpreter": "/app/venv/bin/python",
Expand All @@ -22,7 +31,7 @@
// 101 - Discord login failed
// 102 - Missing privileged intents
// 301 - Migrations failed
"stop_exit_codes": [101, 102, 301]
"stop_exit_codes": [101, 102, 301, 302]
}
]
}
9 changes: 9 additions & 0 deletions modules/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ def __init__(self, message: str):
super().__init__(code=303, message=message)


class TauticordAPIFailure(TauticordException):
"""
Raised when an error occurs during an API call
"""

def __init__(self, message: str):
super().__init__(code=304, message=message)


def determine_exit_code(exception: Exception) -> int:
"""
Determine the exit code based on the exception that was thrown
Expand Down
39 changes: 0 additions & 39 deletions run.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import argparse
import os
import threading

from flask import (
Flask,
request as flask_request,
)
from pydantic import ValidationError as PydanticValidationError

import modules.logs as logging
Expand All @@ -18,8 +13,6 @@
DEFAULT_DATABASE_PATH,
CONSOLE_LOG_LEVEL,
FILE_LOG_LEVEL,
FLASK_ADDRESS,
FLASK_PORT,
)
from migrations.migration_manager import MigrationManager
from modules import versioning
Expand All @@ -42,7 +35,6 @@
KEY_RUN_ARGS_MONITOR_PATH,
KEY_RUN_ARGS_DATABASE_PATH,
)
from modules.webhook_processor import WebhookProcessor

# Parse CLI arguments
parser = argparse.ArgumentParser(description="Tauticord - Discord bot for Tautulli")
Expand Down Expand Up @@ -201,36 +193,6 @@ def set_up_discord_bot(config: Config,
)


@run_with_potential_exit_on_error
def start_api(config: Config, discord_bot: Bot, database_path: str) -> [Flask, threading.Thread]:
api = Flask(APP_NAME)

@api.route('/ping', methods=['GET'])
def ping():
return 'Pong!', 200

@api.route('/hello', methods=['GET'])
def hello_world():
return 'Hello, World!', 200

@api.route('/health', methods=['GET'])
def health_check():
return 'OK', 200

@api.route('/webhooks/tautulli/recently_added', methods=['POST'])
def tautulli_webhook():
return WebhookProcessor.process_tautulli_recently_added_webhook(request=flask_request,
bot=discord_bot,
database_path=database_path)

flask_thread = threading.Thread(
target=lambda: api.run(host=FLASK_ADDRESS, port=FLASK_PORT, debug=False, use_reloader=False))
logging.info("Starting Flask server")
flask_thread.start()

return api, flask_thread


@run_with_potential_exit_on_error
def start(discord_bot: Bot) -> None:
# Connect the bot to Discord (last step, since it will block and trigger all the sub-services)
Expand All @@ -251,5 +213,4 @@ def start(discord_bot: Bot) -> None:
tautulli_connector=_tautulli_connector,
emoji_manager=_emoji_manager,
analytics=_analytics)
_api, _flask_thread = start_api(config=_config, discord_bot=_discord_bot, database_path=args.database)
start(discord_bot=_discord_bot)

0 comments on commit e41d844

Please sign in to comment.