Skip to content

Commit 65b393e

Browse files
authored
Merge branch 'main' into sqlmodel
2 parents 0f0916e + a598bfd commit 65b393e

File tree

16 files changed

+292
-659
lines changed

16 files changed

+292
-659
lines changed
File renamed without changes.

.pre-commit-config.yaml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,27 +31,29 @@ repos:
3131
# 3. Main Linter (with auto-fix)
3232
- repo: https://github.com/astral-sh/ruff-pre-commit
3333
# Ruff version should match the one in pyproject.toml
34-
rev: v0.11.7 # Use the same Ruff version tag as formatter
34+
rev: v0.11.8 # Use the same Ruff version tag as formatter
3535
hooks:
3636
- id: ruff
3737
args: [--fix]
3838

3939
# 4. Main Formatter (after linting/fixing)
4040
- repo: https://github.com/astral-sh/ruff-pre-commit
4141
# Ruff version should match the one in pyproject.toml
42-
rev: v0.11.7
42+
rev: v0.11.8
4343
hooks:
4444
- id: ruff-format
4545

4646
# 5. Project Config / Dependency Checks
47-
- repo: https://github.com/python-poetry/poetry
48-
rev: 2.1.2 # Use the latest tag from the repo
49-
hooks:
50-
- id: poetry-check
47+
# TODO: Disabled due to a issue with "No module named 'jinja2'"
48+
# relevant: https://github.com/mtkennerly/poetry-dynamic-versioning/issues/13
49+
#- repo: https://github.com/python-poetry/poetry
50+
# rev: 2.1.3 # Use the latest tag from the repo
51+
# hooks:
52+
# - id: poetry-check
5153

5254
# 6. Security Check
5355
- repo: https://github.com/gitleaks/gitleaks
54-
rev: v8.24.3 # Use the latest tag from the repo
56+
rev: v8.25.1 # Use the latest tag from the repo
5557
hooks:
5658
- id: gitleaks
5759

docs/mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ plugins:
229229
# https://mkdocstrings.github.io/python/usage/configuration/docstrings/#merge_init_into_class
230230
merge_init_into_class: true
231231
# https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_if_no_docstring
232-
show_if_no_docstring: true
232+
show_if_no_docstring: false
233233
# https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_docstring_attributes
234234
show_docstring_attributes: true
235235
# https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_docstring_functions

