Files
GGThedandClaude Opus 5 66f2838402 chore(lint): interdire d avaler une exception sans laisser de trace
Regle BLE de ruff activee. Ce qu'elle enforce n'est pas "ne jamais attraper
large" : elle se satisfait d'un logger.exception. C'est exactement la
discipline visee — une frontiere peut tout avaler, a condition de laisser de
quoi distinguer un defaut d'une panne. Les cinq noqa que j'avais prepares
d'avance etaient donc inertes ; la raison reste en commentaire simple.

Ce que la regle a trouve, une fois activee :

app.py, demarrage du bot — les deux facons d'echouer, jeton invalide et
import casse, se lisaient a l'identique sur une seule ligne et aucune
n'etait diagnosticable. Passe en error avec exc_info : un club qui ne
recoit plus aucun rappel a perdu une fonctionnalite, et warning mettait ca
a cote des avis de depreciation.

services/notifications.py — le bloc webhook attrapait large autour d'un
requests.post. RequestException couvre toutes les facons dont un appel HTTP
echoue ; le reste est un defaut. La branche DM et la branche webhook etaient
en plus imbriquees dans un seul try alors qu'elles s'excluent.

logging_config.py et les deux scripts CLI gardent leur largeur, avec la
raison sur la ligne. Le filtre de journalisation est le cas ou la trace que
BLE001 reclame est precisement ce qu'il ne faut pas produire : journaliser
depuis un filtre rentre dans le meme filtre.

RUF100 (noqa inutile) n'est volontairement pas active : il ferait remonter
des directives preexistantes sans rapport avec ce chantier.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-11 16:29:13 -04:00

128 lines
4.4 KiB
Python

"""Outbound notifications.
Extracted from app/routes/users.py, where it sat between two route
definitions and pulled `requests`, `logging` and the Discord bot into a
module whose subject is HTTP handlers (ARCH-003).
Failures here are swallowed and logged on purpose: a notification that does
not reach Discord must not roll back the session it was announcing. That is
a property of the caller — a Flask request whose work is already committed —
not of the failure, which is why the breadth is argued for at each of the
two boundaries below rather than assumed (ARCH-008 / QUA-004).
The webhook branch is narrower than it was: `requests.RequestException`
covers every way an HTTP call can fail, and anything else coming out of it
is a defect worth seeing.
"""
import logging
import os
import requests
logger = logging.getLogger(__name__)
#: Either a webhook URL, or a bare Discord user id to DM instead.
DISCORD_WEBHOOK_URL = os.environ.get('DISCORD_WEBHOOK_URL', '')
def send_discord_notification(
player_name,
points,
date_str,
start_time_str,
end_time_str,
team_name,
coach_name,
coach_discord,
coach_discord_id,
request_id=None,
):
"""Send a Discord notification for a One on One request.
Args:
player_name: Who is asking.
points: Free-text discussion points, possibly empty.
date_str, start_time_str, end_time_str: Already formatted for display.
team_name: The player's team, or None.
coach_name: Who is being asked.
coach_discord: The coach's Discord handle, for the webhook footer.
coach_discord_id: The coach's Discord snowflake, for a direct message.
request_id: OneOnOneRequest primary key, so reactions can find it back.
"""
if coach_discord_id:
try:
from app.discord_bot import send_one_on_one_dm
send_one_on_one_dm(
coach_name=coach_name,
coach_discord_id=coach_discord_id,
player_name=player_name,
team_name=team_name,
date_str=date_str,
start_time=start_time_str,
end_time=end_time_str,
points=points,
request_id=request_id,
)
except Exception: # noqa: BLE001 — the request that booked the meeting is already committed
logger.warning('Failed to hand the One on One DM to the bot', exc_info=True)
if not DISCORD_WEBHOOK_URL:
return
# A bare snowflake here means "DM this person instead", and only when the
# coach has no id of their own. Queueing it cannot raise (see _enqueue).
if DISCORD_WEBHOOK_URL.isdigit():
if not coach_discord_id:
from app.discord_bot import send_one_on_one_dm
send_one_on_one_dm(
coach_name=coach_name,
coach_discord_id=DISCORD_WEBHOOK_URL,
player_name=player_name,
team_name=team_name,
date_str=date_str,
start_time=start_time_str,
end_time=end_time_str,
points=points,
)
return
embed = {
"embeds": [
{
"title": "One on One Request",
"color": 3447003,
"fields": [
{"name": "Player", "value": player_name, "inline": True},
{
"name": "Team",
"value": team_name or "Unknown Team",
"inline": True,
},
{"name": "Date", "value": date_str, "inline": True},
{
"name": "Time",
"value": f"{start_time_str} - {end_time_str}",
"inline": True,
},
{
"name": "Discussion Points",
"value": points or "No specific points provided",
"inline": False,
},
],
"footer": {
"text": f"Coach: {coach_name}"
+ (f" (Discord: {coach_discord})" if coach_discord else ""),
},
}
],
}
try:
requests.post(DISCORD_WEBHOOK_URL, json=embed, timeout=5)
except requests.RequestException as exc:
logger.warning('Failed to post the One on One webhook: %s', exc)