pyproject.toml

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,6 @@ version = "0.0.0"
2222
enable = true
2323
metadata = true
2424
dirty = true
25-
# TODO: dirty causes issues with dockerization and always returns true, disabling for now
26-
#format-jinja = """
27-
# {%- if distance == 0 -%}
28-
# {{ serialize_pep440(base, stage, revision, metadata=[] + (["dirty"] if dirty else [])) }}
29-
# {%- elif branch == "main" -%}
30-
# {{ serialize_pep440(base, stage, revision, dev=distance, metadata=[commit] + (["dirty"] if dirty else [])) }}
31-
# {%- else -%}
32-
# {{ serialize_pep440(base, stage, revision, dev=distance, metadata=[commit, branch_escaped] + (["dirty"] if dirty else [])) }}
33-
# {%- endif -%}
34-
#"""
3525
format-jinja = """
3626
{%- if distance == 0 -%}
3727
{{ serialize_pep440(base, stage, revision) }}
@@ -79,6 +69,7 @@ click = "^8.1.8"
7969
levenshtein = "^0.27.1"
8070
sqlmodel = "^0.0.24"
8171
alembic = "^1.15.2"
72+
jinja2 = "^3.1.6"
8273

8374
[tool.poetry.group.dev.dependencies]
8475
pre-commit = ">=4.0.0"

tux/app.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""TuxApp: Orchestration and lifecycle management for the Tux Discord bot."""
2+
3+
import asyncio
4+
import signal
5+
from types import FrameType
6+
7+
import discord
8+
import sentry_sdk
9+
from loguru import logger
10+
11+
from tux.bot import Tux
12+
from tux.help import TuxHelp
13+
from tux.utils.config import CONFIG
14+
from tux.utils.env import get_current_env
15+
16+
17+
async def get_prefix(bot: Tux, message: discord.Message) -> list[str]:
18+
"""Resolve the command prefix for a guild or use the default prefix."""
19+
prefix: str | None = None
20+
if message.guild:
21+
try:
22+
from tux.database.controllers import DatabaseController
23+
24+
prefix = await DatabaseController().guild_config.get_guild_prefix(message.guild.id)
25+
except Exception as e:
26+
logger.error(f"Error getting guild prefix: {e}")
27+
return [prefix or CONFIG.DEFAULT_PREFIX]
28+
29+
30+
class TuxApp:
31+
"""Orchestrates the startup, shutdown, and environment for the Tux bot."""
32+
33+
def __init__(self):
34+
"""Initialize the TuxApp with no bot instance yet."""
35+
self.bot = None
36+
37+
def run(self) -> None:
38+
"""Run the Tux bot application (entrypoint for CLI)."""
39+
asyncio.run(self.start())
40+
41+
def setup_sentry(self) -> None:
42+
"""Initialize Sentry for error monitoring and tracing."""
43+
if not CONFIG.SENTRY_DSN:
44+
logger.warning("No Sentry DSN configured, skipping Sentry setup")
45+
return
46+
47+
logger.info("Setting up Sentry...")
48+
49+
try:
50+
sentry_sdk.init(
51+
dsn=CONFIG.SENTRY_DSN,
52+
release=CONFIG.BOT_VERSION,
53+
environment=get_current_env(),
54+
enable_tracing=True,
55+
attach_stacktrace=True,
56+
send_default_pii=False,
57+
traces_sample_rate=1.0,
58+
profiles_sample_rate=1.0,
59+
)
60+
61+
logger.info(f"Sentry initialized: {sentry_sdk.is_initialized()}")
62+
63+
except Exception as e:
64+
logger.error(f"Failed to initialize Sentry: {e}")
65+
66+
def setup_signals(self) -> None:
67+
"""Set up signal handlers for graceful shutdown."""
68+
signal.signal(signal.SIGTERM, self.handle_sigterm)
69+
signal.signal(signal.SIGINT, self.handle_sigterm)
70+
71+
def handle_sigterm(self, signum: int, frame: FrameType | None) -> None:
72+
"""Handle SIGTERM/SIGINT by raising KeyboardInterrupt for graceful shutdown."""
73+
logger.info(f"Received signal {signum}")
74+
75+
if sentry_sdk.is_initialized():
76+
with sentry_sdk.push_scope() as scope:
77+
scope.set_tag("signal.number", signum)
78+
scope.set_tag("lifecycle.event", "termination_signal")
79+
80+
sentry_sdk.add_breadcrumb(
81+
category="lifecycle",
82+
message=f"Received termination signal {signum}",
83+
level="info",
84+
)
85+
86+
raise KeyboardInterrupt
87+
88+
def validate_config(self) -> bool:
89+
"""Validate that all required configuration is present."""
90+
if not CONFIG.BOT_TOKEN:
91+
logger.critical("No bot token provided. Set DEV_BOT_TOKEN or PROD_BOT_TOKEN in your .env file.")
92+
return False
93+
94+
return True
95+
96+
async def start(self) -> None:
97+
"""Start the Tux bot, handling setup, errors, and shutdown."""
98+
self.setup_sentry()
99+
100+
self.setup_signals()
101+
102+
if not self.validate_config():
103+
return
104+
105+
self.bot = Tux(
106+
command_prefix=get_prefix,
107+
strip_after_prefix=True,
108+
case_insensitive=True,
109+
intents=discord.Intents.all(),
110+
owner_ids={CONFIG.BOT_OWNER_ID, *CONFIG.SYSADMIN_IDS},
111+
allowed_mentions=discord.AllowedMentions(everyone=False),
112+
help_command=TuxHelp(),
113+
activity=None,
114+
status=discord.Status.online,
115+
)
116+
117+
try:
118+
await self.bot.start(CONFIG.BOT_TOKEN, reconnect=True)
119+
120+
except KeyboardInterrupt:
121+
logger.info("Shutdown requested (KeyboardInterrupt)")
122+
except Exception as e:
123+
logger.critical(f"Bot failed to start: {e}")
124+
await self.shutdown()
125+
126+
finally:
127+
await self.shutdown()
128+
129+
async def shutdown(self) -> None:
130+
"""Gracefully shut down the bot and flush Sentry."""
131+
if self.bot and not self.bot.is_closed():
132+
await self.bot.shutdown()
133+
134+
if sentry_sdk.is_initialized():
135+
sentry_sdk.flush()
136+
await asyncio.sleep(0.1)
137+
138+
logger.info("Shutdown complete")

0 commit comments

Comments
 (0)